Passed
Pull Request — 2.0 (#28)
by Donald
03:05 queued 01:30
created

Key::resolveIndex()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 19
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 19
rs 9.6111
c 0
b 0
f 0
ccs 0
cts 0
cp 0
cc 5
nc 4
nop 2
crap 30
1
<?php namespace Chekote\NounStore;
2
3
use InvalidArgumentException;
4
5
class Key
6
{
7
    use Singleton;
8
9
    const ORDINAL_ST = 'st';
10
    const ORDINAL_ND = 'nd';
11
    const ORDINAL_RD = 'rd';
12
    const ORDINAL_TH = 'th';
13
14
    protected static $ordinals = [
15
        0 => self::ORDINAL_TH,
16
        1 => self::ORDINAL_ST,
17
        2 => self::ORDINAL_ND,
18
        3 => self::ORDINAL_RD,
19
        4 => self::ORDINAL_TH,
20
        5 => self::ORDINAL_TH,
21
        6 => self::ORDINAL_TH,
22
        7 => self::ORDINAL_TH,
23
        8 => self::ORDINAL_TH,
24
        9 => self::ORDINAL_TH,
25
    ];
26
27
    const REGEX_NTH = '([1-9][0-9]*)(?:st|nd|rd|th)';
28
    const REGEX_KEY = '/^(' . self::REGEX_NTH . " )?([^']+)('s ([^.]+))?$/";
29
30
    const REGEX_GROUP_KEY_NTH = 2;
31
    const REGEX_GROUP_KEY = 3;
32
    const REGEX_GROUP_PROPERTY = 5;
33
34
    /**
35
     * Builds a key from it's separate key and index values.
36
     *
37
     * @example buildKey("Item", null): "Item"
38
     * @example buildKey("Item", 0): "1st Item"
39
     * @example buildKey("Item", 1): "2nd Item"
40
     * @example buildKey("Item", 2): "3rd Item"
41
     *
42 3
     * @param  string                   $key   The key to check.
43
     * @param  int|null                 $index The index (zero indexed) value for the key. If not specified, the method
44 3
     *                                         will not add an index notation to the key.
45 1
     * @throws InvalidArgumentException if $index is less than -1. Note: It should really be zero or higher, but this
46
     *                                        method does not assert that. The error is bubbling up from getOrdinal()
47
     * @return string                   the key with the index, or just the key if index is null.
48 2
     */
49
    public function build($key, $index)
50 2
    {
51
        if ($index === null) {
52
            return $key;
53
        }
54
55
        $nth = $index + 1;
56
57
        return $nth . $this->getOrdinal($nth) . ' ' . $key;
58
    }
59
60 24
    /**
61
     * Provides the ordinal notation for the specified nth number.
62 24
     *
63 1
     * @param  int                      $nth the number to determine the ordinal for
64
     * @throws InvalidArgumentException if $nth is not a positive number.
65
     * @return string                   the ordinal
66 23
     */
67
    public function getOrdinal($nth)
68
    {
69
        if ($nth < 0) {
70
            throw new InvalidArgumentException('$nth must be a positive number');
71
        }
72
73
        return $nth > 9 && $nth < 20 ? self::ORDINAL_TH : self::$ordinals[substr($nth, -1)];
74
    }
75
76
    /**
77
     * Parses a key into the separate key and index value.
78
     *
79
     * @example parseKey("Item"): ["Item", null, null]
80
     * @example parseKey("Item", 1): ["Item", 1, null]
81
     * @example parseKey("1st Item"): ["Item", 0, null]
82
     * @example parseKey("2nd Item"): ["Item", 1, null]
83
     * @example parseKey("3rd Item"): ["Item", 2, null]
84
     * @example parseKey("3rd Item's price"): ["Item", 2, "price"]
85 14
     *
86
     * @param  string                   $key   the key to parse.
87 14
     * @param  int                      $index [optional] the index to return if the key does not contain one.
88 11
     * @throws InvalidArgumentException if both an $index and $key are provided, but the $key contains an nth value
89 5
     *                                        that does not match the index.
90 5
     * @return array                    a tuple, the 1st being the key with the nth and property removed, the 2nd
91
     *                                        being the index, and the third being the property (or null if no property
92
     *                                        was found).
93
     */
94 6
    public function parse($key, $index = null)
95 6
    {
96
        // @todo use PREG_UNMATCHED_AS_NULL when upgrading to PHP 7.2
97
        if (!preg_match(self::REGEX_KEY, $key, $matches)) {
98 9
            throw new InvalidArgumentException('Key is not valid. Must match pattern ' . self::REGEX_KEY);
99
        }
100
101
        return $this->processMatches($index, $matches);
102
    }
103
104
    /**
105
     * Process the matches resulting from parsing a key via regex.
106
     *
107
     * @param  string                   $index   the index provided along with the original key.
108
     * @param  array                    $matches the matches resulting from parsing the key.
109
     * @throws InvalidArgumentException if both an $index and $key are provided, but the $key contains an nth value
110
     *                                          that does not match the index.
111
     * @throws InvalidArgumentException if nth is not null and is less than 1
112
     * @return array                    a tuple, the 1st being the key with the nth and property removed, the 2nd being the index,
113
     *                                          and the third being the property (or null if no property was found).
114
     */
115
    protected function processMatches($index, array $matches)
116
    {
117
        // @todo remove ternary once PREG_UNMATCHED_AS_NULL is used to generate $matches
118
        $index = $this->resolveIndex($index, $matches[self::REGEX_GROUP_KEY_NTH] ?: null);
0 ignored issues
show
Bug introduced by
$index of type string is incompatible with the type null|integer expected by parameter $index of Chekote\NounStore\Key::resolveIndex(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

118
        $index = $this->resolveIndex(/** @scrutinizer ignore-type */ $index, $matches[self::REGEX_GROUP_KEY_NTH] ?: null);
Loading history...
119
        $key = $matches[self::REGEX_GROUP_KEY];
120
121
        // @todo use Null coalescing operator when upgrading to PHP 7
122
        $property = null;
123
        if (isset($matches[self::REGEX_GROUP_PROPERTY])) {
124
            $property = $matches[self::REGEX_GROUP_PROPERTY] ?: null;
125
        }
126
127
        return [$key, $index, $property];
128
    }
129
130
    /**
131
     * Resolves an index and parsed nth value to an index.
132
     *
133
     * Ensures that if both an index and parsed nth value are provided, that they are equivalent. If only one is
134
     * provided, then the appropriate index will be returned. e.g. if an index is provided, it is returned as-is, as
135
     * it is already an index. If an nth is provided, it will be returned decremented by 1.
136
     *
137
     * @param  int|null                 $index the index to process
138
     * @param  int|null                 $nth   the nth to process
139
     * @throws InvalidArgumentException if both an $index and $key are provided, but the $key contains an nth value
140
     *                                        that does not match the index.
141
     * @throws InvalidArgumentException if nth is not null and is less than 1
142
     * @return int                      the resolved index.
143
     */
144
    protected function resolveIndex($index, $nth)
145
    {
146
        // If we don't have an nth, there's nothing to process. We'll just return the $index, even if it's null.
147
        if ($nth === null) {
148
            return $index;
149
        }
150
151
        $decrementedNth = $nth - 1;
152
153
        // If both index and nth are provided, but they aren't equivalent, we need to error out.
154
        if ($index !== null && $index !== $decrementedNth) {
155
            throw new InvalidArgumentException("index $index was provided with nth $nth, but they are not equivalent");
156
        }
157
158
        if ($decrementedNth < 0) {
159
            throw new InvalidArgumentException('nth must be equal to or larger than 1');
160
        }
161
162
        return $decrementedNth;
163
    }
164
}
165