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}