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