AssociativeArray::where()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
3
/**
4
 * Associative array class.
5
 *
6
 * @author  Nick Lai <[email protected]>
7
 * @license https://opensource.org/licenses/MIT MIT
8
 * @link    https://github.com/nick-lai/associative-array
9
 */
10
11
namespace NickLai;
12
13
use ArrayAccess;
14
use ArrayIterator;
15
use Countable;
16
use IteratorAggregate;
17
use Traversable;
18
19
class AssociativeArray implements ArrayAccess, Countable, IteratorAggregate
20
{
21
    /**
22
     * The rows contained in the associative array.
23
     *
24
     * @var array
25
     */
26
    protected $rows = [];
27
28
    /**
29
     * Create a new associative array.
30
     *
31
     * @param mixed $rows
32
     * @return void
33
     */
34
    public function __construct($rows = [])
35
    {
36
        $this->rows = $this->getAssociativeRows($rows);
37
    }
38
39
    /**
40
     * Create a new associative array instance.
41
     *
42
     * @param mixed $rows
43
     * @return static
44
     */
45
    public static function make($rows = [])
46
    {
47
        return new static($rows);
48
    }
49
50
    /**
51
     * Get rows of selected columns.
52
     *
53
     * @param string|array $keys
54
     * @return static
55
     */
56
    public function select($keys)
57
    {
58
        if (!is_array($keys)) {
59
            $keys = (array)$keys;
60
        }
61
62
        $keys = array_flip($keys);
63
64
        return self::make(array_map(function($row) use ($keys) {
65
            return array_intersect_key($row, $keys);
66
        }, $this->rows));
67
    }
68
69
    /**
70
     * Filter the rows using the given callback.
71
     *
72
     * @param callable $callback
73
     * @return static
74
     */
75
    public function where(callable $callback)
76
    {
77
        return self::make(array_filter($this->rows, $callback, ARRAY_FILTER_USE_BOTH));
78
    }
79
80
    /**
81
     * Inner join rows
82
     *
83
     * @param array $rows
84
     * @param callable $on
85
     * @return static
86
     */
87
    public function innerJoin($rows, callable $on)
88
    {
89
        $result = [];
90
91
        foreach ($this->rows as $index => $leftRow) {
92
            foreach ($rows as $rightRow) {
93
                if ($on($leftRow, $rightRow)) {
94
                    $result[$index] = $leftRow + $rightRow;
95
                    break;
96
                }
97
            }
98
        }
99
100
        return self::make($result);
101
    }
102
103
    /**
104
     * Left join rows
105
     *
106
     * @param array $rows
107
     * @param callable $on
108
     * @return static
109
     */
110
    public function leftJoin($rows, callable $on)
111
    {
112
        $nullRightRow = [];
113
114
        foreach (self::make($rows)->first() as $key => $value) {
115
            $nullRightRow[$key] = null;
116
        }
117
118
        $result = [];
119
120
        foreach ($this->rows as $index => $leftRow) {
121
            $row = $leftRow + $nullRightRow;
122
            foreach ($rows as $rightRow) {
123
                if ($on($leftRow, $rightRow)) {
124
                    $row = $leftRow + $rightRow;
125
                    break;
126
                }
127
            }
128
            $result[$index] = $row;
129
        }
130
131
        return self::make($result);
132
    }
133
134
    /**
135
     * Right join rows
136
     *
137
     * @param array $rows
138
     * @param callable $on
139
     * @return static
140
     */
141
    public function rightJoin($rows, callable $on)
142
    {
143
        return self::make($rows)->leftJoin($this->rows, $on);
144
    }
145
146
    /**
147
     * Order by keys
148
     *
149
     * @param string|array $keys
150
     * @param string|array $directions
151
     * @param bool $keepIndexAssoc
152
     * @return static
153
     */
154
    public function orderBy($keys, $directions = 'asc', bool $keepIndexAssoc = false)
155
    {
156
        if (!is_array($keys)) {
157
            $keys = (array)$keys;
158
        }
159
160
        $key2IsDesc = [];
161
162
        if (is_string($directions)) {
163
            $isDesc = $directions === 'desc';
164
            foreach ($keys as $key) {
165
                $key2IsDesc[$key] = $isDesc;
166
            }
167
        } else {
168
            $i = 0;
169
            foreach ($keys as $key) {
170
                $key2IsDesc[$key] = (($directions[$i++] ?? 'asc') === 'desc');
171
            }
172
        }
173
174
        $result = $this->rows;
175
176
        $sortFunc = $keepIndexAssoc ? 'uasort' : 'usort';
177
178
        $sortFunc($result, function($a, $b) use ($keys, $key2IsDesc) {
179
            foreach ($keys as $key) {
180
                // Return comparison result if `$a[$key]` not equal to `$b[$key]` else continue next comparing
181
                // Use the spaceship operator to compare
182
                if ($comparedResult = $key2IsDesc[$key]
183
                        ? $b[$key] <=> $a[$key]
184
                        : $a[$key] <=> $b[$key]) {
185
                    return $comparedResult;
186
                }
187
            }
188
            return 0;
189
        });
190
191
        return self::make($result);
192
    }
193
194
    /**
195
     * Groups an associative array by keys.
196
     *
197
     * @param array|string $keys
198
     * @return array
199
     */
200
    public function groupBy($keys)
201
    {
202
        if (!is_array($keys)) {
203
            $keys = (array)$keys;
204
        }
205
206
        return self::quickGroup($this->rows, array_reverse($keys));
207
    }
208
209
    /**
210
     * Return the first row
211
     *
212
     * @param mixed $default
213
     * @return mixed
214
     */
215
    public function first($default = null)
216
    {
217
        foreach ($this->rows as $row) {
218
            return $row;
219
        }
220
221
        return $default;
222
    }
223
224
    /**
225
     * Return the last row
226
     *
227
     * @param mixed $default
228
     * @return mixed
229
     */
230
    public function last($default = null)
231
    {
232
        foreach (array_reverse($this->rows) as $row) {
233
            return $row;
234
        }
235
236
        return $default;
237
    }
238
239
    /**
240
     * Count the number of rows in the associative array.
241
     *
242
     * @return int
243
     */
244
    public function count()
245
    {
246
        return count($this->rows);
247
    }
248
249
    /**
250
     * Get the sum of a given key.
251
     *
252
     * @param string $key
253
     * @return mixed
254
     */
255
    public function sum($key)
256
    {
257
        return array_sum(array_column($this->rows, $key));
258
    }
259
260
    /**
261
     * Get the average value of a given key.
262
     *
263
     * @param string $key
264
     * @return mixed
265
     */
266
    public function avg($key)
267
    {
268
        $sum = $this->sum($key);
269
        return $sum ? ($sum / $this->count()) : $sum;
270
    }
271
272
    /**
273
     * Get the instance as an array.
274
     *
275
     * @return array
276
     */
277
    public function toArray()
278
    {
279
        return array_map(function($row) {
280
            return $row instanceof self ? $row->toArray() : $row;
281
        }, $this->rows);
282
    }
283
284
    /**
285
     * Get an iterator for the rows.
286
     *
287
     * @return \ArrayIterator
288
     */
289
    public function getIterator()
290
    {
291
        return new ArrayIterator($this->rows);
292
    }
293
294
    /**
295
     * Determine if a row exists at an offset.
296
     *
297
     * @param mixed $offset
298
     * @return bool
299
     */
300
    public function offsetExists($offset)
301
    {
302
        return array_key_exists($offset, $this->rows);
303
    }
304
305
    /**
306
     * Get a row at a given offset.
307
     *
308
     * @param mixed $offset
309
     * @return mixed
310
     */
311
    public function offsetGet($offset)
312
    {
313
        return $this->rows[$offset];
314
    }
315
316
    /**
317
     * Set the row at a given offset.
318
     *
319
     * @param mixed $offset
320
     * @param mixed $row
321
     * @return void
322
     */
323
    public function offsetSet($offset, $row)
324
    {
325
        if (is_null($offset)) {
326
            $this->rows[] = $row;
327
        } else {
328
            $this->rows[$offset] = $row;
329
        }
330
    }
331
332
    /**
333
     * Unset the row at a given offset.
334
     *
335
     * @param string $offset
336
     * @return void
337
     */
338
    public function offsetUnset($offset)
339
    {
340
        unset($this->rows[$offset]);
341
    }
342
343
    /**
344
     * Quick grouping.
345
     *
346
     * @param array $rows
347
     * @param array $keys
348
     *
349
     * @return array
350
     */
351
    protected static function quickGroup(array $rows, array $keys)
352
    {
353
        $result = [];
354
355
        $groupKey = array_pop($keys);
356
357
        foreach ($rows as $row) {
358
            $result[$row[$groupKey]][] = $row;
359
        }
360
361
        if (count($keys)) {
362
            foreach ($result as $groupBy => $groupedRows) {
363
                $result[$groupBy] = self::quickGroup($groupedRows, $keys);
364
            }
365
        }
366
367
        return $result;
368
    }
369
370
    /**
371
     * Results array of rows from associative array or traversable.
372
     *
373
     * @param mixed $rows
374
     * @return array
375
     */
376
    protected function getAssociativeRows($rows)
377
    {
378
        if (is_array($rows)) {
379
            return $rows;
380
        } elseif ($rows instanceof self) {
381
            return $rows->toArray();
382
        } elseif ($rows instanceof Traversable) {
383
            return iterator_to_array($rows);
384
        }
385
386
        return (array)$rows;
387
    }
388
}
389