Total Complexity | 68 |
Total Lines | 356 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like Date often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Date, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
32 | class Date |
||
33 | { |
||
34 | use AffixesTrait, |
||
|
|||
35 | DisplayTrait, |
||
36 | FormattingTrait, |
||
37 | TextCaseTrait; |
||
38 | |||
39 | // bitmask: ymd |
||
40 | public const DATE_RANGE_STATE_NONE = 0; // 000 |
||
41 | public const DATE_RANGE_STATE_DAY = 1; // 001 |
||
42 | public const DATE_RANGE_STATE_MONTH = 2; // 010 |
||
43 | public const DATE_RANGE_STATE_MONTHDAY = 3; // 011 |
||
44 | public const DATE_RANGE_STATE_YEAR = 4; // 100 |
||
45 | public const DATE_RANGE_STATE_YEARDAY = 5; // 101 |
||
46 | public const DATE_RANGE_STATE_YEARMONTH = 6; // 110 |
||
47 | public const DATE_RANGE_STATE_YEARMONTHDAY = 7; // 111 |
||
48 | |||
49 | private static $localizedDateFormats = [ |
||
50 | 'numeric', |
||
51 | 'text' |
||
52 | ]; |
||
53 | |||
54 | private ListInterface $dateParts; |
||
55 | private string $form = "numeric"; |
||
56 | private string $variable = ""; |
||
57 | private string $datePartsAttribute = ""; |
||
58 | |||
59 | /** |
||
60 | * Date constructor. |
||
61 | * @param SimpleXMLElement $node |
||
62 | * @throws InvalidStylesheetException |
||
63 | */ |
||
64 | public function __construct(SimpleXMLElement $node) |
||
65 | { |
||
66 | $this->dateParts = emptyList(); |
||
67 | |||
68 | /** @var SimpleXMLElement $attribute */ |
||
69 | foreach ($node->attributes() as $attribute) { |
||
70 | switch ($attribute->getName()) { |
||
71 | case 'form': |
||
72 | $this->form = (string) $attribute; |
||
73 | break; |
||
74 | case 'variable': |
||
75 | $this->variable = (string) $attribute; |
||
76 | break; |
||
77 | case 'date-parts': |
||
78 | $this->datePartsAttribute = (string) $attribute; |
||
79 | } |
||
80 | } |
||
81 | /** @var SimpleXMLElement $child */ |
||
82 | foreach ($node->children() as $child) { |
||
83 | if ($child->getName() === "date-part") { |
||
84 | $datePartName = (string) $child->attributes()["name"]; |
||
85 | $this->dateParts->add(pair($this->form . "-" . $datePartName, Util\Factory::create($child))); |
||
86 | } |
||
87 | } |
||
88 | |||
89 | $this->initAffixesAttributes($node); |
||
90 | $this->initDisplayAttributes($node); |
||
91 | $this->initFormattingAttributes($node); |
||
92 | $this->initTextCaseAttributes($node); |
||
93 | } |
||
94 | |||
95 | /** |
||
96 | * @param $data |
||
97 | * @return string |
||
98 | * @throws InvalidStylesheetException |
||
99 | * @throws Exception |
||
100 | */ |
||
101 | public function render($data) |
||
102 | { |
||
103 | $ret = ""; |
||
104 | $var = null; |
||
105 | if (isset($data->{$this->variable})) { |
||
106 | $var = $data->{$this->variable}; |
||
107 | } else { |
||
108 | return ""; |
||
109 | } |
||
110 | |||
111 | try { |
||
112 | $this->prepareDatePartsInVariable($data, $var); |
||
113 | } catch (CiteProcException $e) { |
||
114 | if (isset($data->{$this->variable}->{'raw'}) && |
||
115 | !preg_match("/(\p{L}+)\s?([\-\–&,])\s?(\p{L}+)/u", $data->{$this->variable}->{'raw'})) { |
||
116 | return $this->addAffixes($this->format($this->applyTextCase($data->{$this->variable}->{'raw'}))); |
||
117 | } else { |
||
118 | if (isset($data->{$this->variable}->{'string-literal'})) { |
||
119 | return $this->addAffixes( |
||
120 | $this->format($this->applyTextCase($data->{$this->variable}->{'string-literal'})) |
||
121 | ); |
||
122 | } |
||
123 | } |
||
124 | } |
||
125 | |||
126 | $form = $this->form; |
||
127 | $dateParts = !empty($this->datePartsAttribute) ? |
||
128 | listOf(...explode("-", $this->datePartsAttribute)) : emptyList(); |
||
129 | $this->prepareDatePartsChildren($dateParts, $form); |
||
130 | |||
131 | // No date-parts in date-part attribute defined, take into account that the defined date-part children will |
||
132 | // be used. |
||
133 | if (empty($this->datePartsAttribute) && $this->dateParts->count() > 0) { |
||
134 | /** @var DatePart $part */ |
||
135 | $dateParts = $this->dateParts->map(fn (Pair $pair) => $pair->getValue()->getName()); |
||
136 | } |
||
137 | |||
138 | /* cs:date may have one or more cs:date-part child elements (see Date-part). The attributes set on |
||
139 | these elements override those specified for the localized date formats (e.g. to get abbreviated months for all |
||
140 | locales, the form attribute on the month-cs:date-part element can be set to “short”). These cs:date-part |
||
141 | elements do not affect which, or in what order, date parts are rendered. Affixes, which are very |
||
142 | locale-specific, are not allowed on these cs:date-part elements. */ |
||
143 | |||
144 | if ($this->dateParts->count() > 0) { |
||
145 | if (!isset($var->{'date-parts'})) { // ignore empty date-parts |
||
146 | return ""; |
||
147 | } |
||
148 | |||
149 | if (count($data->{$this->variable}->{'date-parts'}) === 1) { |
||
150 | list($from) = $this->createDateTime($data->{$this->variable}->{'date-parts'}); |
||
151 | $ret .= $this->iterateAndRenderDateParts($dateParts, $from); |
||
152 | } elseif (count($var->{'date-parts'}) === 2) { //date range |
||
153 | list($from, $to) = $this->createDateTime($var->{'date-parts'}); |
||
154 | |||
155 | |||
156 | $interval = $to->diff($from); |
||
157 | $delimiter = ""; |
||
158 | $toRender = 0; |
||
159 | if ($interval->y > 0 && $dateParts->contains("year")) { |
||
160 | $toRender |= self::DATE_RANGE_STATE_YEAR; |
||
161 | $delimiter = $this->dateParts |
||
162 | ->filter(fn (Pair $pair) => $pair->getKey() === $this->form . "-year") |
||
163 | ->first() |
||
164 | ->getValue() |
||
165 | ->getRangeDelimiter(); |
||
166 | } |
||
167 | if ($interval->m > 0 && $from->getMonth() - $to->getMonth() !== 0 && $dateParts->contains("month")) { |
||
168 | $toRender |= self::DATE_RANGE_STATE_MONTH; |
||
169 | $delimiter = $this->dateParts |
||
170 | ->filter(fn (Pair $pair) => $pair->getKey() === $this->form . "-month") |
||
171 | ->first() |
||
172 | ->getValue() |
||
173 | ->getRangeDelimiter(); |
||
174 | } |
||
175 | if ($interval->d > 0 && $from->getDay() - $to->getDay() !== 0 && $dateParts->contains("day")) { |
||
176 | $toRender |= self::DATE_RANGE_STATE_DAY; |
||
177 | $delimiter = $this->dateParts |
||
178 | ->filter(fn (Pair $pair) => $pair->getKey() === "$this->form-day") |
||
179 | ->first() |
||
180 | ->getValue() |
||
181 | ->getRangeDelimiter(); |
||
182 | } |
||
183 | if ($toRender === self::DATE_RANGE_STATE_NONE) { |
||
184 | $ret .= $this->iterateAndRenderDateParts($dateParts, $from); |
||
185 | } else { |
||
186 | $ret .= $this->renderDateRange($toRender, $from, $to, $delimiter); |
||
187 | } |
||
188 | } |
||
189 | |||
190 | if (isset($var->raw) && preg_match("/(\p{L}+)\s?([\-–&,])\s?(\p{L}+)/u", $var->raw, $matches)) { |
||
191 | return $matches[1].$matches[2].$matches[3]; |
||
192 | } |
||
193 | } elseif (!empty($this->datePartsAttribute)) { |
||
194 | // fallback: |
||
195 | // When there are no dateParts children, but date-parts attribute in date |
||
196 | // render numeric |
||
197 | $data = $this->createDateTime($var->{'date-parts'}); |
||
198 | $ret = $this->renderNumeric($data[0]); |
||
199 | } |
||
200 | |||
201 | return !empty($ret) ? $this->addAffixes($this->format($this->applyTextCase($ret))) : ""; |
||
202 | } |
||
203 | |||
204 | /** |
||
205 | * @throws Exception |
||
206 | */ |
||
207 | private function createDateTime(array $dates): array |
||
208 | { |
||
209 | $data = []; |
||
210 | foreach ($dates as $date) { |
||
211 | $date = $this->cleanDate($date); |
||
212 | if ($date[0] < 1000) { |
||
213 | $dateTime = new DateTime(0, 0, 0); |
||
214 | $dateTime->setDay(0)->setMonth(0)->setYear(0); |
||
215 | $data[] = $dateTime; |
||
216 | } |
||
217 | $dateTime = new DateTime( |
||
218 | $date[0], |
||
219 | array_key_exists(1, $date) ? $date[1] : 1, |
||
220 | array_key_exists(2, $date) ? $date[2] : 1 |
||
221 | ); |
||
222 | if (!array_key_exists(1, $date)) { |
||
223 | $dateTime->setMonth(0); |
||
224 | } |
||
225 | if (!array_key_exists(2, $date)) { |
||
226 | $dateTime->setDay(0); |
||
227 | } |
||
228 | $data[] = $dateTime; |
||
229 | } |
||
230 | |||
231 | return $data; |
||
232 | } |
||
233 | |||
234 | private function renderDateRange(int $toRender, DateTime $from, DateTime $to, $delimiter): string |
||
235 | { |
||
236 | $datePartRenderer = DateRangeRenderer::factory($this, $toRender); |
||
237 | return $datePartRenderer->parseDateRange($this->dateParts, $from, $to, $delimiter); |
||
238 | } |
||
239 | |||
240 | private function hasDatePartsFromLocales(string $format): bool |
||
241 | { |
||
242 | $dateXml = CiteProc::getContext()->getLocale()->getDateXml(); |
||
243 | return !empty($dateXml[$format]); |
||
244 | } |
||
245 | |||
246 | private function getDatePartsFromLocales($format): array |
||
247 | { |
||
248 | $ret = []; |
||
249 | // date parts from locales |
||
250 | $dateFromLocale_ = CiteProc::getContext()->getLocale()->getDateXml(); |
||
251 | $dateFromLocale = $dateFromLocale_[$format]; |
||
252 | |||
253 | // no custom date parts within the date element (this)? |
||
254 | if (!empty($dateFromLocale)) { |
||
255 | $dateForm = array_filter( |
||
256 | is_array($dateFromLocale) ? $dateFromLocale : [$dateFromLocale], |
||
257 | function ($element) use ($format) { |
||
258 | /** @var SimpleXMLElement $element */ |
||
259 | $dateForm = (string) $element->attributes()["form"]; |
||
260 | return $dateForm === $format; |
||
261 | } |
||
262 | ); |
||
263 | |||
264 | //has dateForm from locale children (date-part elements)? |
||
265 | $localeDate = array_pop($dateForm); |
||
266 | |||
267 | if ($localeDate instanceof SimpleXMLElement && $localeDate->count() > 0) { |
||
268 | foreach ($localeDate as $child) { |
||
269 | $ret[] = $child; |
||
270 | } |
||
271 | } |
||
272 | } |
||
273 | return $ret; |
||
274 | } |
||
275 | |||
276 | /** |
||
277 | * @return string |
||
278 | */ |
||
279 | public function getVariable() |
||
280 | { |
||
281 | return $this->variable; |
||
282 | } |
||
283 | |||
284 | /** |
||
285 | * @param $data |
||
286 | * @param $var |
||
287 | * @throws CiteProcException |
||
288 | */ |
||
289 | private function prepareDatePartsInVariable($data, $var) |
||
290 | { |
||
291 | if (!isset($data->{$this->variable}->{'date-parts'}) || empty($data->{$this->variable}->{'date-parts'})) { |
||
292 | if (isset($data->{$this->variable}->raw) && !empty($data->{$this->variable}->raw)) { |
||
293 | // try to parse date parts from "raw" attribute |
||
294 | $var->{'date-parts'} = Util\DateHelper::parseDateParts($data->{$this->variable}); |
||
295 | } else { |
||
296 | throw new CiteProcException("No valid date format"); |
||
297 | } |
||
298 | } |
||
299 | } |
||
300 | |||
301 | /** |
||
302 | * @param ListInterface $dateParts |
||
303 | * @param string $form |
||
304 | * @throws InvalidStylesheetException |
||
305 | */ |
||
306 | private function prepareDatePartsChildren($dateParts, $form) |
||
333 | ) |
||
334 | ); |
||
335 | } |
||
336 | } |
||
337 | } |
||
338 | } |
||
339 | |||
340 | |||
341 | private function renderNumeric(DateTime $date) |
||
342 | { |
||
343 | return $date->renderNumeric(); |
||
344 | } |
||
345 | |||
346 | public function getForm() |
||
347 | { |
||
348 | return $this->form; |
||
349 | } |
||
350 | |||
351 | private function cleanDate($date) |
||
358 | } |
||
359 | |||
360 | /** |
||
361 | * @param ListInterface $dateParts |
||
362 | * @param DateTime $from |
||
363 | * @return string |
||
364 | */ |
||
365 | private function iterateAndRenderDateParts(ListInterface $dateParts, DateTime $from): string |
||
366 | { |
||
367 | $glue = $this->datePartsHaveAffixes() ? "" : " "; |
||
368 | $result = $this->dateParts |
||
369 | ->filter(function (Pair $datePartPair) use ($dateParts) { |
||
370 | list($_, $p) = explode("-", $datePartPair->getKey()); |
||
371 | return $dateParts->contains($p); |
||
372 | }) |
||
373 | ->map(fn (Pair $datePartPair) => $datePartPair->getValue()->render($from, $this)) |
||
374 | ->filter() |
||
375 | ->joinToString($glue); |
||
376 | return trim($result); |
||
377 | } |
||
378 | |||
379 | /** |
||
380 | * @return bool |
||
381 | */ |
||
382 | private function datePartsHaveAffixes(): bool |
||
388 | } |
||
389 | } |
||
390 |