Passed
Push — related_object ( 8af542 )
by Donald
03:06
created

Key::resolveIndex()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 18
c 0
b 0
f 0
rs 9.6111
cc 5
nc 4
nop 2
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
     * @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
     *                                         will not add an index notation to the key.
45
     * @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
     */
49
    public function build($key, $index)
50
    {
51
        if ($index === null) {
52
            return $key;
53
        }
54
55
        $nth = $index + 1;
56
57
        return $nth . $this->getOrdinal($nth) . ' ' . $key;
58
    }
59
60
    /**
61
     * Provides the ordinal notation for the specified nth number.
62
     *
63
     * @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
     */
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
     *
86
     * @param  string                   $key   the key to parse.
87
     * @param  int                      $index [optional] the index to return if the key does not contain one.
88
     * @throws InvalidArgumentException if both an $index and $key are provided, but the $key contains an nth value
89
     *                                        that does not match the index.
90
     * @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
    public function parse($key, $index = null)
95
    {
96
        // @todo use PREG_UNMATCHED_AS_NULL when upgrading to PHP 7.2
97
        if (!preg_match(self::REGEX_KEY, $key, $matches)) {
98
            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
        // @todo remove ternary once PREG_UNMATCHED_AS_NULL is used to generate $matches
117
        $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

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