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

Number::factory()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 4

Importance

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