1 package org.djunits.vecmat.table;
2
3 import java.util.Objects;
4
5 import org.djunits.formatter.TableFormat;
6 import org.djunits.formatter.TableFormatter;
7 import org.djunits.quantity.SIQuantity;
8 import org.djunits.quantity.def.Quantity;
9 import org.djunits.unit.Unit;
10 import org.djunits.unit.si.SIUnit;
11 import org.djunits.vecmat.def.Table;
12 import org.djunits.vecmat.dn.VectorN;
13 import org.djunits.vecmat.storage.DataGridSi;
14 import org.djunits.vecmat.storage.DenseDoubleDataSi;
15 import org.djutils.exceptions.Throw;
16
17 /**
18 * QuantityTable is a two-dimensonal table with quantities. The QuantityTable allows for Hadamard (element-wise) operations, but
19 * not for vector/matrix operations. A QuantityTable can be transformed to a MatrixNxM or vice versa.
20 * <p>
21 * Copyright (c) 2025-2026 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
22 * for project information <a href="https://djunits.org" target="_blank">https://djunits.org</a>. The DJUNITS project is
23 * distributed under a <a href="https://djunits.org/docs/license.html" target="_blank">three-clause BSD-style license</a>.
24 * @author Alexander Verbraeck
25 * @param <Q> the quantity type
26 */
27 public class QuantityTable<Q extends Quantity<Q>>
28 extends Table<Q, QuantityTable<Q>, QuantityTable<SIQuantity>, QuantityTable<?>, QuantityTable<Q>>
29 {
30 /** */
31 private static final long serialVersionUID = 600L;
32
33 /** The data of the table, in SI unit. */
34 private final DataGridSi<?> dataGridSi;
35
36 /**
37 * Create a new NxM QuantityTable with a unit, based on a DataGrid storage object. This constructor assumes dataSi stores SI
38 * values. Note: NO safe copy is made.
39 * @param dataGridSi the data of the matrix, in SI unit.
40 * @param displayUnit the display unit to use
41 */
42 public QuantityTable(final DataGridSi<?> dataGridSi, final Unit<?, Q> displayUnit)
43 {
44 super(displayUnit);
45 Throw.whenNull(dataGridSi, "dataGridSi");
46 this.dataGridSi = dataGridSi;
47 }
48
49 @Override
50 public QuantityTable<Q> instantiateSi(final double[] siNew)
51 {
52 return new QuantityTable<Q>(this.dataGridSi.instantiateNew(siNew), getDisplayUnit());
53 }
54
55 @Override
56 public QuantityTable<SIQuantity> instantiateSi(final double[] siNew, final SIUnit siUnit)
57 {
58 return new QuantityTable<SIQuantity>(this.dataGridSi.instantiateNew(siNew), siUnit);
59 }
60
61 /**
62 * Return the internal datagrid object, so we can retrieve data from it.
63 * @return the internal datagrid object
64 */
65 public DataGridSi<?> getDataGrid()
66 {
67 return this.dataGridSi;
68 }
69
70 @Override
71 public double[] getSiArray()
72 {
73 return this.dataGridSi.getSiArray();
74 }
75
76 @Override
77 public double[] unsafeSiArray()
78 {
79 return this.dataGridSi.unsafeSiArray();
80 }
81
82 @Override
83 public double si(final int row, final int col) throws IndexOutOfBoundsException
84 {
85 checkRow(row);
86 checkCol(col);
87 return this.dataGridSi.get(row, col);
88 }
89
90 @Override
91 public VectorN.Row<Q> getRowVector(final int row)
92 {
93 return VectorN.Row.ofSi(getRowSi(row), getDisplayUnit());
94 }
95
96 @Override
97 public VectorN.Row<Q> mgetRowVector(final int mRow)
98 {
99 return VectorN.Row.ofSi(mgetRowSi(mRow), getDisplayUnit());
100 }
101
102 @Override
103 public VectorN.Col<Q> getColumnVector(final int col)
104 {
105 return VectorN.Col.ofSi(getColumnSi(col), getDisplayUnit());
106 }
107
108 @Override
109 public VectorN.Col<Q> mgetColumnVector(final int mCol)
110 {
111 return VectorN.Col.ofSi(mgetColumnSi(mCol), getDisplayUnit());
112 }
113
114 @Override
115 public double[] getRowSi(final int row)
116 {
117 checkRow(row);
118 return this.dataGridSi.getRowArray(row);
119 }
120
121 @Override
122 public double[] getColumnSi(final int col)
123 {
124 checkCol(col);
125 return this.dataGridSi.getColArray(col);
126 }
127
128 @Override
129 public int rows()
130 {
131 return this.dataGridSi.rows();
132 }
133
134 @Override
135 public int cols()
136 {
137 return this.dataGridSi.cols();
138 }
139
140 @Override
141 public int nonZeroCount()
142 {
143 return this.dataGridSi.nonZeroCount();
144 }
145
146 /**
147 * Return the transposed quantity table. A transposed quantity table has the same unit as the original one.
148 * @return the transposed quantity table
149 */
150 @Override
151 @SuppressWarnings("checkstyle:needbraces")
152 public QuantityTable<Q> transpose()
153 {
154 double[] data = unsafeSiArray();
155 double[] newSi = new double[data.length];
156 int rows = rows();
157 int cols = cols();
158 final Unit<?, Q> displayUnit = getDisplayUnit();
159 final Unit<?, Q> baseUnit = displayUnit.getBaseUnit();
160 for (int r = 0; r < rows; r++)
161 for (int c = 0; c < cols; c++)
162 newSi[c * rows + r] = data[r * cols + c];
163 return new QuantityTable<Q>(this.dataGridSi.instantiateNew(newSi, cols, rows), baseUnit).setDisplayUnit(displayUnit);
164 }
165
166 // --------------------------------------- EQUALS AND HASHCODE ---------------------------------
167
168 @Override
169 public int hashCode()
170 {
171 return Objects.hash(this.dataGridSi);
172 }
173
174 @SuppressWarnings("checkstyle:needbraces")
175 @Override
176 public boolean equals(final Object obj)
177 {
178 if (this == obj)
179 return true;
180 if (obj == null)
181 return false;
182 if (getClass() != obj.getClass())
183 return false;
184 QuantityTable<?> other = (QuantityTable<?>) obj;
185 return Objects.equals(this.dataGridSi, other.dataGridSi);
186 }
187
188 // ------------------------------------------ OF METHODS ------------------------------------------
189
190 /**
191 * Create a new QuantityTable with a unit, based on a row-major array with values in the given unit.
192 * @param dataInUnit the table values {a11, a12, ..., A1M, ..., aN1, aN2, ..., aNM} expressed in the unit
193 * @param rows the number of rows
194 * @param cols the number of columns
195 * @param unit the unit of the data, also used as the display unit
196 * @param <Q> the quantity type
197 * @return a new QuantityTable with a unit
198 * @throws IllegalArgumentException when dataInUnit does not contain a square number of values
199 */
200 public static <Q extends Quantity<Q>> QuantityTable<Q> of(final double[] dataInUnit, final int rows, final int cols,
201 final Unit<?, Q> unit)
202 {
203 return new QuantityTable<Q>(DenseDoubleDataSi.of(dataInUnit, rows, cols, unit), unit);
204 }
205
206 /**
207 * Create a QuantityTable without needing generics, based on a row-major array with SI-values.
208 * @param dataSi the table values {a11, a12, ..., A1M, ..., aN1, aN2, ..., aNM} as an array using SI units
209 * @param rows the number of rows
210 * @param cols the number of columns
211 * @param displayUnit the display unit to use
212 * @return a new QuantityTable with a unit
213 * @param <Q> the quantity type
214 * @throws IllegalArgumentException when dataSi does not contain a square number of values
215 */
216 public static <Q extends Quantity<Q>> QuantityTable<Q> ofSi(final double[] dataSi, final int rows, final int cols,
217 final Unit<?, Q> displayUnit)
218 {
219 return new QuantityTable<Q>(DenseDoubleDataSi.ofSi(dataSi, rows, cols), displayUnit);
220 }
221
222 /**
223 * Create a QuantityTable without needing generics, based on a row-major array of quantities. The unit is taken from the
224 * first quantity in the array.
225 * @param data the table values {a11, a12, ..., A1M, ..., aN1, aN2, ..., aNM} expressed as an array of quantities
226 * @param rows the number of rows
227 * @param cols the number of columns
228 * @return a new QuantityTable with a unit
229 * @param <Q> the quantity type
230 * @throws IllegalArgumentException when data does not contain a square number of quantities
231 */
232 public static <Q extends Quantity<Q>> QuantityTable<Q> of(final Q[] data, final int rows, final int cols)
233 {
234 Throw.whenNull(data, "data");
235 Throw.when(data.length == 0, IllegalArgumentException.class, "data.length = 0");
236 return new QuantityTable<Q>(DenseDoubleDataSi.of(data, rows, cols), data[0].getDisplayUnit());
237 }
238
239 /**
240 * Create a new QuantityTable with a unit, based on a 2-dimensional grid with SI-values.
241 * @param gridSi the table values {{a11, a12, ..., A1M}, ..., {aN1, aN2, ..., aNM}} expressed in the SI or base unit
242 * @param displayUnit the unit of the data, which will also be used as the display unit
243 * @param <Q> the quantity type
244 * @return a new QuantityTable with a unit
245 * @throws IllegalArgumentException when dataInUnit does not contain a square number of values
246 */
247 @SuppressWarnings("checkstyle:needbraces")
248 public static <Q extends Quantity<Q>> QuantityTable<Q> ofSi(final double[][] gridSi, final Unit<?, Q> displayUnit)
249 {
250 return new QuantityTable<>(DenseDoubleDataSi.ofSi(gridSi), displayUnit);
251 }
252
253 /**
254 * Create a new QuantityTable with a unit, based on a 2-dimensional grid with values in the given unit.
255 * @param gridInUnit the table values {{a11, a12, ..., A1M}, ..., {aN1, aN2, ..., aNM}} expressed in the unit
256 * @param unit the unit of the values, also used as the display unit
257 * @param <Q> the quantity type
258 * @return a new QuantityTable with a unit
259 * @throws IllegalArgumentException when dataInUnit does not contain a square number of values
260 */
261 @SuppressWarnings("checkstyle:needbraces")
262 public static <Q extends Quantity<Q>> QuantityTable<Q> of(final double[][] gridInUnit, final Unit<?, Q> unit)
263 {
264 return new QuantityTable<>(DenseDoubleDataSi.of(gridInUnit, unit), unit);
265 }
266
267 /**
268 * Create a QuantityTable without needing generics, based on a 2-dimensional grid of quantities. The unit is taken from the
269 * first quantity in the grid.
270 * @param grid the table values {{a11, a12, ..., A1M}, ..., {aN1, aN2, ..., aNM}} expressed as a 2-dimensional array of
271 * quantities
272 * @return a new QuantityTable with a unit
273 * @param <Q> the quantity type
274 * @throws IllegalArgumentException when dataInUnit does not contain a square number of quantities
275 */
276 public static <Q extends Quantity<Q>> QuantityTable<Q> of(final Q[][] grid)
277 {
278 Throw.whenNull(grid, "grid");
279 Throw.when(grid.length == 0, IllegalArgumentException.class, "grid.length = 0");
280 Throw.whenNull(grid[0], "grid[0] = null");
281 Throw.when(grid[0].length == 0, IllegalArgumentException.class, "grid[0].length = 0");
282 Throw.whenNull(grid[0][0], "grid[0][0] = null");
283 return new QuantityTable<>(DenseDoubleDataSi.of(grid), grid[0][0].getDisplayUnit());
284 }
285
286 // ------------------------------------------------- AS() METHODS -------------------------------------------------
287
288 /**
289 * Return the QuantityTable 'as' a QuantityTable with a known quantity, using a unit to express the result in. Throw a
290 * Runtime exception when the SI units of this vector and the target vector do not match.
291 * @param targetUnit the unit to convert the quantity table to
292 * @return a quantity table typed in the target quantity table class
293 * @throws IllegalArgumentException when the units do not match
294 * @param <TQ> target quantity type
295 */
296 public <TQ extends Quantity<TQ>> QuantityTable<TQ> as(final Unit<?, TQ> targetUnit) throws IllegalArgumentException
297 {
298 Throw.when(!getDisplayUnit().siUnit().equals(targetUnit.siUnit()), IllegalArgumentException.class,
299 "QuantityTable.as(%s) called, but units do not match: %s <> %s", targetUnit,
300 getDisplayUnit().siUnit().getDisplayAbbreviation(), targetUnit.siUnit().getDisplayAbbreviation());
301 return new QuantityTable<TQ>(this.dataGridSi.instantiateNew(unsafeSiArray()), targetUnit);
302 }
303
304 // ----------------------------------------- STRING AND FORMATTING METHODS ----------------------------------------
305
306 /**
307 * Concise description of this quantity table.
308 * @return a String with the quantity table, with the unit attached.
309 */
310 @Override
311 public String format()
312 {
313 return format(TableFormat.defaults());
314 }
315
316 /**
317 * String representation of this quantity table after applying the format.
318 * @param format the format to apply for the quantity table
319 * @return a String representation of this quantity table, formatted according to the given format
320 */
321 public String format(final TableFormat format)
322 {
323 return TableFormatter.format(this, format);
324 }
325
326 /**
327 * String representation of this quantity table, expressed in the specified unit.
328 * @param targetUnit the unit into which the values of the quantity table are converted for display
329 * @return printable string with the quantity table's values expressed in the specified unit
330 */
331 @Override
332 public String format(final Unit<?, Q> targetUnit)
333 {
334 return format(TableFormat.defaults().setDisplayUnit(targetUnit));
335 }
336
337 }