f7
f7 is a spreadsheet formula execution library
git clone https://git.vogt.world/f7.git
Log | Files | README.md | LICENSE.md
← All files
name: src/main/js/common/models/Grid.ts
-rw-r--r--
7913
  1import { InvalidArgumentError } from "../errors/InvalidArgumentError";
  2import { isUndefined } from "../utils/Types";
  3
  4/**
  5 * A grid is the fundamental 2D data structure of F7. It should NOT be nested. Values are either filled with an
  6 * object, or left null, although the meaning of an intentionally stored null value depends on where this structure is
  7 * used.
  8 *
  9 * @param <T> - type stored.
 10 */
 11export class Grid<T> {
 12  /**
 13   * Store each value by a row-column key.
 14   * The thing to remember here is that the major dimension is ROWS.
 15   */
 16  private grid: Array<Array<T>>;
 17  /**
 18   * A grid can have a column and row size even if there are no values. But there are many places where we need to know
 19   * how big the grid is, regardless of how full it is. This is the number of columns in this grid.
 20   */
 21  private columns = 0;
 22  /**
 23   * This is the number of rows in this grid.
 24   */
 25  private rows = 0;
 26
 27  constructor(columns: number, rows: number) {
 28    if (columns < 0 || rows < 0) {
 29      throw new InvalidArgumentError("Column size or row size for grid cannot be less than 0.");
 30    }
 31    this.columns = columns;
 32    this.rows = rows;
 33    this.grid = new Array<Array<T>>(rows).fill(undefined);
 34    this.grid = this.grid.map((_) => new Array<T>(columns).fill(undefined));
 35  }
 36
 37  /**
 38   * Start a grid builder.
 39   * @deprecated
 40   */
 41  static builder<T = any>(): GridBuilder<T> {
 42    return new GridBuilder<any>();
 43  }
 44
 45  /**
 46   * Create a grid from an array grid, where the major dimension is rows.
 47   * @param data - to fill new grid with.
 48   */
 49  static from<T>(data: Array<Array<T>>): Grid<T> {
 50    const created = new Grid<T>(0, 0);
 51    data.forEach((rowArray, row) =>
 52      rowArray.forEach((value, column) => created.set(column, row, value))
 53    );
 54    return created;
 55  }
 56
 57  /**
 58   * Set a value in the grid, expanding the grid if it does not contain the index.
 59   * @param column - column index to set.
 60   * @param row - row index to set.
 61   * @param value - value to set.
 62   */
 63  set(column: number, row: number, value: T) {
 64    this.expand(column + 1, row + 1);
 65    this.grid[row][column] = value;
 66  }
 67
 68  /**
 69   * Get a value if it exists in the grid, defaulting to null when it does not exist or it is undefined.
 70   * @param column - column index.
 71   * @param row - row index.
 72   */
 73  get(column: number, row: number) {
 74    if (column > this.columns - 1 || row > this.rows - 1) {
 75      return null;
 76    }
 77    const value = this.grid[row][column];
 78    return isUndefined(value) ? null : value;
 79  }
 80
 81  /**
 82   * Get the number of columns in this grid.
 83   */
 84  getColumns(): number {
 85    return this.columns;
 86  }
 87
 88  /**
 89   * Get the number of rows in this grid.
 90   */
 91  getRows(): number {
 92    return this.rows;
 93  }
 94
 95  /**
 96   * Expand number of columns, if columns is larger than current number of columns.
 97   * @param columns - to possibly expand to.
 98   */
 99  setColumns(columns: number) {
100    if (columns > this.columns) {
101      this.grid = this.grid.map((row) =>
102        row.concat(new Array(columns - this.columns).fill(undefined))
103      );
104      this.columns = columns;
105    }
106  }
107
108  /**
109   * Expand number of rows, if rows is larger than current number of rows.
110   * @param rows - to possibly expand to.
111   */
112  setRows(rows: number) {
113    if (rows > this.rows) {
114      this.grid = this.grid.concat(
115        new Array(rows - this.rows)
116          .fill(null)
117          .map((_) => new Array<T>(this.columns).fill(undefined))
118      );
119      this.rows = rows;
120    }
121  }
122
123  /**
124   * Expand this grid to the number of rows and columns, if they are larger than existing rows and columns respectively.
125   * @param columns - new column count.
126   * @param rows - new column count.
127   */
128  expand(columns: number, rows: number) {
129    this.setRows(rows);
130    this.setColumns(columns);
131  }
132
133  /**
134   * Resize the current grid, retaining values if they fall inside the range of the new grid, removing them if not.
135   * If we're expanding the grid, this is basically the same as using the expand method.
136   * @param columns - new number of columns.
137   * @param rows - new number of rows.
138   */
139  resizeWithDelete(columns: number, rows: number) {
140    const newGrid = new Grid<T>(columns, rows);
141    for (let row = rows - 1; row >= 0; row--) {
142      for (let column = columns - 1; column >= 0; column--) {
143        newGrid.set(column, row, this.get(column, row));
144      }
145    }
146    this.grid = newGrid.raw();
147    this.columns = columns;
148    this.rows = rows;
149  }
150
151  /**
152   * Log to a table for debugging.
153   */
154  log() {
155    console.table(this.grid);
156  }
157
158  /**
159   * Add one grid to this one in the column-wise dimension (right.)
160   *
161   * @param other grid
162   */
163  addGridToRight(other: Grid<T>) {
164    const oldGridColumnSize = this.getColumns();
165    for (let row = other.getRows() - 1; row >= 0; row--) {
166      for (let column = other.getColumns() - 1; column >= 0; column--) {
167        this.set(column + oldGridColumnSize, row, other.get(column, row));
168      }
169    }
170  }
171
172  /**
173   * Add a single value in the 0th row, after the last column, increasing the column count.
174   *
175   * @param value - to add.
176   */
177  addOneToRight(value: T) {
178    this.set(this.columns, 0, value);
179  }
180
181  /**
182   * Add one grid to this one in the row-wise dimension (bottom.)
183   *
184   * @param other grid
185   */
186  addGridToBottom(other: Grid<T>) {
187    const oldGridRowSize = this.getRows();
188    for (let row = other.getRows() - 1; row >= 0; row--) {
189      for (let column = other.getColumns() - 1; column >= 0; column--) {
190        this.set(column, row + oldGridRowSize, other.get(column, row));
191      }
192    }
193  }
194
195  /**
196   * Remove a value at a given row and column.
197   *
198   * @param column - index.
199   * @param row    - index.
200   */
201  remove(column: number, row: number) {
202    if (column < this.columns && row < this.rows) {
203      this.grid[row][column] = undefined;
204    }
205  }
206
207  /**
208   * Does this grid only have one value?
209   * @return true if it just has one value.
210   */
211  isSingle(): boolean {
212    return this.rows === 1 && this.columns === 1;
213  }
214
215  /**
216   * Set an empty/blank/null value at a row/column.
217   *
218   * @param column - index.
219   * @param row    - index.
220   */
221  setNull(column: number, row: number) {
222    this.set(column, row, null);
223  }
224
225  /**
226   * Is the grid empty?
227   *
228   * @return true if it contains no values.
229   */
230  isEmpty(): boolean {
231    return (
232      (this.rows === 0 && this.columns === 0) ||
233      this.grid
234        .map((row) => row.filter(isUndefined).length)
235        .reduce((first: number, second: number) => first + second, 0) > 0
236    );
237  }
238
239  /**
240   * Has dimensions (is at least 1x1) but all are undefined.
241   */
242  hasDimensionsButIsAllUndefined() {
243    return this.rows > 0 && this.columns > 0 && this.allAreUndefined();
244  }
245
246  /**
247   * Map each value to a new Grid.
248   * @param mapper - maps instance of T to instance of of R.
249   */
250  map<R>(mapper: (t: T) => R): Grid<R> {
251    const toReturn = new Grid<R>(this.columns, this.rows);
252    for (let row = this.getRows() - 1; row >= 0; row--) {
253      for (let column = this.getColumns() - 1; column >= 0; column--) {
254        toReturn.set(column, row, mapper(this.get(column, row)));
255      }
256    }
257    return toReturn;
258  }
259
260  /**
261   * Get the raw grid.
262   */
263  raw(): Array<Array<T>> {
264    return this.grid;
265  }
266
267  /**
268   * Are all values undefined?
269   */
270  private allAreUndefined(): boolean {
271    return (
272      this.grid
273        .map((row) => row.filter(isUndefined).length)
274        .reduce((first: number, second: number) => first + second, 0) > 0
275    );
276  }
277}
278
279/**
280 * Grid builder. Basically chains "add" so we can do one-statement building.
281 * @deprecated
282 */
283class GridBuilder<T> {
284  private internal: Grid<T> = new Grid<T>(0, 0);
285
286  add(columnIndex: number, rowIndex: number, value: T): GridBuilder<T> {
287    this.internal.set(columnIndex, rowIndex, value);
288    return this;
289  }
290
291  build(): Grid<T> {
292    return this.internal;
293  }
294}