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

Number::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) 2020 Sebastian Böttger.
7
 * @license     https://opensource.org/licenses/MIT
8
 */
9
10
namespace Seboettg\CiteProc\Rendering\Number;
11
12
use Seboettg\CiteProc\CiteProc;
13
use Seboettg\CiteProc\Locale\Locale;
14
use Seboettg\CiteProc\Rendering\HasParent;
15
use Seboettg\CiteProc\Rendering\Rendering;
16
use Seboettg\CiteProc\Styles\StylesRenderer;
17
use Seboettg\CiteProc\Styles\TextCase;
18
use Seboettg\CiteProc\Util;
19
use SimpleXMLElement;
20
use stdClass;
21
22
/**
23
 * Class Number
24
 * @package Seboettg\CiteProc\Rendering
25
 *
26
 * @author Sebastian Böttger <[email protected]>
27
 */
28
class Number implements HasParent, Rendering
29
{
30
31
    private const RANGE_DELIMITER_HYPHEN = "-";
32
33
    private const RANGE_DELIMITER_AMPERSAND = "&";
34
35
    private const RANGE_DELIMITER_COMMA = ",";
36
37
    private const PATTERN_ORDINAL = "/\s*(\d+)\s*([\-\–&,])\s*(\d+)\s*/";
38
39
    private const PATTERN_LONG_ORDINAL = "/\s*(\d+)\s*([\-\–&,])\s*(\d+)\s*/";
40
41
    private const PATTERN_ROMAN = "/\s*(\d+)\s*([\-\–&,])\s*(\d+)\s*/";
42
43
    private const PATTERN_NUMERIC = "/\s*(\d+)\s*([\-\–&,])\s*(\d+)\s*/";
44
45 58
    public static function factory(SimpleXMLElement $node)
46
    {
47 58
        $form = $variable = null;
48 58
        $context = CiteProc::getContext();
49
50 58
        foreach ($node->attributes() as $attribute) {
51 58
            switch ($attribute->getName()) {
52 58
                case 'variable':
53 58
                    $variable = (string) $attribute;
54 58
                    break;
55 56
                case 'form':
56 49
                    $form = new Form((string) $attribute);
57 58
                    break;
58
            }
59
        }
60 58
        $stylesRenderer = StylesRenderer::factory($node);
61 58
        $locale = $context->getLocale();
62 58
        return new self($variable, $form, $locale, $stylesRenderer);
63
    }
64
65
    /** @var string */
66
    private $variable;
67
68
    /** @var Form  */
69
    private $form;
70
71
    /** @var Locale */
72
    private $locale;
73
74
    /** @var StylesRenderer */
75
    private $stylesRenderer;
76
77
    private $parent;
78
79 58
    public function __construct(
80
        ?string $variable,
81
        ?Form $form,
82
        ?Locale $locale,
83
        StylesRenderer $stylesRenderer
84
    ) {
85 58
        $this->locale = $locale;
86 58
        $this->variable = $variable;
87 58
        $this->form = $form;
88 58
        $this->stylesRenderer = $stylesRenderer;
89 58
    }
90
91
    /**
92
     * @param stdClass $data
93
     * @param int|null $citationNumber
94
     * @return string
95
     */
96 28
    public function render($data, $citationNumber = null)
0 ignored issues
show
Coding Style introduced by
Incorrect spacing between argument "$citationNumber" and equals sign; expected 0 but found 1
Loading history...
Coding Style introduced by
Incorrect spacing between default value and equals sign for argument "$citationNumber"; expected 0 but found 1
Loading history...
97
    {
98 28
        $lang = (isset($data->language) && $data->language != 'en') ? $data->language : 'en';
99
100 28
        if (empty($this->variable) || empty($data->{$this->variable})) {
101 10
            return "";
102
        }
103 21
        $number = $data->{$this->variable};
104 21
        $decimalNumber = $this->toDecimalNumber($number);
105 21
        switch ((string)$this->form) {
106 21
            case Form::ORDINAL:
107 4
                if (preg_match(self::PATTERN_ORDINAL, $decimalNumber, $matches)) {
108 2
                    $num1 = $this->ordinal($matches[1]);
109 2
                    $num2 = $this->ordinal($matches[3]);
110 2
                    $text = $this->buildNumberRangeString($num1, $num2, $matches[2]);
111
                } else {
112 2
                    $text = $this->ordinal($decimalNumber);
113
                }
114 4
                break;
115 18
            case Form::LONG_ORDINAL:
116 3
                if (preg_match(self::PATTERN_LONG_ORDINAL, $decimalNumber, $matches)) {
117 2
                    if (in_array($this->stylesRenderer->getTextCase()->getTextCase()->getValue(), [
118 2
                        TextCase::CAPITALIZE_FIRST,
119
                        TextCase::SENTENCE
120
                    ])) {
121 1
                        $num1 = $this->longOrdinal($matches[1]);
122 1
                        $num2 = $this->longOrdinal($matches[3]);
123
                    } else {
124 2
                        $num1 = $this->stylesRenderer->renderTextCase($this->longOrdinal($matches[1]));
125 2
                        $num2 = $this->stylesRenderer->renderTextCase($this->longOrdinal($matches[3]));
126
                    }
127 2
                    $text = $this->buildNumberRangeString($num1, $num2, $matches[2]);
128
                } else {
129 1
                    $text = $this->longOrdinal($decimalNumber);
130
                }
131 3
                break;
132 16
            case Form::ROMAN:
133 5
                if (preg_match(self::PATTERN_ROMAN, $decimalNumber, $matches)) {
134 1
                    $num1 = Util\NumberHelper::dec2roman($matches[1]);
135 1
                    $num2 = Util\NumberHelper::dec2roman($matches[3]);
136 1
                    $text = $this->buildNumberRangeString($num1, $num2, $matches[2]);
137
                } else {
138 4
                    $text = Util\NumberHelper::dec2roman($decimalNumber);
139
                }
140 5
                break;
141 12
            case Form::NUMERIC:
142
            default:
143
                /*
144
                 During the extraction, numbers separated by a hyphen are stripped of intervening spaces (“2 - 4”
145
                 becomes “2-4”). Numbers separated by a comma receive one space after the comma (“2,3” and “2 , 3”
146
                 become “2, 3”), while numbers separated by an ampersand receive one space before and one after the
147
                 ampersand (“2&3” becomes “2 & 3”).
148
                 */
149 12
                $decimalNumber = $data->{$this->variable};
150 12
                if (preg_match(self::PATTERN_NUMERIC, $decimalNumber, $matches)) {
151 9
                    $text = $this->buildNumberRangeString($matches[1], $matches[3], $matches[2]);
152
                } else {
153 3
                    $text = $decimalNumber;
154
                }
155 12
                break;
156
        }
157 21
        $this->stylesRenderer->getTextCase()->setLanguage($lang);
158 21
        $text = $this->stylesRenderer->renderTextCase($text);
159 21
        $text = $this->stylesRenderer->renderFormatting($text);
160 21
        $text = $this->stylesRenderer->renderAffixes($text);
161 21
        return $this->stylesRenderer->renderDisplay($text);
162
    }
163
164
    /**
165
     * @param $num
166
     * @return string
167
     */
168 4
    public function ordinal($num)
169
    {
170 4
        if (($num / 10) % 10 == 1) {
171 1
            $ordinalSuffix = $this->locale->filter('terms', 'ordinal')->single;
172 4
        } elseif ($num % 10 == 1) {
173
            $ordinalSuffix = $this->locale->filter('terms', 'ordinal-01')->single;
174 4
        } elseif ($num % 10 == 2) {
175 2
            $ordinalSuffix = $this->locale->filter('terms', 'ordinal-02')->single;
176 4
        } elseif ($num % 10 == 3) {
177 1
            $ordinalSuffix = $this->locale->filter('terms', 'ordinal-03')->single;
178
        } else {
179 3
            $ordinalSuffix = $this->locale->filter('terms', 'ordinal-04')->single;
180
        }
181 4
        if (empty($ordinalSuffix)) {
182 3
            $ordinalSuffix = $this->locale->filter('terms', 'ordinal')->single;
183
        }
184 4
        return $num . $ordinalSuffix;
185
    }
186
187
    /**
188
     * @param $num
189
     * @return string
190
     */
191 3
    public function longOrdinal($num)
192
    {
193 3
        $num = sprintf("%02d", $num);
194 3
        $ret = $this->locale->filter('terms', 'long-ordinal-' . $num)->single;
195 3
        if (!$ret) {
196
            return $this->ordinal($num);
197
        }
198 3
        return $ret;
199
    }
200
201
    /**
202
     * @param string|int $num1
203
     * @param string|int $num2
204
     * @param string $delimiter
205
     * @return string
206
     */
207 11
    public function buildNumberRangeString($num1, $num2, string $delimiter)
208
    {
209
210 11
        if (self::RANGE_DELIMITER_AMPERSAND === $delimiter) {
211 1
            $numRange = "$num1 ".htmlentities(self::RANGE_DELIMITER_AMPERSAND)." $num2";
212
        } else {
213 11
            if (self::RANGE_DELIMITER_COMMA === $delimiter) {
214 1
                $numRange = $num1.htmlentities(self::RANGE_DELIMITER_COMMA)." $num2";
215
            } else {
216 11
                $numRange = $num1.self::RANGE_DELIMITER_HYPHEN.$num2;
217
            }
218
        }
219 11
        return $numRange;
220
    }
221
222
    /**
223
     * @param string $number
224
     * @return string
225
     */
226 21
    private function toDecimalNumber(string $number)
227
    {
228 21
        $decimalNumber = $number;
229 21
        if (Util\NumberHelper::isRomanNumber($number)) {
230 1
            $decimalNumber = Util\NumberHelper::roman2Dec($number);
231
        } else {
232 20
            $number = mb_strtolower($number);
233 20
            if (preg_match(Util\NumberHelper::PATTERN_ROMAN_RANGE, $number, $matches)) {
234 1
                $num1 = Util\NumberHelper::roman2Dec(mb_strtoupper($matches[1]));
235 1
                $num2 = Util\NumberHelper::roman2Dec(mb_strtoupper($matches[3]));
236 1
                $decimalNumber = sprintf('%d%s%d', $num1, $matches[2], $num2);
237
            }
238
        }
239 21
        return $decimalNumber;
240
    }
241
242
    public function getParent()
243
    {
244
        return $this->parent;
245
    }
246
247 1
    public function setParent($parent)
248
    {
249 1
        $this->parent = $parent;
250 1
    }
251
}
252