1<?php
2
3/**
4 *
5 * Class for the management of Matrices
6 *
7 * @copyright  Copyright (c) 2018 Mark Baker (https://github.com/MarkBaker/PHPMatrix)
8 * @license    https://opensource.org/licenses/MIT    MIT
9 */
10
11namespace Matrix;
12
13/**
14 * Matrix object.
15 *
16 * @package Matrix
17 *
18 * @property-read int $rows The number of rows in the matrix
19 * @property-read int $columns The number of columns in the matrix
20 * @method Matrix antidiagonal()
21 * @method Matrix adjoint()
22 * @method Matrix cofactors()
23 * @method float determinant()
24 * @method Matrix diagonal()
25 * @method Matrix identity()
26 * @method Matrix inverse()
27 * @method Matrix pseudoInverse()
28 * @method Matrix minors()
29 * @method float trace()
30 * @method Matrix transpose()
31 * @method Matrix add(...$matrices)
32 * @method Matrix subtract(...$matrices)
33 * @method Matrix multiply(...$matrices)
34 * @method Matrix divideby(...$matrices)
35 * @method Matrix divideinto(...$matrices)
36 */
37class Matrix
38{
39    protected $rows;
40    protected $columns;
41    protected $grid = [];
42
43    /*
44     * Create a new Matrix object from an array of values
45     *
46     * @param array $grid
47     */
48    final public function __construct(array $grid)
49    {
50        $this->buildFromArray(array_values($grid));
51    }
52
53    /*
54     * Create a new Matrix object from an array of values
55     *
56     * @param array $grid
57     */
58    protected function buildFromArray(array $grid): void
59    {
60        $this->rows = count($grid);
61        $columns = array_reduce(
62            $grid,
63            function ($carry, $value) {
64                return max($carry, is_array($value) ? count($value) : 1);
65            }
66        );
67        $this->columns = $columns;
68
69        array_walk(
70            $grid,
71            function (&$value) use ($columns) {
72                if (!is_array($value)) {
73                    $value = [$value];
74                }
75                $value = array_pad(array_values($value), $columns, null);
76            }
77        );
78
79        $this->grid = $grid;
80    }
81
82    /**
83     * Validate that a row number is a positive integer
84     *
85     * @param int $row
86     * @return int
87     * @throws Exception
88     */
89    public static function validateRow(int $row): int
90    {
91        if ((!is_numeric($row)) || (intval($row) < 1)) {
92            throw new Exception('Invalid Row');
93        }
94
95        return (int)$row;
96    }
97
98    /**
99     * Validate that a column number is a positive integer
100     *
101     * @param int $column
102     * @return int
103     * @throws Exception
104     */
105    public static function validateColumn(int $column): int
106    {
107        if ((!is_numeric($column)) || (intval($column) < 1)) {
108            throw new Exception('Invalid Column');
109        }
110
111        return (int)$column;
112    }
113
114    /**
115     * Validate that a row number falls within the set of rows for this matrix
116     *
117     * @param int $row
118     * @return int
119     * @throws Exception
120     */
121    protected function validateRowInRange(int $row): int
122    {
123        $row = static::validateRow($row);
124        if ($row > $this->rows) {
125            throw new Exception('Requested Row exceeds matrix size');
126        }
127
128        return $row;
129    }
130
131    /**
132     * Validate that a column number falls within the set of columns for this matrix
133     *
134     * @param int $column
135     * @return int
136     * @throws Exception
137     */
138    protected function validateColumnInRange(int $column): int
139    {
140        $column = static::validateColumn($column);
141        if ($column > $this->columns) {
142            throw new Exception('Requested Column exceeds matrix size');
143        }
144
145        return $column;
146    }
147
148    /**
149     * Return a new matrix as a subset of rows from this matrix, starting at row number $row, and $rowCount rows
150     * A $rowCount value of 0 will return all rows of the matrix from $row
151     * A negative $rowCount value will return rows until that many rows from the end of the matrix
152     *
153     * Note that row numbers start from 1, not from 0
154     *
155     * @param int $row
156     * @param int $rowCount
157     * @return static
158     * @throws Exception
159     */
160    public function getRows(int $row, int $rowCount = 1): Matrix
161    {
162        $row = $this->validateRowInRange($row);
163        if ($rowCount === 0) {
164            $rowCount = $this->rows - $row + 1;
165        }
166
167        return new static(array_slice($this->grid, $row - 1, (int)$rowCount));
168    }
169
170    /**
171     * Return a new matrix as a subset of columns from this matrix, starting at column number $column, and $columnCount columns
172     * A $columnCount value of 0 will return all columns of the matrix from $column
173     * A negative $columnCount value will return columns until that many columns from the end of the matrix
174     *
175     * Note that column numbers start from 1, not from 0
176     *
177     * @param int $column
178     * @param int $columnCount
179     * @return Matrix
180     * @throws Exception
181     */
182    public function getColumns(int $column, int $columnCount = 1): Matrix
183    {
184        $column = $this->validateColumnInRange($column);
185        if ($columnCount < 1) {
186            $columnCount = $this->columns + $columnCount - $column + 1;
187        }
188
189        $grid = [];
190        for ($i = $column - 1; $i < $column + $columnCount - 1; ++$i) {
191            $grid[] = array_column($this->grid, $i);
192        }
193
194        return (new static($grid))->transpose();
195    }
196
197    /**
198     * Return a new matrix as a subset of rows from this matrix, dropping rows starting at row number $row,
199     *     and $rowCount rows
200     * A negative $rowCount value will drop rows until that many rows from the end of the matrix
201     * A $rowCount value of 0 will remove all rows of the matrix from $row
202     *
203     * Note that row numbers start from 1, not from 0
204     *
205     * @param int $row
206     * @param int $rowCount
207     * @return static
208     * @throws Exception
209     */
210    public function dropRows(int $row, int $rowCount = 1): Matrix
211    {
212        $this->validateRowInRange($row);
213        if ($rowCount === 0) {
214            $rowCount = $this->rows - $row + 1;
215        }
216
217        $grid = $this->grid;
218        array_splice($grid, $row - 1, (int)$rowCount);
219
220        return new static($grid);
221    }
222
223    /**
224     * Return a new matrix as a subset of columns from this matrix, dropping columns starting at column number $column,
225     *     and $columnCount columns
226     * A negative $columnCount value will drop columns until that many columns from the end of the matrix
227     * A $columnCount value of 0 will remove all columns of the matrix from $column
228     *
229     * Note that column numbers start from 1, not from 0
230     *
231     * @param int $column
232     * @param int $columnCount
233     * @return static
234     * @throws Exception
235     */
236    public function dropColumns(int $column, int $columnCount = 1): Matrix
237    {
238        $this->validateColumnInRange($column);
239        if ($columnCount < 1) {
240            $columnCount = $this->columns + $columnCount - $column + 1;
241        }
242
243        $grid = $this->grid;
244        array_walk(
245            $grid,
246            function (&$row) use ($column, $columnCount) {
247                array_splice($row, $column - 1, (int)$columnCount);
248            }
249        );
250
251        return new static($grid);
252    }
253
254    /**
255     * Return a value from this matrix, from the "cell" identified by the row and column numbers
256     * Note that row and column numbers start from 1, not from 0
257     *
258     * @param int $row
259     * @param int $column
260     * @return mixed
261     * @throws Exception
262     */
263    public function getValue(int $row, int $column)
264    {
265        $row = $this->validateRowInRange($row);
266        $column = $this->validateColumnInRange($column);
267
268        return $this->grid[$row - 1][$column - 1];
269    }
270
271    /**
272     * Returns a Generator that will yield each row of the matrix in turn as a vector matrix
273     *     or the value of each cell if the matrix is a vector
274     *
275     * @return \Generator|Matrix[]|mixed[]
276     */
277    public function rows(): \Generator
278    {
279        foreach ($this->grid as $i => $row) {
280            yield $i + 1 => ($this->columns == 1)
281                ? $row[0]
282                : new static([$row]);
283        }
284    }
285
286    /**
287     * Returns a Generator that will yield each column of the matrix in turn as a vector matrix
288     *     or the value of each cell if the matrix is a vector
289     *
290     * @return \Generator|Matrix[]|mixed[]
291     */
292    public function columns(): \Generator
293    {
294        for ($i = 0; $i < $this->columns; ++$i) {
295            yield $i + 1 => ($this->rows == 1)
296                ? $this->grid[0][$i]
297                : new static(array_column($this->grid, $i));
298        }
299    }
300
301    /**
302     * Identify if the row and column dimensions of this matrix are equal,
303     *     i.e. if it is a "square" matrix
304     *
305     * @return bool
306     */
307    public function isSquare(): bool
308    {
309        return $this->rows == $this->columns;
310    }
311
312    /**
313     * Identify if this matrix is a vector
314     *     i.e. if it comprises only a single row or a single column
315     *
316     * @return bool
317     */
318    public function isVector(): bool
319    {
320        return $this->rows == 1 || $this->columns == 1;
321    }
322
323    /**
324     * Return the matrix as a 2-dimensional array
325     *
326     * @return array
327     */
328    public function toArray(): array
329    {
330        return $this->grid;
331    }
332
333    protected static $getters = [
334        'rows',
335        'columns',
336    ];
337
338    /**
339     * Access specific properties as read-only (no setters)
340     *
341     * @param string $propertyName
342     * @return mixed
343     * @throws Exception
344     */
345    public function __get(string $propertyName)
346    {
347        $propertyName = strtolower($propertyName);
348
349        // Test for function calls
350        if (in_array($propertyName, self::$getters)) {
351            return $this->$propertyName;
352        }
353
354        throw new Exception('Property does not exist');
355    }
356
357    protected static $functions = [
358        'antidiagonal',
359        'adjoint',
360        'cofactors',
361        'determinant',
362        'diagonal',
363        'identity',
364        'inverse',
365        'minors',
366        'trace',
367        'transpose',
368    ];
369
370    protected static $operations = [
371        'add',
372        'subtract',
373        'multiply',
374        'divideby',
375        'divideinto',
376        'directsum',
377    ];
378
379    /**
380     * Returns the result of the function call or operation
381     *
382     * @param string $functionName
383     * @param mixed[] $arguments
384     * @return Matrix|float
385     * @throws Exception
386     */
387    public function __call(string $functionName, $arguments)
388    {
389        $functionName = strtolower(str_replace('_', '', $functionName));
390
391        if (in_array($functionName, self::$functions, true) || in_array($functionName, self::$operations, true)) {
392            $functionName = "\\" . __NAMESPACE__ . "\\{$functionName}";
393            if (is_callable($functionName)) {
394                $arguments = array_values(array_merge([$this], $arguments));
395                return call_user_func_array($functionName, $arguments);
396            }
397        }
398        throw new Exception('Function or Operation does not exist');
399    }
400}
401