View Javadoc
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 }