Passed
Push — new-api ( f151f9...5a646f )
by Sebastian
04:44
created

Date::render()   F

Complexity

Conditions 27
Paths 475

Size

Total Lines 90
Code Lines 54

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 50
CRAP Score 27.0054

Importance

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