Number   A
last analyzed

Complexity

Total Complexity 33

Size/Duplication

Total Lines 197
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 103
dl 0
loc 197
rs 9.76
c 3
b 0
f 0
wmc 33

6 Methods

Rating   Name   Duplication   Size   Complexity  
A buildNumberRangeString() 0 13 3
A __construct() 0 17 4
A longOrdinal() 0 8 2
C render() 0 59 15
A toDecimalNumber() 0 14 3
A ordinal() 0 17 6
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;
11
12
use Seboettg\CiteProc\CiteProc;
13
use Seboettg\CiteProc\Styles\AffixesTrait;
14
use Seboettg\CiteProc\Styles\DisplayTrait;
15
use Seboettg\CiteProc\Styles\FormattingTrait;
16
use Seboettg\CiteProc\Styles\TextCaseTrait;
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_DEFAULT = "/\s*(\d+)\s*([\-\–&,])\s*(\d+)\s*/";
43
44
    use FormattingTrait,
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\Number.
Loading history...
45
        AffixesTrait,
46
        TextCaseTrait,
47
        DisplayTrait;
48
49
    /**
50
     * @var string
51
     */
52
    private $variable;
53
54
    /**
55
     * @var string
56
     */
57
    private $form;
58
59
    /**
60
     * Number constructor.
61
     * @param SimpleXMLElement $node
62
     */
63
    public function __construct(SimpleXMLElement $node)
64
    {
65
        //<number variable="edition" form="ordinal"/>
66
        /** @var SimpleXMLElement $attribute */
67
        foreach ($node->attributes() as $attribute) {
68
            switch ($attribute->getName()) {
69
                case 'variable':
70
                    $this->variable = (string) $attribute;
71
                    break;
72
                case 'form':
73
                    $this->form = (string) $attribute;
74
            }
75
        }
76
77
        $this->initFormattingAttributes($node);
78
        $this->initAffixesAttributes($node);
79
        $this->initTextCaseAttributes($node);
80
    }
81
82
    /**
83
     * @param stdClass $data
84
     * @param int|null $citationNumber
85
     * @return string
86
     */
87
    public function render($data, $citationNumber = null): string
88
    {
89
        $lang = (isset($data->language) && $data->language != 'en') ? $data->language : 'en';
90
91
        if (empty($this->variable) || empty($data->{$this->variable})) {
92
            return "";
93
        }
94
        $number = $data->{$this->variable};
95
        $decimalNumber = $this->toDecimalNumber($number);
96
        switch ($this->form) {
97
            case 'ordinal':
98
                if (preg_match(self::PATTERN_ORDINAL, $decimalNumber, $matches)) {
99
                    $num1 = self::ordinal($matches[1]);
100
                    $num2 = self::ordinal($matches[3]);
101
                    $text = $this->buildNumberRangeString($num1, $num2, $matches[2]);
102
                } else {
103
                    $text = self::ordinal($decimalNumber);
104
                }
105
                break;
106
            case 'long-ordinal':
107
                if (preg_match(self::PATTERN_LONG_ORDINAL, $decimalNumber, $matches)) {
108
                    if ($this->textCase === "capitalize-first" || $this->textCase === "sentence") {
109
                        $num1 = self::longOrdinal($matches[1]);
110
                        $num2 = self::longOrdinal($matches[3]);
111
                    } else {
112
                        $num1 = $this->applyTextCase(self::longOrdinal($matches[1]));
113
                        $num2 = $this->applyTextCase(self::longOrdinal($matches[3]));
114
                    }
115
                    $text = $this->buildNumberRangeString($num1, $num2, $matches[2]);
116
                } else {
117
                    $text = self::longOrdinal($decimalNumber);
118
                }
119
                break;
120
            case 'roman':
121
                if (preg_match(self::PATTERN_ROMAN, $decimalNumber, $matches)) {
122
                    $num1 = Util\NumberHelper::dec2roman($matches[1]);
123
                    $num2 = Util\NumberHelper::dec2roman($matches[3]);
124
                    $text = $this->buildNumberRangeString($num1, $num2, $matches[2]);
125
                } else {
126
                    $text = Util\NumberHelper::dec2roman($decimalNumber);
127
                }
128
                break;
129
            case 'numeric':
130
            default:
131
                /*
132
                 During the extraction, numbers separated by a hyphen are stripped of intervening spaces (“2 - 4”
133
                 becomes “2-4”). Numbers separated by a comma receive one space after the comma (“2,3” and “2 , 3”
134
                 become “2, 3”), while numbers separated by an ampersand receive one space before and one after the
135
                 ampersand (“2&3” becomes “2 & 3”).
136
                 */
137
                $decimalNumber = $data->{$this->variable};
138
                if (preg_match(self::PATTERN_NUMERIC_DEFAULT, $decimalNumber, $matches)) {
139
                    $text = $this->buildNumberRangeString($matches[1], $matches[3], $matches[2]);
140
                } else {
141
                    $text = $decimalNumber;
142
                }
143
                break;
144
        }
145
        return $this->wrapDisplayBlock($this->addAffixes($this->format($this->applyTextCase($text, $lang))));
146
    }
