Passed
Push — master ( f36a08...6258cd )
by Sebastian
12:50
created

Date::cleanDate()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

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