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

Date::render()   F

Complexity

Conditions 27
Paths 475

Size

Total Lines 90
Code Lines 54

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 50
CRAP Score 27.0054

Importance

Changes 0
Metric Value
cc 27
eloc 54
c 0
b 0
f 0
nc 475
nop 1
dl 0
loc 90
ccs 50
cts 51
cp 0.9804
crap 27.0054
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\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