147
148
    /**
149
     * @param $num
150
     * @return string
151
     */
152
    public static function ordinal($num): string
153
    {
154
        if ((int) ($num / 10) % 10 == 1) {
155
            $ordinalSuffix = CiteProc::getContext()->getLocale()->filter('terms', 'ordinal')->single;
156
        } elseif ($num % 10 == 1) {
157
            $ordinalSuffix = CiteProc::getContext()->getLocale()->filter('terms', 'ordinal-01')->single;
158
        } elseif ($num % 10 == 2) {
159
            $ordinalSuffix = CiteProc::getContext()->getLocale()->filter('terms', 'ordinal-02')->single;
160
        } elseif ($num % 10 == 3) {
161
            $ordinalSuffix = CiteProc::getContext()->getLocale()->filter('terms', 'ordinal-03')->single;
162
        } else {
163
            $ordinalSuffix = CiteProc::getContext()->getLocale()->filter('terms', 'ordinal-04')->single;
164
        }
165
        if (empty($ordinalSuffix)) {
166
            $ordinalSuffix = CiteProc::getContext()->getLocale()->filter('terms', 'ordinal')->single;
167
        }
168
        return $num . $ordinalSuffix;
169
    }
170
171
    /**
172
     * @param $num
173
     * @return string
174
     */
175
    public static function longOrdinal($num)
176
    {
177
        $num = sprintf("%02d", $num);
178
        $ret = CiteProc::getContext()->getLocale()->filter('terms', 'long-ordinal-'.$num)->single;
179
        if (!$ret) {
180
            return self::ordinal($num);
181
        }
182
        return $ret;
183
    }
184
185
    /**
186
     * @param string|int $num1
187
     * @param string|int $num2
188
     * @param string $delim
189
     * @return string
190
     */
191
    public function buildNumberRangeString($num1, $num2, $delim)
192
    {
193
194
        if (self::RANGE_DELIMITER_AMPERSAND === $delim) {
195
            $numRange = "$num1 ".htmlentities(self::RANGE_DELIMITER_AMPERSAND)." $num2";
196
        } else {
197
            if (self::RANGE_DELIMITER_COMMA === $delim) {
198
                $numRange = $num1.htmlentities(self::RANGE_DELIMITER_COMMA)." $num2";
199
            } else {
200
                $numRange = $num1.self::RANGE_DELIMITER_HYPHEN.$num2;
201
            }
202
        }
203
        return $numRange;
204
    }
205
206
    /**
207
     * @param string $number
208
     * @return string
209
     */
210
    private function toDecimalNumber($number)
211
    {
212
        $decimalNumber = $number;
213
        if (Util\NumberHelper::isRomanNumber($number)) {
214
            $decimalNumber = Util\NumberHelper::roman2Dec($number);
215
        } else {
216
            $number = mb_strtolower($number);
217
            if (preg_match(Util\NumberHelper::PATTERN_ROMAN_RANGE, $number, $matches)) {
218
                $num1 = Util\NumberHelper::roman2Dec(mb_strtoupper($matches[1]));
219
                $num2 = Util\NumberHelper::roman2Dec(mb_strtoupper($matches[3]));
220
                $decimalNumber = sprintf('%d%s%d', $num1, $matches[2], $num2);
221
            }
222
        }
223
        return $decimalNumber;
224
    }
225
}
226