Passed
Push — new-api ( 44902a...f133eb )
by Sebastian
04:49
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
/*
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