Skip to content

Vector forms have no From{Unit} factories, so every vector is constructed in raw SI base units #237

Description

@matt-edmondson

Speed<T> gets FromMeterPerSecond, FromKilometerPerHour, FromMilePerHour, FromFootPerSecond, FromKnot — one factory per entry in the dimension's availableUnits. Velocity3D<T> gets none. The only way to build one is:

Velocity3D<double> v = new() { X = 7660.0, Y = 0.0, Z = 0.0 };  // m/s, and you had better remember that

The same is true of Position3D, Displacement3D, Force3D and the other 34 vector types. The library's premise is that the unit lives in the type instead of in the caller's head, and the vector forms opt out of it. In a domain that works natively in kilometres — orbital mechanics, which is what surfaced this — every construction site carries a hand-written ×1000, which is precisely the class of error the library exists to prevent.

Design

Emit, per vector form, one factory per unit in the dimension's availableUnits, taking one T per component:

/// <summary>Creates a Position3D from components in kilometers.</summary>
public static Position3D<T> FromKilometer(T x, T y, T z) => new()
{
    X = x * Units.ConversionConstants.Values<T>.KilometerToMeters,
    Y = y * Units.ConversionConstants.Values<T>.KilometerToMeters,
    Z = z * Units.ConversionConstants.Values<T>.KilometerToMeters,
};

Reading the same Values<T> holder the scalar factories already use, so a decimal or PreciseNumber vector converts at its own precision rather than through double — the 5.2.0 behaviour, extended to the vectors it skipped.

Naming follows the existing mechanical rule: the unit's singular-lemma name from units.json, verbatim. FromKilometer(x, y) for V2, FromKilometer(x, y, z, w) for V4.

Four things to settle in review

1. No Vector0Guards. Vector components are signed by construction. The non-negativity rule belongs to the V0 form and must not leak into the vectors — a Position3D with a negative X is ordinary.

2. Offset units must be refused, not mishandled. A conversion with a non-zero ToBaseOffset (the temperature scales) is meaningless applied componentwise to a displacement. No dimension with offset units declares a vector form today, so this is defensive — but the generator should emit nothing and report a new SEM0xx if the combination ever appears, rather than emitting something quietly wrong. That matches how SEM004 handles the analogous typo case.

3. The reader is the open design question. Speed<T>.In(unit) returns T. The vector equivalent cannot return the vector type, because the result is no longer in base units and would be a lie in the type system. Options:

public (T X, T Y, T Z) In(IUnit unit);   // proposed
public T XIn(IUnit unit);                // per-component alternative

The tuple reads better at the call site; the per-component form composes better in generic code. Worth deciding before the factories land, since they should ship together.

4. Does this want Create-style raw constructors too? The object-initializer path stays available either way, so probably not — but if the factories become the documented route, the initializer's meaning ("these are base units") should be spelled out in the XML docs rather than assumed.

Scope

QuantitiesGenerator, regenerating 37 vector classes under Semantics.Quantities/Generated/. No new type names, so scripts/Generate-AliasProps.ps1 should produce identical output — run it and confirm rather than assume, because verify-generated will fail the PR otherwise.

Tests: extend StorageConversionTests<T> with the vector factories over double and decimal, exact where the factor terminates and to a relative tolerance where it does not, matching how the scalar factories are covered.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions