1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
|
<?php /** * A class of utilities for dealing with arrays. */
namespace Automattic\WooCommerce\Utilities;
/** * A class of utilities for dealing with arrays. */ class ArrayUtil {
/** * Automatic selector type for the 'select' method. */ public const SELECT_BY_AUTO = 0;
/** * Object method selector type for the 'select' method. */ public const SELECT_BY_OBJECT_METHOD = 1;
/** * Object property selector type for the 'select' method. */ public const SELECT_BY_OBJECT_PROPERTY = 2;
/** * Array key selector type for the 'select' method. */ public const SELECT_BY_ARRAY_KEY = 3;
/** * Get a value from an nested array by specifying the entire key hierarchy with '::' as separator. * * E.g. for [ 'foo' => [ 'bar' => [ 'fizz' => 'buzz' ] ] ] the value for key 'foo::bar::fizz' would be 'buzz'. * * @param array $items The array to get the value from. * @param string $key The complete key hierarchy, using '::' as separator. * @param mixed $default_value The value to return if the key doesn't exist in the array. * * @return mixed The retrieved value, or the supplied default value. * @throws \Exception $array is not an array. */ public static function get_nested_value( array $items, string $key, $default_value = null ) { $key_stack = explode( '::', $key ); $subkey = array_shift( $key_stack );
if ( isset( $items[ $subkey ] ) ) { $value = $items[ $subkey ];
if ( count( $key_stack ) ) { foreach ( $key_stack as $subkey ) { if ( is_array( $value ) && isset( $value[ $subkey ] ) ) { $value = $value[ $subkey ]; } else { $value = $default_value; break; } } } } else { $value = $default_value; }
return $value; }
/** * Checks if a given key exists in an array and its value can be evaluated as 'true'. * * @param array $items The array to check. * @param string $key The key for the value to check. * @return bool True if the key exists in the array and the value can be evaluated as 'true'. */ public static function is_truthy( array $items, string $key ) { return isset( $items[ $key ] ) && $items[ $key ]; }
/** * Gets the value for a given key from an array, or a default value if the key doesn't exist in the array. * * This is equivalent to "$array[$key] ?? $default" except in one case: * when they key exists, has a null value, and a non-null default is supplied: * * $array = ['key' => null] * $array['key'] ?? 'default' => 'default' * ArrayUtil::get_value_or_default($array, 'key', 'default') => null * * @param array $items The array to get the value from. * @param string $key The key to use to retrieve the value. * @param null $default_value The default value to return if the key doesn't exist in the array. * @return mixed|null The value for the key, or the default value passed. */ public static function get_value_or_default( array $items, string $key, $default_value = null ) { return array_key_exists( $key, $items ) ? $items[ $key ] : $default_value; }
/** * Converts an array of numbers to a human-readable range, such as "1,2,3,5" to "1-3, 5". It also supports * floating point numbers, however with some perhaps unexpected / undefined behaviour if used within a range. * Source: https://stackoverflow.com/a/34254663/4574 * * @param array $items An array (in any order, see $sort) of individual numbers. * @param string $item_separator The string that separates sequential range groups. Defaults to ', '. * @param string $range_separator The string that separates ranges. Defaults to '-'. A plausible example otherwise would be ' to '. * @param bool|true $sort Sort the array prior to iterating? You'll likely always want to sort, but if not, you can set this to false. * * @return string */ public static function to_ranges_string( array $items, string $item_separator = ', ', string $range_separator = '-', bool $sort = true ): string { if ( $sort ) { sort( $items ); }
$point = null; $range = false; $str = '';
foreach ( $items as $i ) { if ( null === $point ) { $str .= $i; } elseif ( ( $point + 1 ) === $i ) { $range = true; } else { if ( $range ) { $str .= $range_separator . $point; $range = false; } $str .= $item_separator . $i; } $point = $i; }
if ( $range ) { $str .= $range_separator . $point; }
return $str; }
/** * Helper function to generate a callback which can be executed on an array to select a value from each item. * * @param string $selector_name Field/property/method name to select. * @param int $selector_type Selector type. * * @return \Closure Callback to select the value. */ private static function get_selector_callback( string $selector_name, int $selector_type = self::SELECT_BY_AUTO ): \Closure { if ( self::SELECT_BY_OBJECT_METHOD === $selector_type ) { $callback = function ( $item ) use ( $selector_name ) { return $item->$selector_name(); }; } elseif ( self::SELECT_BY_OBJECT_PROPERTY === $selector_type ) { $callback = function ( $item ) use ( $selector_name ) { return $item->$selector_name; }; } elseif ( self::SELECT_BY_ARRAY_KEY === $selector_type ) { $callback = function ( $item ) use ( $selector_name ) { return $item[ $selector_name ]; }; } else { $callback = function ( $item ) use ( $selector_name ) { if ( is_array( $item ) ) { return $item[ $selector_name ]; } elseif ( method_exists( $item, $selector_name ) ) { return $item->$selector_name(); } else { return $item->$selector_name; } }; } return $callback; }
/** * Select one single value from all the items in an array of either arrays or objects based on a selector. * For arrays, the selector is a key name; for objects, the selector can be either a method name or a property name. * * @param array $items Items to apply the selection to. * @param string $selector_name Key, method or property name to use as a selector. * @param int $selector_type Selector type, one of the SELECT_BY_* constants. * @return array The selected values. */ public static function select( array $items, string $selector_name, int $selector_type = self::SELECT_BY_AUTO ): array { $callback = self::get_selector_callback( $selector_name, $selector_type ); return array_map( $callback, $items ); }
/** * Returns a new assoc array with format [ $key1 => $item1, $key2 => $item2, ... ] where $key is the value of the selector and items are original items passed. * * @param array $items Items to use for conversion. * @param string $selector_name Key, method or property name to use as a selector. * @param int $selector_type Selector type, one of the SELECT_BY_* constants. * * @return array The converted assoc array. */ public static function select_as_assoc( array $items, string $selector_name, int $selector_type = self::SELECT_BY_AUTO ): array { $selector_callback = self::get_selector_callback( $selector_name, $selector_type ); $result = array(); foreach ( $items as $item ) { $key = $selector_callback( $item ); self::ensure_key_is_array( $result, $key ); $result[ $key ][] = $item; } return $result; }
/** * Returns whether two assoc array are same. The comparison is done recursively by keys, and the functions returns on first difference found. * * @param array $array1 First array to compare. * @param array $array2 Second array to compare. * @param bool $strict Whether to use strict comparison. * * @return bool Whether the arrays are different. */ public static function deep_compare_array_diff( array $array1, array $array2, bool $strict = true ) { return self::deep_compute_or_compare_array_diff( $array1, $array2, true, $strict ); }
/** * Computes difference between two assoc arrays recursively. Similar to PHP's native assoc_array_diff, but also supports nested arrays. * * @param array $array1 First array. * @param array $array2 Second array. * @param bool $strict Whether to also match type of values. * * @return array The difference between the two arrays. */ public static function deep_assoc_array_diff( array $array1, array $array2, bool $strict = true ): array { return self::deep_compute_or_compare_array_diff( $array1, $array2, false, $strict ); }
/** * Helper method to compare to compute difference between two arrays. Comparison is done recursively. * * @param array $array1 First array. * @param array $array2 Second array. * @param bool $compare Whether to compare the arrays. If true, then function will return false on first difference, in order to be slightly more efficient. * @param bool $strict Whether to do string comparison. * * @return array|bool The difference between the two arrays, or if array are same, depending upon $compare param. */ private static function deep_compute_or_compare_array_diff( array $array1, array $array2, bool $compare, bool $strict = true ) { $diff = array(); foreach ( $array1 as $key => $value ) { if ( is_array( $value ) ) { if ( ! array_key_exists( $key, $array2 ) || ! is_array( $array2[ $key ] ) ) { if ( $compare ) { return true; } $diff[ $key ] = $value; continue; } $new_diff = self::deep_assoc_array_diff( $value, $array2[ $key ], $strict ); if ( ! empty( $new_diff ) ) { if ( $compare ) { return true; } $diff[ $key ] = $new_diff; } } elseif ( $strict ) { if ( ! array_key_exists( $key, $array2 ) || $value !== $array2[ $key ] ) { if ( $compare ) { return true; } $diff[ $key ] = $value; } // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual -- Intentional when $strict is false. } elseif ( ! array_key_exists( $key, $array2 ) || $value != $array2[ $key ] ) { if ( $compare ) { return true; } $diff[ $key ] = $value; } }
return $compare ? false : $diff; }
/** * Push a value to an array, but only if the value isn't in the array already. * * @param array $items The array. * @param mixed $value The value to maybe push. * @return bool True if the value has been added to the array, false if the value was already in the array. */ public static function push_once( array &$items, $value ): bool { if ( in_array( $value, $items, true ) ) { return false; }
$items[] = $value; return true; }
/** * Ensure that an associative array has a given key, and if not, set the key to an empty array. * * @param array $items The array to check. * @param string $key The key to check. * @param bool $throw_if_existing_is_not_array If true, an exception will be thrown if the key already exists in the array but the value is not an array. * @return bool True if the key has been added to the array, false if not (the key already existed). * @throws \Exception The key already exists in the array but the value is not an array. */ public static function ensure_key_is_array( array &$items, string $key, bool $throw_if_existing_is_not_array = false ): bool { if ( ! isset( $items[ $key ] ) ) { $items[ $key ] = array(); return true; }
if ( $throw_if_existing_is_not_array && ! is_array( $items[ $key ] ) ) { $type = is_object( $items[ $key ] ) ? get_class( $items[ $key ] ) : gettype( $items[ $key ] ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped throw new \Exception( "Array key exists but it's not an array, it's a {$type}" ); }
return false; }
/** * Given an array of associative arrays, all having a shared key name ("column"), generates a new array in which * keys are the distinct column values found, and values are arrays with all the matches found * (or only the last matching array found, if $single_values is true). * See ArrayUtilTest for examples. * * @param array $items The array to process. * @param string $column The name of the key to group by. * @param bool $single_values True to only return the last suitable array found for each column value. * @return array The grouped array. */ public static function group_by_column( array $items, string $column, bool $single_values = false ): array { if ( $single_values ) { return array_combine( array_column( $items, $column ), array_values( $items ) ); }
$distinct_column_values = array_unique( array_column( $items, $column ), SORT_REGULAR ); $result = array_fill_keys( $distinct_column_values, array() );
foreach ( $items as $value ) { $result[ $value[ $column ] ][] = $value; }
return $result; } }
|