Passed
Push — new-api ( 44902a...f133eb )
by Sebastian
04:49
created

Date::render()   F

Complexity

Conditions 27
Paths 475

Size

Total Lines 88
Code Lines 53

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 49
CRAP Score 27.0058

Importance

Changes 0
Metric Value
cc 27
eloc 53
c 0
b 0
f 0
nc 475
nop 1
dl 0
loc 88
ccs 49
cts 50
cp 0.98
crap 27.0058
rs 0.7291

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/*
3
 * citeproc-php
4
 *
5
 * @link        http://github.com/seboettg/citeproc-php for the source repository
6
 * @copyright   Copyright (c) 2016 Sebastian Böttger.
7
 * @license     https://opensource.org/licenses/MIT
8
 */
9
10
namespace Seboettg\CiteProc\Rendering\Date;
11
12
use Exception;
13
use Seboettg\CiteProc\CiteProc;
14
use Seboettg\CiteProc\Exception\CiteProcException;
15
use Seboettg\CiteProc\Exception\InvalidStylesheetException;
16
use Seboettg\CiteProc\Rendering\Date\DateRange\DateRangeRenderer;
17
use Seboettg\CiteProc\Rendering\Observer\RenderingObserver;
18
use Seboettg\CiteProc\Rendering\Observer\RenderingObserverTrait;
19
use Seboettg\CiteProc\Styles\StylesRenderer;
20
use Seboettg\CiteProc\Util;
21
use Seboettg\Collection\ArrayList;
22
use SimpleXMLElement;
23
24
/**
25
 * Class Date
26
 * @package Seboettg\CiteProc\Rendering
27
 *
28
 * @author Sebastian Böttger <[email protected]>
29
 */
