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}