Passed
Push — new-api ( e6ef7d...34a0a9 )
by Sebastian
04:34
created

Number::toDecimalNumber()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3

Importance

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