Passed
Push — new-api ( 5677f6...2a03a6 )
by Sebastian
04:12
created

Date::setParent()   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
/*
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