30
class Date implements RenderingObserver
31
{
32
    use RenderingObserverTrait;
33
34
    // bitmask: ymd
35
    const DATE_RANGE_STATE_NONE         = 0; // 000
36
    const DATE_RANGE_STATE_DAY          = 1; // 001
37
    const DATE_RANGE_STATE_MONTH        = 2; // 010
38
    const DATE_RANGE_STATE_MONTHDAY     = 3; // 011
39
    const DATE_RANGE_STATE_YEAR         = 4; // 100
40
    const DATE_RANGE_STATE_YEARDAY      = 5; // 101
41
    const DATE_RANGE_STATE_YEARMONTH    = 6; // 110
42
    const DATE_RANGE_STATE_YEARMONTHDAY = 7; // 111
43
44 74
    public static function factory(SimpleXMLElement $node)
45
    {
46 74
        $variable = $form = $datePartsAttribute = "";
47 74
        $dateParts = new ArrayList();
48 74
        foreach ($node->attributes() as $attribute) {
49 74
            switch ($attribute->getName()) {
50 74
                case 'form':
51 37
                    $form = (string) $attribute;
52 37
                    break;
53 74
                case 'variable':
54 74
                    $variable = (string) $attribute;
55 74
                    break;
56 47
                case 'date-parts':
57 74
                    $datePartsAttribute = (string) $attribute;
58
            }
59
        }
60 74
        foreach ($node->children() as $child) {
61 58
            if ($child->getName() === "date-part") {
62 58
                $datePartName = (string) $child->attributes()["name"];
63 58
                $dateParts->set($form . "-" . $datePartName, Util\Factory::create($child));
64
            }
65
        }
66 74
        $stylesRenderer = StylesRenderer::factory($node);
67 74
        return new self(
68 74
            $variable,
69 74
            $form,
70 74
            $datePartsAttribute,
71 74
            $dateParts,
72 74
            $stylesRenderer
73
        );
74
    }
75
76 74
    public function __construct(
77
        string $variable,
78
        string $form,
79
        string $datePartsAttribute,
80
        ArrayList $dateParts,
81
        StylesRenderer $stylesRenderer
82
    ) {
83 74
        $this->variable = $variable;
84 74
        $this->form = $form;
85 74
        $this->datePartsAttribute = $datePartsAttribute;
86 74
        $this->dateParts = $dateParts;
87 74
        $this->stylesRenderer = $stylesRenderer;
88 74
    }
89
90
    private static $localizedDateFormats = [
91
        'numeric',
92
        'text'
93
    ];
94
95
    /**
96
     * @var ArrayList
97
     */
98
    private $dateParts;
99
100
    /**
101
     * @var string
102
     */
103
    private $form = "";
104
105
    /**
106
     * @var string
107
     */
108
    private $variable = "";
109
110
    /**
111
     * @var string
112
     */
113
    private $datePartsAttribute = "";
114
115
    /**
116
     * @var StylesRenderer
117
     */
118
    private $stylesRenderer;
119
120
    /**
121
     * @param $data
122
     * @return string
123
     * @throws InvalidStylesheetException
124
     * @throws Exception
125
     */
126 67
    public function render($data)
127
    {
128 67
        $ret = "";
129 67
        $var = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $var is dead and can be removed.
Loading history...
130 67
        if (isset($data->{$this->variable})) {
131 63
            $var = $data->{$this->variable};
132
        } else {
133 10
            return "";
134
        }
135
136
        try {
137 63
            $this->prepareDatePartsInVariable($data, $var);
138 2
        } catch (CiteProcException $e) {
139 2
            if (isset($data->{$this->variable}->{'raw'}) &&
140 2
                !preg_match("/(\p{L}+)\s?([\-\–&,])\s?(\p{L}+)/u", $data->{$this->variable}->{'raw'})) {
141 1
                return $this->renderStyles($data->{$this->variable}->{'raw'});
142
            } else {
143 1
                if (isset($data->{$this->variable}->{'string-literal'})) {
144 1
                    return $this->renderStyles($data->{$this->variable}->{'string-literal'});
145
                }
146
            }
147
        }
148
149 62
        $form = $this->form;
150 62
        $dateParts = !empty($this->datePartsAttribute) ? explode("-", $this->datePartsAttribute) : [];
151 62
        $this->prepareDatePartsChildren($dateParts, $form);
152
153
        // No date-parts in date-part attribute defined, take into account that the defined date-part children will
154
        // be used.
155 62
        if (empty($this->datePartsAttribute) && $this->dateParts->count() > 0) {
156
            /** @var DatePart $part */
157 50
            foreach ($this->dateParts as $part) {
158 50
                $dateParts[] = $part->getName();
159
            }
160
        }
161
162
        /* cs:date may have one or more cs:date-part child elements (see Date-part). The attributes set on
163
        these elements override those specified for the localized date formats (e.g. to get abbreviated months for all
164
        locales, the form attribute on the month-cs:date-part element can be set to “short”). These cs:date-part
165
        elements do not affect which, or in what order, date parts are rendered. Affixes, which are very
166
        locale-specific, are not allowed on these cs:date-part elements. */
167
168 62
        if ($this->dateParts->count() > 0) {
169 61
            if (!isset($var->{'date-parts'})) { // ignore empty date-parts
170
                return "";
171
            }
172
173 61
            if (count($data->{$this->variable}->{'date-parts'}) === 1) {
174 59
                $data_ = $this->createDateTime($data->{$this->variable}->{'date-parts'});
175 59
                $ret .= $this->iterateAndRenderDateParts($dateParts, $data_);
176 2
            } elseif (count($var->{'date-parts'}) === 2) { //date range
177 2
                $data_ = $this->createDateTime($var->{'date-parts'});
178 2
                $from = $data_[0];
179 2
                $to = $data_[1];
180 2
                $interval = $to->diff($from);
181 2
                $delimiter = "";
182 2
                $toRender = 0;
183 2
                if ($interval->y > 0 && in_array('year', $dateParts)) {
184 1
                    $toRender |= self::DATE_RANGE_STATE_YEAR;
185 1
                    $delimiter = $this->dateParts->get($this->form."-year")->getRangeDelimiter();
186
                }
187 2
                if ($interval->m > 0 && $from->getMonth() - $to->getMonth() !== 0 && in_array('month', $dateParts)) {
188 1
                    $toRender |= self::DATE_RANGE_STATE_MONTH;
189 1
                    $delimiter = $this->dateParts->get($this->form."-month")->getRangeDelimiter();
190
                }
191 2
                if ($interval->d > 0 && $from->getDay() - $to->getDay() !== 0 && in_array('day', $dateParts)) {
192 1
                    $toRender |= self::DATE_RANGE_STATE_DAY;
193 1
                    $delimiter = $this->dateParts->get($this->form."-day")->getRangeDelimiter();
194
                }
195 2
                if ($toRender === self::DATE_RANGE_STATE_NONE) {
196 1
                    $ret .= $this->iterateAndRenderDateParts($dateParts, $data_);
197
                } else {
198 1
                    $ret .= $this->renderDateRange($toRender, $from, $to, $delimiter);
199
                }
200
            }
201
202 61
            if (isset($var->raw) && preg_match("/(\p{L}+)\s?([\-\–&,])\s?(\p{L}+)/u", $var->raw, $matches)) {
203 61
                return $matches[1].$matches[2].$matches[3];
204
            }
205 1
        } elseif (!empty($this->datePartsAttribute)) {
206
            // fallback:
207
            // When there are no dateParts children, but date-parts attribute in date
208
            // render numeric
209 1
            $data = $this->createDateTime($var->{'date-parts'});
210 1
            $ret = $this->renderNumeric($data[0]);
211
        }
212
213 62
        return !empty($ret) ? $this->renderStyles($ret) : "";
214
    }
215
216
    /**
217
     * @param array $dates
218
     * @return array
219
     * @throws Exception
220
     */
221 62
    private function createDateTime($dates)
222
    {
223 62
        $data = [];
224 62
        foreach ($dates as $date) {
225 62
            $date = $this->cleanDate($date);
226 62
            if ($date[0] < 1000) {
227 1
                $dateTime = new DateTime(0, 0, 0);
228 1
                $dateTime->setDay(0)->setMonth(0)->setYear(0);
229 1
                $data[] = $dateTime;
230
            }
231 62
            $dateTime = new DateTime(
232 62
                $date[0],
233 62
                array_key_exists(1, $date) ? $date[1] : 1,
234 62
                array_key_exists(2, $date) ? $date[2] : 1
235
            );
236 62
            if (!array_key_exists(1, $date)) {
237 31
                $dateTime->setMonth(0);
238
            }
239 62
            if (!array_key_exists(2, $date)) {
240 36
                $dateTime->setDay(0);
241
            }
242 62
            $data[] = $dateTime;
243
        }
244
245 62
        return $data;
246
    }
247
248
    /**
249
     * @param int $toRender
250
     * @param DateTime $from
251
     * @param DateTime $to
252
     * @param $delimiter
253
     * @return string
254
     */
255 1
    private function renderDateRange($toRender, DateTime $from, DateTime $to, $delimiter)
256
    {
257 1
        $datePartRenderer = DateRangeRenderer::factory($this, $toRender);
258 1
        return $datePartRenderer->parseDateRange($this->dateParts, $from, $to, $delimiter);
259
    }
260
261
    /**
262
     * @param string $format
263
     * @return bool
264
     */
265 17
    private function hasDatePartsFromLocales($format)
266
    {
267 17
        $dateXml = CiteProc::getContext()->getLocale()->getDateXml();
268 17
        return !empty($dateXml[$format]);
269
    }
270
271
    /**
272
     * @param string $format
273
     * @return array
274
     */
275 17
    private function getDatePartsFromLocales($format)
276
    {
277 17
        $ret = [];
278
        // date parts from locales
279 17
        $dateFromLocale_ = CiteProc::getContext()->getLocale()->getDateXml();
280 17
        $dateFromLocale = $dateFromLocale_[$format];
281
282
        // no custom date parts within the date element (this)?
283 17
        if (!empty($dateFromLocale)) {
284 17
            $dateForm = array_filter(
285 17
                is_array($dateFromLocale) ? $dateFromLocale : [$dateFromLocale],
286
                function ($element) use ($format) {
287
                    /** @var SimpleXMLElement $element */
288 17
                    $dateForm = (string) $element->attributes()["form"];
289 17
                    return $dateForm === $format;
290 17
                }
291
            );
292
293
            //has dateForm from locale children (date-part elements)?
294 17
            $localeDate = array_pop($dateForm);
295
296 17
            if ($localeDate instanceof SimpleXMLElement && $localeDate->count() > 0) {
297 17
                foreach ($localeDate as $child) {
298 17
                    $ret[] = $child;
299
                }
300
            }
301
        }
302 17
        return $ret;
303
    }
304
305
    /**
306
     * @return string
307
     */
308 28
    public function getVariable()
309
    {
310 28
        return $this->variable;
311
    }
312
313
    /**
314
     * @param $data
315
     * @param $var
316
     * @throws CiteProcException
317
     */
318 63
    private function prepareDatePartsInVariable($data, $var)
319
    {
320 63
        if (!isset($data->{$this->variable}->{'date-parts'}) || empty($data->{$this->variable}->{'date-parts'})) {
321 9
            if (isset($data->{$this->variable}->raw) && !empty($data->{$this->variable}->raw)) {
322
                // try to parse date parts from "raw" attribute
323 8
                $var->{'date-parts'} = Util\DateHelper::parseDateParts($data->{$this->variable});
324
            } else {
325 1
                throw new CiteProcException("No valid date format");
326
            }
327
        }
328 62
    }
329
330
    /**
331
     * @param $dateParts
332
     * @param string $form
333
     * @throws InvalidStylesheetException
334
     */
335 62
    private function prepareDatePartsChildren($dateParts, $form)
336
    {
337
        /* Localized date formats are selected with the optional form attribute, which must set to either “numeric”
338
        (for fully numeric formats, e.g. “12-15-2005”), or “text” (for formats with a non-numeric month, e.g.
339
        “December 15, 2005”). Localized date formats can be customized in two ways. First, the date-parts attribute may
340
        be used to show fewer date parts. The possible values are:
341
            - “year-month-day” - (default), renders the year, month and day
342
            - “year-month” - renders the year and month
343
            - “year” - renders the year */
344
345 62
        if ($this->dateParts->count() < 1 && in_array($form, self::$localizedDateFormats)) {
346 17
            if ($this->hasDatePartsFromLocales($form)) {
347 17
                $datePartsFromLocales = $this->getDatePartsFromLocales($form);
348
                array_filter($datePartsFromLocales, function (SimpleXMLElement $item) use ($dateParts) {
349 17
                    return in_array($item["name"], $dateParts);
350 17
                });
351
352 17
                foreach ($datePartsFromLocales as $datePartNode) {
353 17
                    $datePart = $datePartNode["name"];
354 17
                    $this->dateParts->set("$form-$datePart", Util\Factory::create($datePartNode));
355
                }
356
            } else { //otherwise create default date parts
357
                foreach ($dateParts as $datePart) {
358
                    $this->dateParts->add(
359
                        "$form-$datePart",
360
                        new DatePart(
361
                            new SimpleXMLElement('<date-part name="'.$datePart.'" form="'.$form.'" />')
362
                        )
363
                    );
364
                }
365
            }
366
        }
367 62
    }
368
369
370 1
    private function renderNumeric(DateTime $date)
371
    {
372 1
        return $date->renderNumeric();
373
    }
374
375 13
    public function getForm()
376
    {
377 13
        return $this->form;
378
    }
379
380 62
    private function cleanDate($date)
381
    {
382 62
        $ret = [];
383 62
        foreach ($date as $key => $datePart) {
384 62
            $ret[$key] = Util\NumberHelper::extractNumber(Util\StringHelper::removeBrackets($datePart));
385
        }
386 62
        return $ret;
387
    }
388
389
    /**
390
     * @param array $dateParts
391
     * @param array $data_
392
     * @return string
393
     */
394 60
    private function iterateAndRenderDateParts(array $dateParts, array $data_)
395
    {
396 60
        $result = [];
397
        /** @var DatePart $datePart */
398 60
        foreach ($this->dateParts as $key => $datePart) {
399
            /** @noinspection PhpUnusedLocalVariableInspection */
400 60
            list($f, $p) = explode("-", $key);
401 60
            if (in_array($p, $dateParts)) {
402 60
                $result[] = $datePart->render($data_[0], $this);
403
            }
404
        }
405 60
        $result = array_filter($result);
406 60
        $glue = $this->datePartsHaveAffixes() ? "" : " ";
407 60
        $return = implode($glue, $result);
408 60
        return trim($return);
409
    }
410
411
    /**
412
     * @return bool
413
     */
414 60
    private function datePartsHaveAffixes()
415
    {
416
        $result = $this->dateParts->filter(function (DatePart $datePart) {
417 60
            return $datePart->renderSuffix() !== "" || $datePart->renderPrefix() !== "";
418 60
        });
419 60
        return $result->count() > 0;
420
    }
421
422
    /**
423
     * @param string $var
424
     * @return string
425
     */
426 62
    private function renderStyles(string $var): string
427
    {
428 62
        return $this->stylesRenderer->renderAffixes(
429 62
            $this->stylesRenderer->renderFormatting(
430 62
                $this->stylesRenderer->renderTextCase($var)
431
            )
432
        );
433
    }
434
}
435