Skip to content

[minor] Convert units exactly for every storage type - #226

Merged
matt-edmondson merged 1 commit into
mainfrom
exact-storage-conversions
Sep 13, 2026
Merged

[minor] Convert units exactly for every storage type#226
matt-edmondson merged 1 commit into
mainfrom
exact-storage-conversions

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Summary

Unit conversions and vector lengths are now exact to the precision of the storage type, not just to the precision of double. With decimal storage, every conversion factor is correct to 28 digits and Length() returns √2 as 1.4142135623730950488016887242. double and float behave as before, apart from two factors that moved one step closer to their exact values.

This is also what a non-IEEE storage type such as PreciseNumber needs. With PreciseNumber 2.0 as storage, Length.FromFoot(1).In(Units.Inch) is exactly 12, where double gives 12.000000000000002.

Changes

  • Factors for each storage type. ConversionsGenerator and MagnitudesGenerator emit a nested Values<T> holder, parsed once for each closed generic type, next to the existing const double. Generated From{Unit} factories, IUnit.ToBase<T>, FromBase<T>, and every In(unit) read it. Integer storage, and any type that can't parse the literal, falls back to T.CreateChecked(double).
  • Exact metadata. conversions.json accepts a fraction such as "5/18". A malformed value reports the new SEM009 error. Thirteen factors that were stored already rounded now carry their exact definitions: repeating ratios as fractions, and π-based factors as 150-digit literals. SquareMile, PoundForce, and PoundFoot were already exact and are unchanged.
  • Square roots. Every vector's Length() and Distance() call the internal StorageMath.Sqrt. double, float, Half, and the integer primitives keep their previous expression exactly. Other types refine the double estimate with Newton steps until the value stops changing.
  • Public additions. IUnit.ToBaseFactorAs<T>() and ToBaseOffsetAs<T>() have default implementations, so existing implementers keep compiling. StorageTypes now lists decimal.
  • Docs. CLAUDE.md and docs/physics-generator.md describe the fraction syntax, the per-type factors, and SEM009.

Risk

  • 172 generated files changed. The only removed lines are the intended rewrites: T.CreateChecked(constant) becoming Values<T>, the double square root becoming StorageMath.Sqrt, the 13 metadata constants, and the StorageTypes list.
  • PsiToPascals is a long literal rather than a fraction. As a fraction, float storage would land further from the exact value, because the numerator would have to round to a float before the division.
  • An older mismatch remains unchanged. QuantitiesGenerator ignores conversionFactor when a unit also has a magnitude, and UnitsGenerator applies both. No unit has both today.

Testing

  • 1227 tests pass, 0 fail, and 4 are skipped (the g++ and clang++ compile tests, since no compiler is installed locally). 59 of the tests are new.
  • StorageConversionTests<T> runs the same conversion and relationship checks for double and decimal, including exact decimal results such as a foot being 12 inches and a 3-4-0 vector having length 5. Adding a storage type takes one derived class.
  • UnitFactorConsistencyTests checks that every unit's ToBaseFactorAs<double>() matches its ToBaseFactor property.
  • The existing zero-allocation tests still pass, and there's a new one for In(unit).
  • Generate-AliasProps.ps1 produces no content changes.

🤖 Generated with Claude Code

https://claude.ai/code/session_01K5Bk9UjGdGUtC5C6qK5ZxD

Unit factors, offsets, and metric prefixes reached a storage type through a
double. Decimal storage therefore carried factor errors as large as 2e-13, and a
150-digit factor such as DegreeToRadians was cut to 17 digits by the compiler.
Each generated factor is now parsed once per storage type from its metadata
literal and cached, and IUnit.ToBase, FromBase, the From{Unit} factories, and
every In(unit) use it. Decimal conversions are correct to 28 digits. Double
results are bit-identical for all but two factors, which moved to the nearest
double.

conversions.json accepts an exact fraction such as "5/18" as well as a decimal
literal, and a malformed value reports the new SEM009 error. Thirteen factors
that had been stored already rounded now carry their exact definitions.

Vector Length() and Distance() call StorageMath.Sqrt. Double, float, Half, and
the integer primitives keep the exact expression they had. Other storage types
refine the double estimate with Newton steps, so decimal gets 28 digits.

IUnit gains ToBaseFactorAs<T>() and ToBaseOffsetAs<T>() with default
implementations, so existing implementers don't break. StorageTypes now lists
decimal.

StorageConversionTests runs the same conversion and relationship checks for
double and decimal, and adding a storage type takes one line.

Claude-Session: https://claude.ai/code/session_01K5Bk9UjGdGUtC5C6qK5ZxD
@sonarqubecloud

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit ccf1f80 into main Sep 13, 2026
13 checks passed
@matt-edmondson
matt-edmondson deleted the exact-storage-conversions branch September 13, 2026 08:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant