averbraeck / djunits

Delft Java UNIT System for using strongly-typed quantities and units
BSD 3-Clause "New" or "Revised" License
1 stars 0 forks source link

Add constructors for UnitVector #9

Closed averbraeck closed 1 year ago

averbraeck commented 1 year ago

The construction of Vector and Matrix instances with units is cumbersome, and not intuitive. Currently, constructing involves code like:

  double[] doubleValues = new double[] {0.2, 10.0, 5.7, 100.0, 15.0};
  DurationVector dva = DoubleVector.instantiate(doubleValues, DurationUnit.MINUTE, StorageType.DENSE);

More intuitive would be:

  double[] doubleValues = new double[] {0.2, 10.0, 5.7, 100.0, 15.0};
  DurationVector dva = new DurationVector(doubleValues, DurationUnit.MINUTE);

The code could assume that the StorageType is DENSE, since we offer the data as an array. In case we would offer the data as a Map of indices to values, the code could assume that the StorageType is SPARSE. This would create in total 20 constructors for each Vector (and a similar number for each Matrix).

averbraeck commented 1 year ago

One problem that can be immediately foreseen is that because of type erasure of generics, some of the constructors cannot be easily created: List<Double> has the same signature as, e.g., List<Length>. Similarly, SortedMap<Integer, Double> for sparse vectors has the same erasure signature as SortedMap<Integer, Speed>. We have to think of a way to solve this. One way could be to check the first item and assume all other entries are of the same type. Note, however, that a Scalar extends Number, just like Double...

averbraeck commented 1 year ago

Code generation makes it very easy to create these constructors, so that is for sure not a barrier.

WJSchakel commented 1 year ago

Wouldn't instantiation methods using Double need a unit, where those using Length or Speed would not, circumventing type erasure?

LengthVector.instantiate(SortedMap<Integer, Double>, LengthUnit.SI)

LengthVector.instantiate(SortedMap<Integer, Length>)

As Double is a final class, this would be safe. Using SortedMap<Integer, Number> or SortedMap<Integer, ? extends Number> can lead to errors, e.g.:

SortedMap<Integer, Length> map = ...
LengthVector.instantiate(map, LengthUnit.FOOT)` // Length.si from doubleValue() interpreted as foot
averbraeck commented 1 year ago

I am making a reference implementation, where I can see quickly what works for the constructors and what not.

averbraeck commented 1 year ago

Combinations of arguments we could include and that make sense; example for an Area object:

DoubleVectorData, AreaUnit - construct directly from a DoubleVectorData object with a displayUnit

double[], AreaUnit, StorageType - construct from double array with a displayUnit double[], AreaUnit - construct from double array with a displayUnit; assume DENSE b/c of array double[], StorageType - construct from double array with SI unit for display double[] - construct from double array with with SI unit for display; assume DENSE b/c of array

Area[], AreaUnit, StorageType - construct from an Area array with a displayUnit Area[], AreaUnit - construct from an Area array with a displayUnit; assume DENSE b/c of array Area[], StorageType - construct from an Area array with SI unit for display Area[] - construct from an Area array with with SI unit for display; assume DENSE b/c of array

List<Double>, AreaUnit, StorageType - construct from Double list with a displayUnit List<Double>, AreaUnit - construct from Double list with a displayUnit; assume DENSE b/c of array List<Double>, StorageType - construct from Double list with SI unit for display List<Double> - construct from Double list with with SI unit for display; assume DENSE b/c of array Clashes with List<Area>

List<Area>, AreaUnit, StorageType - construct from an Area list with a displayUnit List<Area>, AreaUnit - construct from an Area array with a displayUnit; assume DENSE b/c of array List<Area>, StorageType - construct from an Area array with SI unit for display List<Area> - construct from an Area array with with SI unit for display; assume DENSE b/c of array Clashes with List<Double>

SortedMap<Integer, Double>, int, AreaUnit, StorageType - construct from sparse Double map with a displayUnit SortedMap<Integer, Double>, int, AreaUnit - construct from Double map with a displayUnit; assume SPARSE b/c of map SortedMap<Integer, Double>, int, StorageType - construct from sparse Double map with SI unit for display SortedMap<Integer, Double>, int - construct from sparse Double map with with SI unit for display; assume SPARSE b/c of map Clashes with SortedMap<Integer, Area>

SortedMap<Integer, Area>, int, AreaUnit, StorageType - construct from sparse Area map with a displayUnit SortedMap<Integer, Area>, int, AreaUnit - construct from Area map with a displayUnit; assume SPARSE b/c of map SortedMap<Integer, Area>, int, StorageType - construct from sparse Area map with SI unit for display SortedMap<Integer, Area>, int - construct from sparse Area map with with SI unit for display; assume SPARSE b/c of map Clashes with SortedMap<Integer, Double>

I count 24 constructors that could make sense in total...

averbraeck commented 1 year ago

The clashes can be solved by using a List<? extends Number> and SortedMap<Integer, ? extends Number>., since both Scalar and Double extend Number. The nice thing is that the doubleValue() of a Scalar is the SI value that needs to be stored.

This would reduce the number of constructors per Vector to 16... (which is not a problem because the code is generated).

A disadvantage is that the following snippet of code would probably work:

  List<Length> ll = List.of(new Length(3.0, LengthUnit.METER), 
        new Length(2.0, LengthUnit.CENTIMETER));
  AreaVector av = new AreaVector(ll, AreaUnit.SQUARE_FOOT);

or even:

  List<? extends Scalar> sl = List.of(new Duration(3.0, DurationUnit.SECOND), 
        new Length(2.0, LengthUnit.CENTIMETER));
  AreaVector av = new AreaVector(sl, AreaUnit.SQUARE_FOOT);
averbraeck commented 1 year ago

A second disadvantage (or problem to solve) is that the displayUnit for a List<Number> or Map<Integer, Number> indicates the unit in which the values are provided. In case of a List<Scalar> or Map<Integer, Scalar>, the internal storage is already in SI units, and the displayUnit is literally only meant for the display unit itself. This means that the doubleValue() of the underlying Number has a different meaning, dependent on whether the List/Map contains numbers or scalars...

averbraeck commented 1 year ago

Probably the last problem can be solved by a constructor like:

public AreaVector(final List<? extends Number> data, final AreaUnit displayUnit, final StorageType storageType)
{
  this(data.size() == 0 ? DoubleVectorData.instantiate(new double[] {}, IdentityScale.SCALE, storageType)
      : data.get(0) instanceof Area ? DoubleVectorData.instantiate(data, IdentityScale.SCALE, storageType)
      : DoubleVectorData.instantiate(data, displayUnit.getScale(), storageType),
  displayUnit);
}
averbraeck commented 1 year ago

Constructors for Vector and FloatVector created and tested -- 100% test coverage and looking good!

averbraeck commented 1 year ago

Test Coverage for Vector classes and FloatVector classes shows correct working of the classes and their constructors.