Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

   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  
  11  namespace 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   */
  37  class 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  }