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