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

Date::renderNumeric()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
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