From b7ed5cbc2aca81874f8f396d68b195a523b2a921 Mon Sep 17 00:00:00 2001 From: Matt Edmondson Date: Mon, 14 Sep 2026 22:22:33 +1000 Subject: [PATCH 1/2] [patch] Fix storage conversion regressions from exact factors Restore integer storage behavior broken in 5.2.0. 5.2.0 converted every conversion factor and metric magnitude for a storage type in one static initializer, so a single value too large for the type (Tera or CurieToBecquerels for int, Yotta for long) made every factory and In(unit) for that type throw TypeInitializationException. Length.FromKilometer(1) threw instead of returning 1000. Each holder value is now a property over a nullable parsed field, and a type that does not parse the literal converts the double at each read, so each value succeeds or throws OverflowException on its own, exactly as the 5.1 factories did. Make StorageMath.Sqrt return the converged root or throw. A value a double cannot hold is scaled by powers of four into [1, 4) for the seed and the root scaled back, and a root that does not settle throws ArithmeticException. A BigInteger of 2^2048 now gives 2^1024 instead of about 2^1792. Recompute every literal built on pi in conversions.json and domains.json from pi itself, correctly rounded to 150 significant digits. The old literals were wrong from the 97th to 104th significant digit. Add PiLiteralTests, which checks each literal to its last digit against pi computed by Machin's formula. Replace AMetricMagnitudeIsExact, which passed through the old double route too, with a test that combines a magnitude with a 17-digit factor and fails through that route. Emit a d suffix on every double constant, so a literal such as 100000000000000000000 compiles, and make SEM009 reject a literal, operand, or quotient beyond the range of double, or a non-zero value that rounds to zero. Treat a TryParse that throws NotSupportedException or ArgumentException for NumberStyles.Float as a type that cannot parse the literal, so it falls back to the double instead of failing. Correct the claim that no double constant changed. PsiToPascals and RevolutionPerMinuteToRadianPerSecond moved to the adjacent double in 5.2.0, and with them IUnit.ToBaseFactor of Psi and RevolutionPerMinute. Document it in CLAUDE.md and docs/physics-generator.md. Multiply both magnitude and conversion factor in QuantitiesGenerator when a unit declares both, as UnitsGenerator already does. Claude-Session: https://claude.ai/code/session_01K5Bk9UjGdGUtC5C6qK5ZxD --- CLAUDE.md | 56 +- .../ConversionConstants.g.cs | 622 +++++++++++++----- .../MetricMagnitudes.g.cs | 105 ++- .../PhysicalConstants.g.cs | 24 +- Semantics.Quantities/StorageLiteral.cs | 100 ++- Semantics.Quantities/StorageMath.cs | 113 +++- .../AnalyzerReleases.Unshipped.md | 2 +- .../Generators/ConversionValue.cs | 81 ++- .../Generators/ConversionsGenerator.cs | 39 +- .../Generators/MagnitudesGenerator.cs | 29 +- .../Generators/QuantitiesGenerator.cs | 11 +- .../Metadata/conversions.json | 20 +- .../Metadata/domains.json | 12 +- .../SemanticsDiagnostics.cs | 15 +- .../Quantities/GeneratorDiagnosticTests.cs | 115 +++- .../IntegerStorageConversionTests.cs | 109 +++ .../Quantities/ParseThrowingNumber.cs | 170 +++++ Semantics.Test/Quantities/PiLiteralTests.cs | 172 +++++ .../Quantities/StorageConversionTests.cs | 14 +- .../Quantities/StorageLiteralTests.cs | 33 + Semantics.Test/Quantities/StorageMathTests.cs | 31 + docs/physics-generator.md | 45 +- 22 files changed, 1576 insertions(+), 342 deletions(-) create mode 100644 Semantics.Test/Quantities/IntegerStorageConversionTests.cs create mode 100644 Semantics.Test/Quantities/ParseThrowingNumber.cs create mode 100644 Semantics.Test/Quantities/PiLiteralTests.cs create mode 100644 Semantics.Test/Quantities/StorageLiteralTests.cs diff --git a/CLAUDE.md b/CLAUDE.md index ba867ab8..753e64a8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -253,19 +253,40 @@ numeric types, which rejects an exponent. Unit conversion factors and metric magnitudes reach a storage type the same way constants do. `ConversionsGenerator` and `MagnitudesGenerator` still emit `double` constants, which back the -public `IUnit.ToBaseFactor` and `ToBaseOffset` properties, and alongside them a `Values` holder -that calls `StorageLiteral.Parse` or `StorageLiteral.Divide` once per closed generic type. The -generated `From{Unit}` factories multiply by `Values`, and each generated unit implements -`IUnit.ToBaseFactorAs()` and `ToBaseOffsetAs()` explicitly from it, which is what -`ToBase`/`FromBase` and every `In(unit)` read. Before this, every factor went through -`T.CreateChecked(double)`, so a `decimal` quantity converted with the 15 significant digits that -conversion keeps. - -- A value in `conversions.json` is a decimal literal or an exact fraction of two, `"5/9"`. Write a - repeating ratio as the fraction, not as its rounded decimal, and anything built on π as a long - literal. SEM009 reports a value that is neither. -- An integer storage type, or one that cannot parse the literal, falls back to the old - `T.CreateChecked(double)`, so `int` quantities convert exactly as they did. +public `IUnit.ToBaseFactor` and `ToBaseOffset` properties, and alongside them a `Values` holder. +Each value in the holder is a property over a private nullable field that `StorageLiteral.Parse` +or `StorageLiteral.Divide` fills once per closed generic type, and the property converts the +`double` constant with `T.CreateChecked` when that field is null. The generated `From{Unit}` factories +multiply by these properties, and each generated unit implements `IUnit.ToBaseFactorAs()` and +`ToBaseOffsetAs()` explicitly from them, which is what `ToBase`/`FromBase` and every `In(unit)` +read. Before 5.2.0 every factor went through `T.CreateChecked(double)`, so a `decimal` quantity +converted with the 15 significant digits that conversion keeps. + +- A value in `conversions.json` is a decimal literal or an exact fraction of two, `"5/9"`, that a + `double` can hold. Write a repeating ratio as the fraction, not as its rounded decimal. SEM009 + reports a value that is neither form, is beyond the range of `double`, or is non-zero and rounds to + zero in it. +- Write anything built on π as a long literal computed from π itself and correctly rounded to 150 + significant digits, never derived from another literal. `DegreeToRadians` once carried an error from + its 98th digit into every factor taken from it. `PiLiteralTests` checks each literal built on π in + `conversions.json` and `domains.json` against π computed there by Machin's formula. +- An integer storage type, or one that cannot parse the literal, gets `null` from `StorageLiteral` and + converts the `double` at each read, so integer quantities convert exactly as they did before 5.2.0. + A factor too large for the type (`CurieToBecquerels` for `int`, `Yotta` for `long`) throws + `OverflowException` from the factory that uses it, and every other factory keeps working. 5.2.0 + converted every value for a type in one static initializer, so that single overflow made every + conversion for `int` throw `TypeInitializationException`. Keep conversions out of the initializer. + `IntegerStorageConversionTests` pins the behavior against the 5.1 expressions. +- A parse that throws `NotSupportedException` or `ArgumentException` for `NumberStyles.Float`, as a + numeric type outside the base library may, counts as one that cannot parse the literal. Nothing else + is caught. +- Writing factors as their exact definitions in 5.2.0 moved two `double` constants to the adjacent + representable value, each closer to the true value than before: `PsiToPascals` from + 6894.757293168361 to 6894.757293168362, and `RevolutionPerMinuteToRadianPerSecond` from + 0.10471975511965977 to 0.10471975511965978. The public `IUnit.ToBaseFactor` of `Psi` and + `RevolutionPerMinute` moved with them. Every other constant kept its value. Each constant is written + with a `d` suffix, because a literal such as `100000000000000000000` is otherwise an integer literal + the compiler rejects. - `ToBaseFactorAs()` and `ToBaseOffsetAs()` are default-implemented on `IUnit`, so a unit written outside the library needs nothing new. They are not named `Get…`, because CA1721 rejects a `GetToBaseFactor` method next to the `ToBaseFactor` property. @@ -273,8 +294,11 @@ conversion keeps. Vector `Length()` and `Distance()` call `StorageMath.Sqrt`. The binary floating point and integer primitives take the `Math.Sqrt` round trip the generated code always inlined, so their results are unchanged. Any other type is seeded from that root and refined with Newton steps in its own -arithmetic, so a `decimal` length has 28 significant digits. The logarithmic scales and the -hand-written audio types still compute through `double`. +arithmetic, so a `decimal` length has 28 significant digits. A value a `double` cannot hold, such as a +`BigInteger` of 2^2048, is scaled by powers of four into [1, 4) for the seed, and the root is scaled +back by the matching power of two. A root that does not settle throws `ArithmeticException` rather +than returning an estimate. The logarithmic scales and the hand-written audio types still compute +through `double`. `StorageConversionTests` runs the same conversions, relationships and vector lengths over `double` and `decimal`, exactly where the answer terminates and to a relative tolerance where it @@ -368,7 +392,7 @@ var converted = sourceString.As(); - **SEM006** — a metadata file a generator declared in `MetadataFileNames` was not supplied as an `AdditionalFile`. Previously this produced no output and no explanation, which is indistinguishable from a generator that simply had nothing to emit. - **SEM007** — a metadata file could not be parsed. Replaces the base generator's `CONV001` in category `SourceGenerator`, and covers the path that used to swallow the exception, where a malformed `units.json` silently produced factories with no scale factor. - **SEM008** — a relationship's declared result does not follow from the dimensions of its operands, or its value is signed and the declared result is a magnitude. The check comes from `Semantics.Vocabulary`, shared with the C++ projection; before that this side checked the names (SEM001) and the forms (SEM003) and then emitted the operator, so `Sensitivity * Pressure -> ElectricPotential` shipped as a working C# operator computing the wrong physics — which is what found that bug, and it is now fixed. **No operator is generated** for a refused relationship, in any of the directions C# spells a product in — that followed from making the vocabulary drive emission rather than only check it, and the removal is documented in `docs/migration-guide-5.0.md`. Suppressed in `Semantics.Quantities.csproj` because ktsu.Sdk builds warnings as errors and the four below are outstanding; `UnkeepableRelationshipTests` pins the set, and asserts that none of them is in the compiled surface, so a fifth fails there rather than disappearing into the suppression. - - **SEM009**: a factor's `value` in `conversions.json` is neither a decimal literal nor a fraction of two with a non-zero denominator. An error, and no constant is generated for it, because every unit using the factor would otherwise fail to compile far from the metadata line that caused it. + - **SEM009**: a factor's `value` in `conversions.json` is neither a decimal literal nor a fraction of two with a non-zero denominator, or a `double` cannot hold it (a literal, operand, or quotient beyond its range, or a non-zero value that rounds to zero). An error, and no constant is generated for it, because every unit using the factor would otherwise fail to compile far from the metadata line that caused it, or convert with a wrong factor. - Descriptors are allocated from `SemanticsDiagnostics`, which is the one place to add a new one. `AnalyzerReleaseTrackingTests` fails if the identifier is missing from `AnalyzerReleases.Unshipped.md`, so RS2008 no longer surfaces only after a push. - See `docs/physics-generator.md` for the full schema and an end-to-end "add a dimension" walk-through. diff --git a/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.ConversionsGenerator/ConversionConstants.g.cs b/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.ConversionsGenerator/ConversionConstants.g.cs index c65064f4..81fddb42 100644 --- a/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.ConversionsGenerator/ConversionConstants.g.cs +++ b/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.ConversionsGenerator/ConversionConstants.g.cs @@ -12,88 +12,88 @@ namespace ktsu.Semantics.Quantities.Units; internal static class ConversionConstants { /// Foot to meter conversion: 0.3048 m/ft (exact by definition) - internal const double FeetToMeters = 0.3048; + internal const double FeetToMeters = 0.3048d; /// Inch to meter conversion: 0.0254 m/in (exact by definition) - internal const double InchesToMeters = 0.0254; + internal const double InchesToMeters = 0.0254d; /// Yard to meter conversion: 0.9144 m/yd (exact by definition) - internal const double YardToMeters = 0.9144; + internal const double YardToMeters = 0.9144d; /// Mile to meter conversion: 1609.344 m/mi (exact by definition) - internal const double MileToMeters = 1609.344; + internal const double MileToMeters = 1609.344d; /// Angstrom to meter conversion: 1e-10 m/Å (exact by definition) - internal const double AngstromToMeters = 1e-10; + internal const double AngstromToMeters = 1e-10d; /// Nautical mile to meter conversion: 1852 m/nmi (exact by definition) - internal const double NauticalMileToMeters = 1852; + internal const double NauticalMileToMeters = 1852d; /// Pound mass to kilogram: 0.453592 kg/lb (exact) - internal const double PoundMassToKilogram = 0.453592; + internal const double PoundMassToKilogram = 0.453592d; /// Pound to kilogram conversion: 0.45359237 kg/lb (exact by definition) - internal const double PoundToKilograms = 0.45359237; + internal const double PoundToKilograms = 0.45359237d; /// Ounce to kilogram conversion: 0.028349523125 kg/oz (exact) - internal const double OunceToKilograms = 0.028349523125; + internal const double OunceToKilograms = 0.028349523125d; /// Metric ton to kilogram conversion: 1000 kg/t (exact by definition) - internal const double TonToKilograms = 1000; + internal const double TonToKilograms = 1000d; /// Stone to kilogram conversion: 6.35029318 kg/st (14 lb, exact) - internal const double StoneToKilograms = 6.35029318; + internal const double StoneToKilograms = 6.35029318d; /// Short ton to kilogram conversion: 907.18474 kg/ton (2000 lb, exact) - internal const double ShortTonToKilograms = 907.18474; + internal const double ShortTonToKilograms = 907.18474d; /// Atomic mass unit to kilogram: 1.66053906660e-27 kg/u (2018 CODATA) - internal const double AtomicMassUnitToKilograms = 1.66053906660e-27; + internal const double AtomicMassUnitToKilograms = 1.66053906660e-27d; /// Liter to cubic meter conversion: 0.001 m³/L (exact by definition) - internal const double LiterToCubicMeters = 0.001; + internal const double LiterToCubicMeters = 0.001d; /// US gallon to cubic meter conversion: 0.003785411784 m³/gal (exact) - internal const double GallonToCubicMeters = 0.003785411784; + internal const double GallonToCubicMeters = 0.003785411784d; /// Cubic centimeter to cubic meter: 1e-6 m³/cm³ (exact by definition) - internal const double CubicCentimeterToCubicMeters = 1e-6; + internal const double CubicCentimeterToCubicMeters = 1e-6d; /// Cubic foot to cubic meter: 0.028316846592 m³/ft³ (exact) - internal const double CubicFootToCubicMeters = 0.028316846592; + internal const double CubicFootToCubicMeters = 0.028316846592d; /// Cubic inch to cubic meter: 1.6387064e-5 m³/in³ (exact) - internal const double CubicInchToCubicMeters = 1.6387064e-5; + internal const double CubicInchToCubicMeters = 1.6387064e-5d; /// Imperial gallon to cubic meter: 0.00454609 m³/imp gal (exact by definition) - internal const double ImperialGallonToCubicMeters = 0.00454609; + internal const double ImperialGallonToCubicMeters = 0.00454609d; /// US liquid quart to cubic meter: 0.000946352946 m³/qt (exact) - internal const double USQuartToCubicMeters = 0.000946352946; + internal const double USQuartToCubicMeters = 0.000946352946d; /// US liquid pint to cubic meter: 0.000473176473 m³/pt (exact) - internal const double USPintToCubicMeters = 0.000473176473; + internal const double USPintToCubicMeters = 0.000473176473d; /// US fluid ounce to cubic meter: 2.95735295625e-5 m³/fl oz (exact) - internal const double USFluidOunceToCubicMeters = 2.95735295625e-5; + internal const double USFluidOunceToCubicMeters = 2.95735295625e-5d; /// Minute to second conversion: 60 s/min (exact) - internal const double MinuteToSeconds = 60; + internal const double MinuteToSeconds = 60d; /// Hour to second conversion: 3600 s/h (exact) - internal const double HourToSeconds = 3600; + internal const double HourToSeconds = 3600d; /// Day to second conversion: 86400 s/day (exact) - internal const double DayToSeconds = 86400; + internal const double DayToSeconds = 86400d; /// Year to second conversion: 31557600 s/year (365.25 days, exact) - internal const double YearToSeconds = 31557600; + internal const double YearToSeconds = 31557600d; /// Week to second conversion: 604800 s/wk (exact) - internal const double WeekToSeconds = 604800; + internal const double WeekToSeconds = 604800d; /// Celsius to Kelvin temperature offset: 273.15 K (exact by definition) - internal const double CelsiusToKelvinOffset = 273.15; + internal const double CelsiusToKelvinOffset = 273.15d; /// Fahrenheit-to-Kelvin degree scale factor: 5/9 K/°F (exact, stored as a fraction) internal const double FahrenheitScale = 5d / 9d; @@ -101,179 +101,179 @@ internal static class ConversionConstants /// Fahrenheit to Kelvin affine offset: 459.67 × 5/9 = 45967/180 ≈ 255.372 K (exact, stored as a fraction) internal const double FahrenheitToKelvinOffset = 45967d / 180d; - /// Degree to radian conversion: π/180 rad/° (exact) - internal const double DegreeToRadians = 0.017453292519943295769236907684886127134428718885417254560971914401710091146034494436822415696345097379101040706699150667990539631694451077627806983; + /// Degree to radian conversion: π/180 rad/°, correctly rounded to 150 significant digits + internal const double DegreeToRadians = 0.0174532925199432957692369076848861271344287188854172545609719144017100911460344944368224156963450948221230449250737905924838546922752810123984742189340d; - /// Gradian to radian conversion: π/200 rad/grad (π to 150 significant digits, taken as DegreeToRadians × 180) - internal const double GradianToRadians = 0.0157079632679489661923132169163975144209858469968755291048747229615390820314310449931401741267105876411909366360292356011914856685250059698650262847; + /// Gradian to radian conversion: π/200 rad/grad, correctly rounded to 150 significant digits + internal const double GradianToRadians = 0.0157079632679489661923132169163975144209858469968755291048747229615390820314310449931401741267105853399107404325664115332354692230477529111586267970406d; - /// Revolution to radian conversion: 2π rad/rev (π to 150 significant digits, taken as DegreeToRadians × 180) - internal const double RevolutionToRadians = 6.28318530717958647692528676655900576839433879875021164194988918461563281257241799725606965068423505647637465441169424047659426741000238794601051388; + /// Revolution to radian conversion: 2π rad/rev, correctly rounded to 150 significant digits + internal const double RevolutionToRadians = 6.28318530717958647692528676655900576839433879875021164194988918461563281257241799725606965068423413596429617302656461329418768921910116446345071881626d; /// Calorie to joule conversion: 4.184 J/cal (exact, thermochemical calorie) - internal const double CalorieToJoules = 4.184; + internal const double CalorieToJoules = 4.184d; /// Kilowatt-hour to joule conversion: 3600000 J/kWh (exact) - internal const double KilowattHourToJoules = 3600000; + internal const double KilowattHourToJoules = 3600000d; /// Mechanical horsepower to watt conversion: 550 ft⋅lbf/s = 550 × 0.3048 × 4.4482216152605 = 745.69987158227022 W/hp (exact) - internal const double HorsepowerToWatts = 745.69987158227022; + internal const double HorsepowerToWatts = 745.69987158227022d; /// Electron volt to joule conversion: 1.602176634e-19 J/eV (exact, based on elementary charge) - internal const double ElectronVoltToJoules = 1.602176634e-19; + internal const double ElectronVoltToJoules = 1.602176634e-19d; /// Kilocalorie to joule conversion: 4184 J/kcal (exact, thermochemical) - internal const double KilocalorieToJoules = 4184; + internal const double KilocalorieToJoules = 4184d; /// Watt-hour to joule conversion: 3600 J/Wh (exact) - internal const double WattHourToJoules = 3600; + internal const double WattHourToJoules = 3600d; /// Erg to joule conversion: 1e-7 J/erg (exact by definition) - internal const double ErgToJoules = 1e-7; + internal const double ErgToJoules = 1e-7d; /// British thermal unit (IT) to joule conversion: 1055.05585262 J/BTU (exact) - internal const double BtuToJoules = 1055.05585262; + internal const double BtuToJoules = 1055.05585262d; /// Bar to pascal conversion: 100000 Pa/bar (exact by definition) - internal const double BarToPascals = 100000; + internal const double BarToPascals = 100000d; /// Atmosphere to pascal conversion: 101325 Pa/atm (exact by definition) - internal const double AtmosphereToPascals = 101325; + internal const double AtmosphereToPascals = 101325d; /// PSI to pascal conversion: 4.4482216152605 N / 0.0254² m² = 8896443230521/1290320000 Pa/psi, written to 150 significant digits (a literal rather than the fraction, because float storage rounds that numerator before dividing) - internal const double PsiToPascals = 6894.75729316836133672267344534689069378138756277512555025110050220100440200880401760803521607043214086428172856345712691425382850765701531403062806126; + internal const double PsiToPascals = 6894.75729316836133672267344534689069378138756277512555025110050220100440200880401760803521607043214086428172856345712691425382850765701531403062806126d; /// Torr to pascal conversion: 101325/760 = 20265/152 Pa/Torr (exact, stored as a fraction) internal const double TorrToPascals = 20265d / 152d; /// Square foot to square meter conversion: 0.09290304 m²/ft² (exact) - internal const double SquareFootToSquareMeters = 0.09290304; + internal const double SquareFootToSquareMeters = 0.09290304d; /// Square inch to square meter conversion: 0.00064516 m²/in² (exact) - internal const double SquareInchToSquareMeters = 0.00064516; + internal const double SquareInchToSquareMeters = 0.00064516d; /// Barn to square meter conversion: 1e-28 m² (exact by definition) - internal const double BarnToSquareMeters = 1e-28; + internal const double BarnToSquareMeters = 1e-28d; /// Square kilometer to square meter: 1e6 m²/km² (exact by definition) - internal const double SquareKilometerToSquareMeters = 1e6; + internal const double SquareKilometerToSquareMeters = 1e6d; /// Square centimeter to square meter: 1e-4 m²/cm² (exact by definition) - internal const double SquareCentimeterToSquareMeters = 1e-4; + internal const double SquareCentimeterToSquareMeters = 1e-4d; /// Square mile to square meter: 2589988.110336 m²/mi² (exact) - internal const double SquareMileToSquareMeters = 2589988.110336; + internal const double SquareMileToSquareMeters = 2589988.110336d; /// Hectare to square meter: 10000 m²/ha (exact by definition) - internal const double HectareToSquareMeters = 10000; + internal const double HectareToSquareMeters = 10000d; /// Acre to square meter: 4046.8564224 m²/ac (exact) - internal const double AcreToSquareMeters = 4046.8564224; + internal const double AcreToSquareMeters = 4046.8564224d; /// Kilometers per hour to meters per second conversion: 1000/3600 = 5/18 m/s per km/h (exact, stored as a fraction) internal const double KilometerPerHourToMeterPerSecond = 5d / 18d; /// Miles per hour to meters per second conversion: 0.44704 m/s per mph (exact) - internal const double MilePerHourToMeterPerSecond = 0.44704; + internal const double MilePerHourToMeterPerSecond = 0.44704d; /// Feet per second to meters per second: 0.3048 m/s per ft/s (exact) - internal const double FootPerSecondToMeterPerSecond = 0.3048; + internal const double FootPerSecondToMeterPerSecond = 0.3048d; /// Knot to meters per second: 1852/3600 = 463/900 m/s per kn (exact, stored as a fraction) internal const double KnotToMeterPerSecond = 463d / 900d; - /// RPM to rad/s conversion: 2π/60 = π/30 rad/s per rpm (π to 150 significant digits, taken as DegreeToRadians × 180) - internal const double RevolutionPerMinuteToRadianPerSecond = 0.104719755119659774615421446109316762806572313312503527365831486410260546876206966620934494178070584274606244240194904007943237790166706465766841898; + /// RPM to rad/s conversion: 2π/60 = π/30 rad/s per rpm, correctly rounded to 150 significant digits + internal const double RevolutionPerMinuteToRadianPerSecond = 0.104719755119659774615421446109316762806572313312503527365831486410260546876206966620934494178070568932738269550442743554903128153651686074390845313604d; /// Pound-foot to Newton-meter conversion: 1.3558179483314004 N⋅m per lb⋅ft (exact) - internal const double PoundFootToNewtonMeters = 1.3558179483314004; + internal const double PoundFootToNewtonMeters = 1.3558179483314004d; /// Molar to cubic meter concentration conversion: 1000.0 mol/m³ per mol/L (exact) - internal const double MolarToCubicMeter = 1000.0; + internal const double MolarToCubicMeter = 1000.0d; /// Millimolar to mole per cubic meter: 1 mol/m³ per mM (exact) - internal const double MillimolarToMolePerCubicMeter = 1.0; + internal const double MillimolarToMolePerCubicMeter = 1.0d; /// Micromolar to mole per cubic meter: 0.001 mol/m³ per μM (exact) - internal const double MicromolarToMolePerCubicMeter = 0.001; + internal const double MicromolarToMolePerCubicMeter = 0.001d; /// Stokes to square meter per second: 1e-4 m²/s per St (exact by definition) - internal const double StokesToSquareMeterPerSecond = 1e-4; + internal const double StokesToSquareMeterPerSecond = 1e-4d; /// Poise to pascal second: 0.1 Pa·s per P (exact by definition) - internal const double PoiseToPascalSecond = 0.1; + internal const double PoiseToPascalSecond = 0.1d; /// Liter per second to cubic meter per second: 0.001 m³/s per L/s (exact by definition) - internal const double LiterPerSecondToCubicMeterPerSecond = 0.001; + internal const double LiterPerSecondToCubicMeterPerSecond = 0.001d; /// Centipoise to pascal second: 0.001 Pa·s per cP (exact by definition) - internal const double CentipoiseToPascalSecond = 0.001; + internal const double CentipoiseToPascalSecond = 0.001d; /// Dyne per centimeter to newton per meter: 0.001 N/m per dyn/cm (exact) - internal const double DynePerCentimeterToNewtonPerMeter = 0.001; + internal const double DynePerCentimeterToNewtonPerMeter = 0.001d; /// Gram per cubic centimeter to kilogram per cubic meter: 1000 kg/m³ per g/cm³ (exact) - internal const double GramPerCubicCentimeterToKilogramPerCubicMeter = 1000; + internal const double GramPerCubicCentimeterToKilogramPerCubicMeter = 1000d; /// Gram per liter to kilogram per cubic meter: 1 kg/m³ per g/L (exact) - internal const double GramPerLiterToKilogramPerCubicMeter = 1.0; + internal const double GramPerLiterToKilogramPerCubicMeter = 1.0d; /// Gauss to Tesla: 1e-4 T per G (exact by definition) - internal const double GaussToTesla = 1e-4; + internal const double GaussToTesla = 1e-4d; /// Ampere-hour to coulomb conversion: 3600 C/Ah (exact) - internal const double AmpereHourToCoulombs = 3600; + internal const double AmpereHourToCoulombs = 3600d; /// Gram per mole to kilogram per mole: 0.001 kg/mol per g/mol (exact by definition) - internal const double GramPerMoleToKilogramPerMole = 0.001; + internal const double GramPerMoleToKilogramPerMole = 0.001d; /// Kilojoule per mole to joule per mole: 1000 J/mol per kJ/mol (exact by definition) - internal const double KilojoulePerMoleToJoulePerMole = 1000; + internal const double KilojoulePerMoleToJoulePerMole = 1000d; /// Calorie per mole to joule per mole: 4.184 J/mol per cal/mol (exact, thermochemical) - internal const double CaloriePerMoleToJoulePerMole = 4.184; + internal const double CaloriePerMoleToJoulePerMole = 4.184d; /// Enzyme unit (1 μmol/min) to katal: 1e-6/60 = 1/60000000 kat/U (exact, stored as a fraction) internal const double EnzymeUnitToKatals = 1d / 60000000d; /// Standard gravity to meters per second squared: 9.80665 m/s² per g (exact by definition) - internal const double StandardGravityToMeterPerSecondSquared = 9.80665; + internal const double StandardGravityToMeterPerSecondSquared = 9.80665d; /// Dyne to newton conversion: 1e-5 N/dyn (exact by definition) - internal const double DyneToNewtons = 1e-5; + internal const double DyneToNewtons = 1e-5d; /// Pound-force to newton conversion: 4.4482216152605 N/lbf (exact) - internal const double PoundForceToNewtons = 4.4482216152605; + internal const double PoundForceToNewtons = 4.4482216152605d; /// Curie to becquerel conversion: 3.7e10 Bq/Ci (exact by definition) - internal const double CurieToBecquerels = 3.7e10; + internal const double CurieToBecquerels = 3.7e10d; /// Rad to gray conversion: 0.01 Gy/rad (exact by definition) - internal const double RadToGrays = 0.01; + internal const double RadToGrays = 0.01d; /// Rem to sievert conversion: 0.01 Sv/rem (exact by definition) - internal const double RemToSieverts = 0.01; + internal const double RemToSieverts = 0.01d; /// Roentgen to coulomb per kilogram: 2.58e-4 C/kg per R (exact by definition) - internal const double RoentgenToCoulombsPerKilogram = 2.58e-4; + internal const double RoentgenToCoulombsPerKilogram = 2.58e-4d; /// Foot-candle to lux conversion: 1 lm/ft² = 1/0.09290304 = 100000000/9290304 lx/fc (exact, stored as a fraction) internal const double FootCandleToLux = 100000000d / 9290304d; - /// Foot-lambert to candela per square meter: 1/(π × 0.09290304) cd/m² per fL (π to 150 significant digits, taken as DegreeToRadians × 180) - internal const double FootLambertToCandelaPerSquareMeter = 3.42625909963539052691674596165021859423458362052428959800814578422615229026362399099348746319287426200971476339509417994666710818534309005070458689906; + /// Foot-lambert to candela per square meter: 1/(π × 0.09290304) cd/m² per fL, correctly rounded to 150 significant digits + internal const double FootLambertToCandelaPerSquareMeter = 3.42625909963539052691674596165021859423458362052428959800814578422615229026362399099348746319287476397054052755987227192765515199749284098513821314634d; /// Percent to ratio: 0.01 (exact by definition) - internal const double PercentToRatio = 0.01; + internal const double PercentToRatio = 0.01d; /// Parts per million to ratio: 1e-6 (exact by definition) - internal const double PartPerMillionToRatio = 1e-6; + internal const double PartPerMillionToRatio = 1e-6d; /// Parts per billion to ratio: 1e-9 (exact by definition) - internal const double PartPerBillionToRatio = 1e-9; + internal const double PartPerBillionToRatio = 1e-9d; /// Percent by weight to mass-fraction ratio: 0.01 (exact by definition) - internal const double PercentByWeightToRatio = 0.01; + internal const double PercentByWeightToRatio = 0.01d; /// /// Caches each conversion constant materialised into at that type's own precision. @@ -282,268 +282,532 @@ internal static class Values where T : struct, INumber { /// Foot to meter conversion: 0.3048 m/ft (exact by definition) - internal static readonly T FeetToMeters = StorageLiteral.Parse("0.3048", ConversionConstants.FeetToMeters); + internal static T FeetToMeters => ParsedFeetToMeters ?? T.CreateChecked(ConversionConstants.FeetToMeters); + + /// FeetToMeters parsed into , or when the is converted at each read. + private static readonly T? ParsedFeetToMeters = StorageLiteral.Parse("0.3048"); /// Inch to meter conversion: 0.0254 m/in (exact by definition) - internal static readonly T InchesToMeters = StorageLiteral.Parse("0.0254", ConversionConstants.InchesToMeters); + internal static T InchesToMeters => ParsedInchesToMeters ?? T.CreateChecked(ConversionConstants.InchesToMeters); + + /// InchesToMeters parsed into , or when the is converted at each read. + private static readonly T? ParsedInchesToMeters = StorageLiteral.Parse("0.0254"); /// Yard to meter conversion: 0.9144 m/yd (exact by definition) - internal static readonly T YardToMeters = StorageLiteral.Parse("0.9144", ConversionConstants.YardToMeters); + internal static T YardToMeters => ParsedYardToMeters ?? T.CreateChecked(ConversionConstants.YardToMeters); + + /// YardToMeters parsed into , or when the is converted at each read. + private static readonly T? ParsedYardToMeters = StorageLiteral.Parse("0.9144"); /// Mile to meter conversion: 1609.344 m/mi (exact by definition) - internal static readonly T MileToMeters = StorageLiteral.Parse("1609.344", ConversionConstants.MileToMeters); + internal static T MileToMeters => ParsedMileToMeters ?? T.CreateChecked(ConversionConstants.MileToMeters); + + /// MileToMeters parsed into , or when the is converted at each read. + private static readonly T? ParsedMileToMeters = StorageLiteral.Parse("1609.344"); /// Angstrom to meter conversion: 1e-10 m/Å (exact by definition) - internal static readonly T AngstromToMeters = StorageLiteral.Parse("1e-10", ConversionConstants.AngstromToMeters); + internal static T AngstromToMeters => ParsedAngstromToMeters ?? T.CreateChecked(ConversionConstants.AngstromToMeters); + + /// AngstromToMeters parsed into , or when the is converted at each read. + private static readonly T? ParsedAngstromToMeters = StorageLiteral.Parse("1e-10"); /// Nautical mile to meter conversion: 1852 m/nmi (exact by definition) - internal static readonly T NauticalMileToMeters = StorageLiteral.Parse("1852", ConversionConstants.NauticalMileToMeters); + internal static T NauticalMileToMeters => ParsedNauticalMileToMeters ?? T.CreateChecked(ConversionConstants.NauticalMileToMeters); + + /// NauticalMileToMeters parsed into , or when the is converted at each read. + private static readonly T? ParsedNauticalMileToMeters = StorageLiteral.Parse("1852"); /// Pound mass to kilogram: 0.453592 kg/lb (exact) - internal static readonly T PoundMassToKilogram = StorageLiteral.Parse("0.453592", ConversionConstants.PoundMassToKilogram); + internal static T PoundMassToKilogram => ParsedPoundMassToKilogram ?? T.CreateChecked(ConversionConstants.PoundMassToKilogram); + + /// PoundMassToKilogram parsed into , or when the is converted at each read. + private static readonly T? ParsedPoundMassToKilogram = StorageLiteral.Parse("0.453592"); /// Pound to kilogram conversion: 0.45359237 kg/lb (exact by definition) - internal static readonly T PoundToKilograms = StorageLiteral.Parse("0.45359237", ConversionConstants.PoundToKilograms); + internal static T PoundToKilograms => ParsedPoundToKilograms ?? T.CreateChecked(ConversionConstants.PoundToKilograms); + + /// PoundToKilograms parsed into , or when the is converted at each read. + private static readonly T? ParsedPoundToKilograms = StorageLiteral.Parse("0.45359237"); /// Ounce to kilogram conversion: 0.028349523125 kg/oz (exact) - internal static readonly T OunceToKilograms = StorageLiteral.Parse("0.028349523125", ConversionConstants.OunceToKilograms); + internal static T OunceToKilograms => ParsedOunceToKilograms ?? T.CreateChecked(ConversionConstants.OunceToKilograms); + + /// OunceToKilograms parsed into , or when the is converted at each read. + private static readonly T? ParsedOunceToKilograms = StorageLiteral.Parse("0.028349523125"); /// Metric ton to kilogram conversion: 1000 kg/t (exact by definition) - internal static readonly T TonToKilograms = StorageLiteral.Parse("1000", ConversionConstants.TonToKilograms); + internal static T TonToKilograms => ParsedTonToKilograms ?? T.CreateChecked(ConversionConstants.TonToKilograms); + + /// TonToKilograms parsed into , or when the is converted at each read. + private static readonly T? ParsedTonToKilograms = StorageLiteral.Parse("1000"); /// Stone to kilogram conversion: 6.35029318 kg/st (14 lb, exact) - internal static readonly T StoneToKilograms = StorageLiteral.Parse("6.35029318", ConversionConstants.StoneToKilograms); + internal static T StoneToKilograms => ParsedStoneToKilograms ?? T.CreateChecked(ConversionConstants.StoneToKilograms); + + /// StoneToKilograms parsed into , or when the is converted at each read. + private static readonly T? ParsedStoneToKilograms = StorageLiteral.Parse("6.35029318"); /// Short ton to kilogram conversion: 907.18474 kg/ton (2000 lb, exact) - internal static readonly T ShortTonToKilograms = StorageLiteral.Parse("907.18474", ConversionConstants.ShortTonToKilograms); + internal static T ShortTonToKilograms => ParsedShortTonToKilograms ?? T.CreateChecked(ConversionConstants.ShortTonToKilograms); + + /// ShortTonToKilograms parsed into , or when the is converted at each read. + private static readonly T? ParsedShortTonToKilograms = StorageLiteral.Parse("907.18474"); /// Atomic mass unit to kilogram: 1.66053906660e-27 kg/u (2018 CODATA) - internal static readonly T AtomicMassUnitToKilograms = StorageLiteral.Parse("1.66053906660e-27", ConversionConstants.AtomicMassUnitToKilograms); + internal static T AtomicMassUnitToKilograms => ParsedAtomicMassUnitToKilograms ?? T.CreateChecked(ConversionConstants.AtomicMassUnitToKilograms); + + /// AtomicMassUnitToKilograms parsed into , or when the is converted at each read. + private static readonly T? ParsedAtomicMassUnitToKilograms = StorageLiteral.Parse("1.66053906660e-27"); /// Liter to cubic meter conversion: 0.001 m³/L (exact by definition) - internal static readonly T LiterToCubicMeters = StorageLiteral.Parse("0.001", ConversionConstants.LiterToCubicMeters); + internal static T LiterToCubicMeters => ParsedLiterToCubicMeters ?? T.CreateChecked(ConversionConstants.LiterToCubicMeters); + + /// LiterToCubicMeters parsed into , or when the is converted at each read. + private static readonly T? ParsedLiterToCubicMeters = StorageLiteral.Parse("0.001"); /// US gallon to cubic meter conversion: 0.003785411784 m³/gal (exact) - internal static readonly T GallonToCubicMeters = StorageLiteral.Parse("0.003785411784", ConversionConstants.GallonToCubicMeters); + internal static T GallonToCubicMeters => ParsedGallonToCubicMeters ?? T.CreateChecked(ConversionConstants.GallonToCubicMeters); + + /// GallonToCubicMeters parsed into , or when the is converted at each read. + private static readonly T? ParsedGallonToCubicMeters = StorageLiteral.Parse("0.003785411784"); /// Cubic centimeter to cubic meter: 1e-6 m³/cm³ (exact by definition) - internal static readonly T CubicCentimeterToCubicMeters = StorageLiteral.Parse("1e-6", ConversionConstants.CubicCentimeterToCubicMeters); + internal static T CubicCentimeterToCubicMeters => ParsedCubicCentimeterToCubicMeters ?? T.CreateChecked(ConversionConstants.CubicCentimeterToCubicMeters); + + /// CubicCentimeterToCubicMeters parsed into , or when the is converted at each read. + private static readonly T? ParsedCubicCentimeterToCubicMeters = StorageLiteral.Parse("1e-6"); /// Cubic foot to cubic meter: 0.028316846592 m³/ft³ (exact) - internal static readonly T CubicFootToCubicMeters = StorageLiteral.Parse("0.028316846592", ConversionConstants.CubicFootToCubicMeters); + internal static T CubicFootToCubicMeters => ParsedCubicFootToCubicMeters ?? T.CreateChecked(ConversionConstants.CubicFootToCubicMeters); + + /// CubicFootToCubicMeters parsed into , or when the is converted at each read. + private static readonly T? ParsedCubicFootToCubicMeters = StorageLiteral.Parse("0.028316846592"); /// Cubic inch to cubic meter: 1.6387064e-5 m³/in³ (exact) - internal static readonly T CubicInchToCubicMeters = StorageLiteral.Parse("1.6387064e-5", ConversionConstants.CubicInchToCubicMeters); + internal static T CubicInchToCubicMeters => ParsedCubicInchToCubicMeters ?? T.CreateChecked(ConversionConstants.CubicInchToCubicMeters); + + /// CubicInchToCubicMeters parsed into , or when the is converted at each read. + private static readonly T? ParsedCubicInchToCubicMeters = StorageLiteral.Parse("1.6387064e-5"); /// Imperial gallon to cubic meter: 0.00454609 m³/imp gal (exact by definition) - internal static readonly T ImperialGallonToCubicMeters = StorageLiteral.Parse("0.00454609", ConversionConstants.ImperialGallonToCubicMeters); + internal static T ImperialGallonToCubicMeters => ParsedImperialGallonToCubicMeters ?? T.CreateChecked(ConversionConstants.ImperialGallonToCubicMeters); + + /// ImperialGallonToCubicMeters parsed into , or when the is converted at each read. + private static readonly T? ParsedImperialGallonToCubicMeters = StorageLiteral.Parse("0.00454609"); /// US liquid quart to cubic meter: 0.000946352946 m³/qt (exact) - internal static readonly T USQuartToCubicMeters = StorageLiteral.Parse("0.000946352946", ConversionConstants.USQuartToCubicMeters); + internal static T USQuartToCubicMeters => ParsedUSQuartToCubicMeters ?? T.CreateChecked(ConversionConstants.USQuartToCubicMeters); + + /// USQuartToCubicMeters parsed into , or when the is converted at each read. + private static readonly T? ParsedUSQuartToCubicMeters = StorageLiteral.Parse("0.000946352946"); /// US liquid pint to cubic meter: 0.000473176473 m³/pt (exact) - internal static readonly T USPintToCubicMeters = StorageLiteral.Parse("0.000473176473", ConversionConstants.USPintToCubicMeters); + internal static T USPintToCubicMeters => ParsedUSPintToCubicMeters ?? T.CreateChecked(ConversionConstants.USPintToCubicMeters); + + /// USPintToCubicMeters parsed into , or when the is converted at each read. + private static readonly T? ParsedUSPintToCubicMeters = StorageLiteral.Parse("0.000473176473"); /// US fluid ounce to cubic meter: 2.95735295625e-5 m³/fl oz (exact) - internal static readonly T USFluidOunceToCubicMeters = StorageLiteral.Parse("2.95735295625e-5", ConversionConstants.USFluidOunceToCubicMeters); + internal static T USFluidOunceToCubicMeters => ParsedUSFluidOunceToCubicMeters ?? T.CreateChecked(ConversionConstants.USFluidOunceToCubicMeters); + + /// USFluidOunceToCubicMeters parsed into , or when the is converted at each read. + private static readonly T? ParsedUSFluidOunceToCubicMeters = StorageLiteral.Parse("2.95735295625e-5"); /// Minute to second conversion: 60 s/min (exact) - internal static readonly T MinuteToSeconds = StorageLiteral.Parse("60", ConversionConstants.MinuteToSeconds); + internal static T MinuteToSeconds => ParsedMinuteToSeconds ?? T.CreateChecked(ConversionConstants.MinuteToSeconds); + + /// MinuteToSeconds parsed into , or when the is converted at each read. + private static readonly T? ParsedMinuteToSeconds = StorageLiteral.Parse("60"); /// Hour to second conversion: 3600 s/h (exact) - internal static readonly T HourToSeconds = StorageLiteral.Parse("3600", ConversionConstants.HourToSeconds); + internal static T HourToSeconds => ParsedHourToSeconds ?? T.CreateChecked(ConversionConstants.HourToSeconds); + + /// HourToSeconds parsed into , or when the is converted at each read. + private static readonly T? ParsedHourToSeconds = StorageLiteral.Parse("3600"); /// Day to second conversion: 86400 s/day (exact) - internal static readonly T DayToSeconds = StorageLiteral.Parse("86400", ConversionConstants.DayToSeconds); + internal static T DayToSeconds => ParsedDayToSeconds ?? T.CreateChecked(ConversionConstants.DayToSeconds); + + /// DayToSeconds parsed into , or when the is converted at each read. + private static readonly T? ParsedDayToSeconds = StorageLiteral.Parse("86400"); /// Year to second conversion: 31557600 s/year (365.25 days, exact) - internal static readonly T YearToSeconds = StorageLiteral.Parse("31557600", ConversionConstants.YearToSeconds); + internal static T YearToSeconds => ParsedYearToSeconds ?? T.CreateChecked(ConversionConstants.YearToSeconds); + + /// YearToSeconds parsed into , or when the is converted at each read. + private static readonly T? ParsedYearToSeconds = StorageLiteral.Parse("31557600"); /// Week to second conversion: 604800 s/wk (exact) - internal static readonly T WeekToSeconds = StorageLiteral.Parse("604800", ConversionConstants.WeekToSeconds); + internal static T WeekToSeconds => ParsedWeekToSeconds ?? T.CreateChecked(ConversionConstants.WeekToSeconds); + + /// WeekToSeconds parsed into , or when the is converted at each read. + private static readonly T? ParsedWeekToSeconds = StorageLiteral.Parse("604800"); /// Celsius to Kelvin temperature offset: 273.15 K (exact by definition) - internal static readonly T CelsiusToKelvinOffset = StorageLiteral.Parse("273.15", ConversionConstants.CelsiusToKelvinOffset); + internal static T CelsiusToKelvinOffset => ParsedCelsiusToKelvinOffset ?? T.CreateChecked(ConversionConstants.CelsiusToKelvinOffset); + + /// CelsiusToKelvinOffset parsed into , or when the is converted at each read. + private static readonly T? ParsedCelsiusToKelvinOffset = StorageLiteral.Parse("273.15"); /// Fahrenheit-to-Kelvin degree scale factor: 5/9 K/°F (exact, stored as a fraction) - internal static readonly T FahrenheitScale = StorageLiteral.Divide("5", "9", ConversionConstants.FahrenheitScale); + internal static T FahrenheitScale => ParsedFahrenheitScale ?? T.CreateChecked(ConversionConstants.FahrenheitScale); + + /// FahrenheitScale parsed into , or when the is converted at each read. + private static readonly T? ParsedFahrenheitScale = StorageLiteral.Divide("5", "9"); /// Fahrenheit to Kelvin affine offset: 459.67 × 5/9 = 45967/180 ≈ 255.372 K (exact, stored as a fraction) - internal static readonly T FahrenheitToKelvinOffset = StorageLiteral.Divide("45967", "180", ConversionConstants.FahrenheitToKelvinOffset); + internal static T FahrenheitToKelvinOffset => ParsedFahrenheitToKelvinOffset ?? T.CreateChecked(ConversionConstants.FahrenheitToKelvinOffset); + + /// FahrenheitToKelvinOffset parsed into , or when the is converted at each read. + private static readonly T? ParsedFahrenheitToKelvinOffset = StorageLiteral.Divide("45967", "180"); + + /// Degree to radian conversion: π/180 rad/°, correctly rounded to 150 significant digits + internal static T DegreeToRadians => ParsedDegreeToRadians ?? T.CreateChecked(ConversionConstants.DegreeToRadians); + + /// DegreeToRadians parsed into , or when the is converted at each read. + private static readonly T? ParsedDegreeToRadians = StorageLiteral.Parse("0.0174532925199432957692369076848861271344287188854172545609719144017100911460344944368224156963450948221230449250737905924838546922752810123984742189340"); + + /// Gradian to radian conversion: π/200 rad/grad, correctly rounded to 150 significant digits + internal static T GradianToRadians => ParsedGradianToRadians ?? T.CreateChecked(ConversionConstants.GradianToRadians); - /// Degree to radian conversion: π/180 rad/° (exact) - internal static readonly T DegreeToRadians = StorageLiteral.Parse("0.017453292519943295769236907684886127134428718885417254560971914401710091146034494436822415696345097379101040706699150667990539631694451077627806983", ConversionConstants.DegreeToRadians); + /// GradianToRadians parsed into , or when the is converted at each read. + private static readonly T? ParsedGradianToRadians = StorageLiteral.Parse("0.0157079632679489661923132169163975144209858469968755291048747229615390820314310449931401741267105853399107404325664115332354692230477529111586267970406"); - /// Gradian to radian conversion: π/200 rad/grad (π to 150 significant digits, taken as DegreeToRadians × 180) - internal static readonly T GradianToRadians = StorageLiteral.Parse("0.0157079632679489661923132169163975144209858469968755291048747229615390820314310449931401741267105876411909366360292356011914856685250059698650262847", ConversionConstants.GradianToRadians); + /// Revolution to radian conversion: 2π rad/rev, correctly rounded to 150 significant digits + internal static T RevolutionToRadians => ParsedRevolutionToRadians ?? T.CreateChecked(ConversionConstants.RevolutionToRadians); - /// Revolution to radian conversion: 2π rad/rev (π to 150 significant digits, taken as DegreeToRadians × 180) - internal static readonly T RevolutionToRadians = StorageLiteral.Parse("6.28318530717958647692528676655900576839433879875021164194988918461563281257241799725606965068423505647637465441169424047659426741000238794601051388", ConversionConstants.RevolutionToRadians); + /// RevolutionToRadians parsed into , or when the is converted at each read. + private static readonly T? ParsedRevolutionToRadians = StorageLiteral.Parse("6.28318530717958647692528676655900576839433879875021164194988918461563281257241799725606965068423413596429617302656461329418768921910116446345071881626"); /// Calorie to joule conversion: 4.184 J/cal (exact, thermochemical calorie) - internal static readonly T CalorieToJoules = StorageLiteral.Parse("4.184", ConversionConstants.CalorieToJoules); + internal static T CalorieToJoules => ParsedCalorieToJoules ?? T.CreateChecked(ConversionConstants.CalorieToJoules); + + /// CalorieToJoules parsed into , or when the is converted at each read. + private static readonly T? ParsedCalorieToJoules = StorageLiteral.Parse("4.184"); /// Kilowatt-hour to joule conversion: 3600000 J/kWh (exact) - internal static readonly T KilowattHourToJoules = StorageLiteral.Parse("3600000", ConversionConstants.KilowattHourToJoules); + internal static T KilowattHourToJoules => ParsedKilowattHourToJoules ?? T.CreateChecked(ConversionConstants.KilowattHourToJoules); + + /// KilowattHourToJoules parsed into , or when the is converted at each read. + private static readonly T? ParsedKilowattHourToJoules = StorageLiteral.Parse("3600000"); /// Mechanical horsepower to watt conversion: 550 ft⋅lbf/s = 550 × 0.3048 × 4.4482216152605 = 745.69987158227022 W/hp (exact) - internal static readonly T HorsepowerToWatts = StorageLiteral.Parse("745.69987158227022", ConversionConstants.HorsepowerToWatts); + internal static T HorsepowerToWatts => ParsedHorsepowerToWatts ?? T.CreateChecked(ConversionConstants.HorsepowerToWatts); + + /// HorsepowerToWatts parsed into , or when the is converted at each read. + private static readonly T? ParsedHorsepowerToWatts = StorageLiteral.Parse("745.69987158227022"); /// Electron volt to joule conversion: 1.602176634e-19 J/eV (exact, based on elementary charge) - internal static readonly T ElectronVoltToJoules = StorageLiteral.Parse("1.602176634e-19", ConversionConstants.ElectronVoltToJoules); + internal static T ElectronVoltToJoules => ParsedElectronVoltToJoules ?? T.CreateChecked(ConversionConstants.ElectronVoltToJoules); + + /// ElectronVoltToJoules parsed into , or when the is converted at each read. + private static readonly T? ParsedElectronVoltToJoules = StorageLiteral.Parse("1.602176634e-19"); /// Kilocalorie to joule conversion: 4184 J/kcal (exact, thermochemical) - internal static readonly T KilocalorieToJoules = StorageLiteral.Parse("4184", ConversionConstants.KilocalorieToJoules); + internal static T KilocalorieToJoules => ParsedKilocalorieToJoules ?? T.CreateChecked(ConversionConstants.KilocalorieToJoules); + + /// KilocalorieToJoules parsed into , or when the is converted at each read. + private static readonly T? ParsedKilocalorieToJoules = StorageLiteral.Parse("4184"); /// Watt-hour to joule conversion: 3600 J/Wh (exact) - internal static readonly T WattHourToJoules = StorageLiteral.Parse("3600", ConversionConstants.WattHourToJoules); + internal static T WattHourToJoules => ParsedWattHourToJoules ?? T.CreateChecked(ConversionConstants.WattHourToJoules); + + /// WattHourToJoules parsed into , or when the is converted at each read. + private static readonly T? ParsedWattHourToJoules = StorageLiteral.Parse("3600"); /// Erg to joule conversion: 1e-7 J/erg (exact by definition) - internal static readonly T ErgToJoules = StorageLiteral.Parse("1e-7", ConversionConstants.ErgToJoules); + internal static T ErgToJoules => ParsedErgToJoules ?? T.CreateChecked(ConversionConstants.ErgToJoules); + + /// ErgToJoules parsed into , or when the is converted at each read. + private static readonly T? ParsedErgToJoules = StorageLiteral.Parse("1e-7"); /// British thermal unit (IT) to joule conversion: 1055.05585262 J/BTU (exact) - internal static readonly T BtuToJoules = StorageLiteral.Parse("1055.05585262", ConversionConstants.BtuToJoules); + internal static T BtuToJoules => ParsedBtuToJoules ?? T.CreateChecked(ConversionConstants.BtuToJoules); + + /// BtuToJoules parsed into , or when the is converted at each read. + private static readonly T? ParsedBtuToJoules = StorageLiteral.Parse("1055.05585262"); /// Bar to pascal conversion: 100000 Pa/bar (exact by definition) - internal static readonly T BarToPascals = StorageLiteral.Parse("100000", ConversionConstants.BarToPascals); + internal static T BarToPascals => ParsedBarToPascals ?? T.CreateChecked(ConversionConstants.BarToPascals); + + /// BarToPascals parsed into , or when the is converted at each read. + private static readonly T? ParsedBarToPascals = StorageLiteral.Parse("100000"); /// Atmosphere to pascal conversion: 101325 Pa/atm (exact by definition) - internal static readonly T AtmosphereToPascals = StorageLiteral.Parse("101325", ConversionConstants.AtmosphereToPascals); + internal static T AtmosphereToPascals => ParsedAtmosphereToPascals ?? T.CreateChecked(ConversionConstants.AtmosphereToPascals); + + /// AtmosphereToPascals parsed into , or when the is converted at each read. + private static readonly T? ParsedAtmosphereToPascals = StorageLiteral.Parse("101325"); /// PSI to pascal conversion: 4.4482216152605 N / 0.0254² m² = 8896443230521/1290320000 Pa/psi, written to 150 significant digits (a literal rather than the fraction, because float storage rounds that numerator before dividing) - internal static readonly T PsiToPascals = StorageLiteral.Parse("6894.75729316836133672267344534689069378138756277512555025110050220100440200880401760803521607043214086428172856345712691425382850765701531403062806126", ConversionConstants.PsiToPascals); + internal static T PsiToPascals => ParsedPsiToPascals ?? T.CreateChecked(ConversionConstants.PsiToPascals); + + /// PsiToPascals parsed into , or when the is converted at each read. + private static readonly T? ParsedPsiToPascals = StorageLiteral.Parse("6894.75729316836133672267344534689069378138756277512555025110050220100440200880401760803521607043214086428172856345712691425382850765701531403062806126"); /// Torr to pascal conversion: 101325/760 = 20265/152 Pa/Torr (exact, stored as a fraction) - internal static readonly T TorrToPascals = StorageLiteral.Divide("20265", "152", ConversionConstants.TorrToPascals); + internal static T TorrToPascals => ParsedTorrToPascals ?? T.CreateChecked(ConversionConstants.TorrToPascals); + + /// TorrToPascals parsed into , or when the is converted at each read. + private static readonly T? ParsedTorrToPascals = StorageLiteral.Divide("20265", "152"); /// Square foot to square meter conversion: 0.09290304 m²/ft² (exact) - internal static readonly T SquareFootToSquareMeters = StorageLiteral.Parse("0.09290304", ConversionConstants.SquareFootToSquareMeters); + internal static T SquareFootToSquareMeters => ParsedSquareFootToSquareMeters ?? T.CreateChecked(ConversionConstants.SquareFootToSquareMeters); + + /// SquareFootToSquareMeters parsed into , or when the is converted at each read. + private static readonly T? ParsedSquareFootToSquareMeters = StorageLiteral.Parse("0.09290304"); /// Square inch to square meter conversion: 0.00064516 m²/in² (exact) - internal static readonly T SquareInchToSquareMeters = StorageLiteral.Parse("0.00064516", ConversionConstants.SquareInchToSquareMeters); + internal static T SquareInchToSquareMeters => ParsedSquareInchToSquareMeters ?? T.CreateChecked(ConversionConstants.SquareInchToSquareMeters); + + /// SquareInchToSquareMeters parsed into , or when the is converted at each read. + private static readonly T? ParsedSquareInchToSquareMeters = StorageLiteral.Parse("0.00064516"); /// Barn to square meter conversion: 1e-28 m² (exact by definition) - internal static readonly T BarnToSquareMeters = StorageLiteral.Parse("1e-28", ConversionConstants.BarnToSquareMeters); + internal static T BarnToSquareMeters => ParsedBarnToSquareMeters ?? T.CreateChecked(ConversionConstants.BarnToSquareMeters); + + /// BarnToSquareMeters parsed into , or when the is converted at each read. + private static readonly T? ParsedBarnToSquareMeters = StorageLiteral.Parse("1e-28"); /// Square kilometer to square meter: 1e6 m²/km² (exact by definition) - internal static readonly T SquareKilometerToSquareMeters = StorageLiteral.Parse("1e6", ConversionConstants.SquareKilometerToSquareMeters); + internal static T SquareKilometerToSquareMeters => ParsedSquareKilometerToSquareMeters ?? T.CreateChecked(ConversionConstants.SquareKilometerToSquareMeters); + + /// SquareKilometerToSquareMeters parsed into , or when the is converted at each read. + private static readonly T? ParsedSquareKilometerToSquareMeters = StorageLiteral.Parse("1e6"); /// Square centimeter to square meter: 1e-4 m²/cm² (exact by definition) - internal static readonly T SquareCentimeterToSquareMeters = StorageLiteral.Parse("1e-4", ConversionConstants.SquareCentimeterToSquareMeters); + internal static T SquareCentimeterToSquareMeters => ParsedSquareCentimeterToSquareMeters ?? T.CreateChecked(ConversionConstants.SquareCentimeterToSquareMeters); + + /// SquareCentimeterToSquareMeters parsed into , or when the is converted at each read. + private static readonly T? ParsedSquareCentimeterToSquareMeters = StorageLiteral.Parse("1e-4"); /// Square mile to square meter: 2589988.110336 m²/mi² (exact) - internal static readonly T SquareMileToSquareMeters = StorageLiteral.Parse("2589988.110336", ConversionConstants.SquareMileToSquareMeters); + internal static T SquareMileToSquareMeters => ParsedSquareMileToSquareMeters ?? T.CreateChecked(ConversionConstants.SquareMileToSquareMeters); + + /// SquareMileToSquareMeters parsed into , or when the is converted at each read. + private static readonly T? ParsedSquareMileToSquareMeters = StorageLiteral.Parse("2589988.110336"); /// Hectare to square meter: 10000 m²/ha (exact by definition) - internal static readonly T HectareToSquareMeters = StorageLiteral.Parse("10000", ConversionConstants.HectareToSquareMeters); + internal static T HectareToSquareMeters => ParsedHectareToSquareMeters ?? T.CreateChecked(ConversionConstants.HectareToSquareMeters); + + /// HectareToSquareMeters parsed into , or when the is converted at each read. + private static readonly T? ParsedHectareToSquareMeters = StorageLiteral.Parse("10000"); /// Acre to square meter: 4046.8564224 m²/ac (exact) - internal static readonly T AcreToSquareMeters = StorageLiteral.Parse("4046.8564224", ConversionConstants.AcreToSquareMeters); + internal static T AcreToSquareMeters => ParsedAcreToSquareMeters ?? T.CreateChecked(ConversionConstants.AcreToSquareMeters); + + /// AcreToSquareMeters parsed into , or when the is converted at each read. + private static readonly T? ParsedAcreToSquareMeters = StorageLiteral.Parse("4046.8564224"); /// Kilometers per hour to meters per second conversion: 1000/3600 = 5/18 m/s per km/h (exact, stored as a fraction) - internal static readonly T KilometerPerHourToMeterPerSecond = StorageLiteral.Divide("5", "18", ConversionConstants.KilometerPerHourToMeterPerSecond); + internal static T KilometerPerHourToMeterPerSecond => ParsedKilometerPerHourToMeterPerSecond ?? T.CreateChecked(ConversionConstants.KilometerPerHourToMeterPerSecond); + + /// KilometerPerHourToMeterPerSecond parsed into , or when the is converted at each read. + private static readonly T? ParsedKilometerPerHourToMeterPerSecond = StorageLiteral.Divide("5", "18"); /// Miles per hour to meters per second conversion: 0.44704 m/s per mph (exact) - internal static readonly T MilePerHourToMeterPerSecond = StorageLiteral.Parse("0.44704", ConversionConstants.MilePerHourToMeterPerSecond); + internal static T MilePerHourToMeterPerSecond => ParsedMilePerHourToMeterPerSecond ?? T.CreateChecked(ConversionConstants.MilePerHourToMeterPerSecond); + + /// MilePerHourToMeterPerSecond parsed into , or when the is converted at each read. + private static readonly T? ParsedMilePerHourToMeterPerSecond = StorageLiteral.Parse("0.44704"); /// Feet per second to meters per second: 0.3048 m/s per ft/s (exact) - internal static readonly T FootPerSecondToMeterPerSecond = StorageLiteral.Parse("0.3048", ConversionConstants.FootPerSecondToMeterPerSecond); + internal static T FootPerSecondToMeterPerSecond => ParsedFootPerSecondToMeterPerSecond ?? T.CreateChecked(ConversionConstants.FootPerSecondToMeterPerSecond); + + /// FootPerSecondToMeterPerSecond parsed into , or when the is converted at each read. + private static readonly T? ParsedFootPerSecondToMeterPerSecond = StorageLiteral.Parse("0.3048"); /// Knot to meters per second: 1852/3600 = 463/900 m/s per kn (exact, stored as a fraction) - internal static readonly T KnotToMeterPerSecond = StorageLiteral.Divide("463", "900", ConversionConstants.KnotToMeterPerSecond); + internal static T KnotToMeterPerSecond => ParsedKnotToMeterPerSecond ?? T.CreateChecked(ConversionConstants.KnotToMeterPerSecond); + + /// KnotToMeterPerSecond parsed into , or when the is converted at each read. + private static readonly T? ParsedKnotToMeterPerSecond = StorageLiteral.Divide("463", "900"); + + /// RPM to rad/s conversion: 2π/60 = π/30 rad/s per rpm, correctly rounded to 150 significant digits + internal static T RevolutionPerMinuteToRadianPerSecond => ParsedRevolutionPerMinuteToRadianPerSecond ?? T.CreateChecked(ConversionConstants.RevolutionPerMinuteToRadianPerSecond); - /// RPM to rad/s conversion: 2π/60 = π/30 rad/s per rpm (π to 150 significant digits, taken as DegreeToRadians × 180) - internal static readonly T RevolutionPerMinuteToRadianPerSecond = StorageLiteral.Parse("0.104719755119659774615421446109316762806572313312503527365831486410260546876206966620934494178070584274606244240194904007943237790166706465766841898", ConversionConstants.RevolutionPerMinuteToRadianPerSecond); + /// RevolutionPerMinuteToRadianPerSecond parsed into , or when the is converted at each read. + private static readonly T? ParsedRevolutionPerMinuteToRadianPerSecond = StorageLiteral.Parse("0.104719755119659774615421446109316762806572313312503527365831486410260546876206966620934494178070568932738269550442743554903128153651686074390845313604"); /// Pound-foot to Newton-meter conversion: 1.3558179483314004 N⋅m per lb⋅ft (exact) - internal static readonly T PoundFootToNewtonMeters = StorageLiteral.Parse("1.3558179483314004", ConversionConstants.PoundFootToNewtonMeters); + internal static T PoundFootToNewtonMeters => ParsedPoundFootToNewtonMeters ?? T.CreateChecked(ConversionConstants.PoundFootToNewtonMeters); + + /// PoundFootToNewtonMeters parsed into , or when the is converted at each read. + private static readonly T? ParsedPoundFootToNewtonMeters = StorageLiteral.Parse("1.3558179483314004"); /// Molar to cubic meter concentration conversion: 1000.0 mol/m³ per mol/L (exact) - internal static readonly T MolarToCubicMeter = StorageLiteral.Parse("1000.0", ConversionConstants.MolarToCubicMeter); + internal static T MolarToCubicMeter => ParsedMolarToCubicMeter ?? T.CreateChecked(ConversionConstants.MolarToCubicMeter); + + /// MolarToCubicMeter parsed into , or when the is converted at each read. + private static readonly T? ParsedMolarToCubicMeter = StorageLiteral.Parse("1000.0"); /// Millimolar to mole per cubic meter: 1 mol/m³ per mM (exact) - internal static readonly T MillimolarToMolePerCubicMeter = StorageLiteral.Parse("1.0", ConversionConstants.MillimolarToMolePerCubicMeter); + internal static T MillimolarToMolePerCubicMeter => ParsedMillimolarToMolePerCubicMeter ?? T.CreateChecked(ConversionConstants.MillimolarToMolePerCubicMeter); + + /// MillimolarToMolePerCubicMeter parsed into , or when the is converted at each read. + private static readonly T? ParsedMillimolarToMolePerCubicMeter = StorageLiteral.Parse("1.0"); /// Micromolar to mole per cubic meter: 0.001 mol/m³ per μM (exact) - internal static readonly T MicromolarToMolePerCubicMeter = StorageLiteral.Parse("0.001", ConversionConstants.MicromolarToMolePerCubicMeter); + internal static T MicromolarToMolePerCubicMeter => ParsedMicromolarToMolePerCubicMeter ?? T.CreateChecked(ConversionConstants.MicromolarToMolePerCubicMeter); + + /// MicromolarToMolePerCubicMeter parsed into , or when the is converted at each read. + private static readonly T? ParsedMicromolarToMolePerCubicMeter = StorageLiteral.Parse("0.001"); /// Stokes to square meter per second: 1e-4 m²/s per St (exact by definition) - internal static readonly T StokesToSquareMeterPerSecond = StorageLiteral.Parse("1e-4", ConversionConstants.StokesToSquareMeterPerSecond); + internal static T StokesToSquareMeterPerSecond => ParsedStokesToSquareMeterPerSecond ?? T.CreateChecked(ConversionConstants.StokesToSquareMeterPerSecond); + + /// StokesToSquareMeterPerSecond parsed into , or when the is converted at each read. + private static readonly T? ParsedStokesToSquareMeterPerSecond = StorageLiteral.Parse("1e-4"); /// Poise to pascal second: 0.1 Pa·s per P (exact by definition) - internal static readonly T PoiseToPascalSecond = StorageLiteral.Parse("0.1", ConversionConstants.PoiseToPascalSecond); + internal static T PoiseToPascalSecond => ParsedPoiseToPascalSecond ?? T.CreateChecked(ConversionConstants.PoiseToPascalSecond); + + /// PoiseToPascalSecond parsed into , or when the is converted at each read. + private static readonly T? ParsedPoiseToPascalSecond = StorageLiteral.Parse("0.1"); /// Liter per second to cubic meter per second: 0.001 m³/s per L/s (exact by definition) - internal static readonly T LiterPerSecondToCubicMeterPerSecond = StorageLiteral.Parse("0.001", ConversionConstants.LiterPerSecondToCubicMeterPerSecond); + internal static T LiterPerSecondToCubicMeterPerSecond => ParsedLiterPerSecondToCubicMeterPerSecond ?? T.CreateChecked(ConversionConstants.LiterPerSecondToCubicMeterPerSecond); + + /// LiterPerSecondToCubicMeterPerSecond parsed into , or when the is converted at each read. + private static readonly T? ParsedLiterPerSecondToCubicMeterPerSecond = StorageLiteral.Parse("0.001"); /// Centipoise to pascal second: 0.001 Pa·s per cP (exact by definition) - internal static readonly T CentipoiseToPascalSecond = StorageLiteral.Parse("0.001", ConversionConstants.CentipoiseToPascalSecond); + internal static T CentipoiseToPascalSecond => ParsedCentipoiseToPascalSecond ?? T.CreateChecked(ConversionConstants.CentipoiseToPascalSecond); + + /// CentipoiseToPascalSecond parsed into , or when the is converted at each read. + private static readonly T? ParsedCentipoiseToPascalSecond = StorageLiteral.Parse("0.001"); /// Dyne per centimeter to newton per meter: 0.001 N/m per dyn/cm (exact) - internal static readonly T DynePerCentimeterToNewtonPerMeter = StorageLiteral.Parse("0.001", ConversionConstants.DynePerCentimeterToNewtonPerMeter); + internal static T DynePerCentimeterToNewtonPerMeter => ParsedDynePerCentimeterToNewtonPerMeter ?? T.CreateChecked(ConversionConstants.DynePerCentimeterToNewtonPerMeter); + + /// DynePerCentimeterToNewtonPerMeter parsed into , or when the is converted at each read. + private static readonly T? ParsedDynePerCentimeterToNewtonPerMeter = StorageLiteral.Parse("0.001"); /// Gram per cubic centimeter to kilogram per cubic meter: 1000 kg/m³ per g/cm³ (exact) - internal static readonly T GramPerCubicCentimeterToKilogramPerCubicMeter = StorageLiteral.Parse("1000", ConversionConstants.GramPerCubicCentimeterToKilogramPerCubicMeter); + internal static T GramPerCubicCentimeterToKilogramPerCubicMeter => ParsedGramPerCubicCentimeterToKilogramPerCubicMeter ?? T.CreateChecked(ConversionConstants.GramPerCubicCentimeterToKilogramPerCubicMeter); + + /// GramPerCubicCentimeterToKilogramPerCubicMeter parsed into , or when the is converted at each read. + private static readonly T? ParsedGramPerCubicCentimeterToKilogramPerCubicMeter = StorageLiteral.Parse("1000"); /// Gram per liter to kilogram per cubic meter: 1 kg/m³ per g/L (exact) - internal static readonly T GramPerLiterToKilogramPerCubicMeter = StorageLiteral.Parse("1.0", ConversionConstants.GramPerLiterToKilogramPerCubicMeter); + internal static T GramPerLiterToKilogramPerCubicMeter => ParsedGramPerLiterToKilogramPerCubicMeter ?? T.CreateChecked(ConversionConstants.GramPerLiterToKilogramPerCubicMeter); + + /// GramPerLiterToKilogramPerCubicMeter parsed into , or when the is converted at each read. + private static readonly T? ParsedGramPerLiterToKilogramPerCubicMeter = StorageLiteral.Parse("1.0"); /// Gauss to Tesla: 1e-4 T per G (exact by definition) - internal static readonly T GaussToTesla = StorageLiteral.Parse("1e-4", ConversionConstants.GaussToTesla); + internal static T GaussToTesla => ParsedGaussToTesla ?? T.CreateChecked(ConversionConstants.GaussToTesla); + + /// GaussToTesla parsed into , or when the is converted at each read. + private static readonly T? ParsedGaussToTesla = StorageLiteral.Parse("1e-4"); /// Ampere-hour to coulomb conversion: 3600 C/Ah (exact) - internal static readonly T AmpereHourToCoulombs = StorageLiteral.Parse("3600", ConversionConstants.AmpereHourToCoulombs); + internal static T AmpereHourToCoulombs => ParsedAmpereHourToCoulombs ?? T.CreateChecked(ConversionConstants.AmpereHourToCoulombs); + + /// AmpereHourToCoulombs parsed into , or when the is converted at each read. + private static readonly T? ParsedAmpereHourToCoulombs = StorageLiteral.Parse("3600"); /// Gram per mole to kilogram per mole: 0.001 kg/mol per g/mol (exact by definition) - internal static readonly T GramPerMoleToKilogramPerMole = StorageLiteral.Parse("0.001", ConversionConstants.GramPerMoleToKilogramPerMole); + internal static T GramPerMoleToKilogramPerMole => ParsedGramPerMoleToKilogramPerMole ?? T.CreateChecked(ConversionConstants.GramPerMoleToKilogramPerMole); + + /// GramPerMoleToKilogramPerMole parsed into , or when the is converted at each read. + private static readonly T? ParsedGramPerMoleToKilogramPerMole = StorageLiteral.Parse("0.001"); /// Kilojoule per mole to joule per mole: 1000 J/mol per kJ/mol (exact by definition) - internal static readonly T KilojoulePerMoleToJoulePerMole = StorageLiteral.Parse("1000", ConversionConstants.KilojoulePerMoleToJoulePerMole); + internal static T KilojoulePerMoleToJoulePerMole => ParsedKilojoulePerMoleToJoulePerMole ?? T.CreateChecked(ConversionConstants.KilojoulePerMoleToJoulePerMole); + + /// KilojoulePerMoleToJoulePerMole parsed into , or when the is converted at each read. + private static readonly T? ParsedKilojoulePerMoleToJoulePerMole = StorageLiteral.Parse("1000"); /// Calorie per mole to joule per mole: 4.184 J/mol per cal/mol (exact, thermochemical) - internal static readonly T CaloriePerMoleToJoulePerMole = StorageLiteral.Parse("4.184", ConversionConstants.CaloriePerMoleToJoulePerMole); + internal static T CaloriePerMoleToJoulePerMole => ParsedCaloriePerMoleToJoulePerMole ?? T.CreateChecked(ConversionConstants.CaloriePerMoleToJoulePerMole); + + /// CaloriePerMoleToJoulePerMole parsed into , or when the is converted at each read. + private static readonly T? ParsedCaloriePerMoleToJoulePerMole = StorageLiteral.Parse("4.184"); /// Enzyme unit (1 μmol/min) to katal: 1e-6/60 = 1/60000000 kat/U (exact, stored as a fraction) - internal static readonly T EnzymeUnitToKatals = StorageLiteral.Divide("1", "60000000", ConversionConstants.EnzymeUnitToKatals); + internal static T EnzymeUnitToKatals => ParsedEnzymeUnitToKatals ?? T.CreateChecked(ConversionConstants.EnzymeUnitToKatals); + + /// EnzymeUnitToKatals parsed into , or when the is converted at each read. + private static readonly T? ParsedEnzymeUnitToKatals = StorageLiteral.Divide("1", "60000000"); /// Standard gravity to meters per second squared: 9.80665 m/s² per g (exact by definition) - internal static readonly T StandardGravityToMeterPerSecondSquared = StorageLiteral.Parse("9.80665", ConversionConstants.StandardGravityToMeterPerSecondSquared); + internal static T StandardGravityToMeterPerSecondSquared => ParsedStandardGravityToMeterPerSecondSquared ?? T.CreateChecked(ConversionConstants.StandardGravityToMeterPerSecondSquared); + + /// StandardGravityToMeterPerSecondSquared parsed into , or when the is converted at each read. + private static readonly T? ParsedStandardGravityToMeterPerSecondSquared = StorageLiteral.Parse("9.80665"); /// Dyne to newton conversion: 1e-5 N/dyn (exact by definition) - internal static readonly T DyneToNewtons = StorageLiteral.Parse("1e-5", ConversionConstants.DyneToNewtons); + internal static T DyneToNewtons => ParsedDyneToNewtons ?? T.CreateChecked(ConversionConstants.DyneToNewtons); + + /// DyneToNewtons parsed into , or when the is converted at each read. + private static readonly T? ParsedDyneToNewtons = StorageLiteral.Parse("1e-5"); /// Pound-force to newton conversion: 4.4482216152605 N/lbf (exact) - internal static readonly T PoundForceToNewtons = StorageLiteral.Parse("4.4482216152605", ConversionConstants.PoundForceToNewtons); + internal static T PoundForceToNewtons => ParsedPoundForceToNewtons ?? T.CreateChecked(ConversionConstants.PoundForceToNewtons); + + /// PoundForceToNewtons parsed into , or when the is converted at each read. + private static readonly T? ParsedPoundForceToNewtons = StorageLiteral.Parse("4.4482216152605"); /// Curie to becquerel conversion: 3.7e10 Bq/Ci (exact by definition) - internal static readonly T CurieToBecquerels = StorageLiteral.Parse("3.7e10", ConversionConstants.CurieToBecquerels); + internal static T CurieToBecquerels => ParsedCurieToBecquerels ?? T.CreateChecked(ConversionConstants.CurieToBecquerels); + + /// CurieToBecquerels parsed into , or when the is converted at each read. + private static readonly T? ParsedCurieToBecquerels = StorageLiteral.Parse("3.7e10"); /// Rad to gray conversion: 0.01 Gy/rad (exact by definition) - internal static readonly T RadToGrays = StorageLiteral.Parse("0.01", ConversionConstants.RadToGrays); + internal static T RadToGrays => ParsedRadToGrays ?? T.CreateChecked(ConversionConstants.RadToGrays); + + /// RadToGrays parsed into , or when the is converted at each read. + private static readonly T? ParsedRadToGrays = StorageLiteral.Parse("0.01"); /// Rem to sievert conversion: 0.01 Sv/rem (exact by definition) - internal static readonly T RemToSieverts = StorageLiteral.Parse("0.01", ConversionConstants.RemToSieverts); + internal static T RemToSieverts => ParsedRemToSieverts ?? T.CreateChecked(ConversionConstants.RemToSieverts); + + /// RemToSieverts parsed into , or when the is converted at each read. + private static readonly T? ParsedRemToSieverts = StorageLiteral.Parse("0.01"); /// Roentgen to coulomb per kilogram: 2.58e-4 C/kg per R (exact by definition) - internal static readonly T RoentgenToCoulombsPerKilogram = StorageLiteral.Parse("2.58e-4", ConversionConstants.RoentgenToCoulombsPerKilogram); + internal static T RoentgenToCoulombsPerKilogram => ParsedRoentgenToCoulombsPerKilogram ?? T.CreateChecked(ConversionConstants.RoentgenToCoulombsPerKilogram); + + /// RoentgenToCoulombsPerKilogram parsed into , or when the is converted at each read. + private static readonly T? ParsedRoentgenToCoulombsPerKilogram = StorageLiteral.Parse("2.58e-4"); /// Foot-candle to lux conversion: 1 lm/ft² = 1/0.09290304 = 100000000/9290304 lx/fc (exact, stored as a fraction) - internal static readonly T FootCandleToLux = StorageLiteral.Divide("100000000", "9290304", ConversionConstants.FootCandleToLux); + internal static T FootCandleToLux => ParsedFootCandleToLux ?? T.CreateChecked(ConversionConstants.FootCandleToLux); + + /// FootCandleToLux parsed into , or when the is converted at each read. + private static readonly T? ParsedFootCandleToLux = StorageLiteral.Divide("100000000", "9290304"); - /// Foot-lambert to candela per square meter: 1/(π × 0.09290304) cd/m² per fL (π to 150 significant digits, taken as DegreeToRadians × 180) - internal static readonly T FootLambertToCandelaPerSquareMeter = StorageLiteral.Parse("3.42625909963539052691674596165021859423458362052428959800814578422615229026362399099348746319287426200971476339509417994666710818534309005070458689906", ConversionConstants.FootLambertToCandelaPerSquareMeter); + /// Foot-lambert to candela per square meter: 1/(π × 0.09290304) cd/m² per fL, correctly rounded to 150 significant digits + internal static T FootLambertToCandelaPerSquareMeter => ParsedFootLambertToCandelaPerSquareMeter ?? T.CreateChecked(ConversionConstants.FootLambertToCandelaPerSquareMeter); + + /// FootLambertToCandelaPerSquareMeter parsed into , or when the is converted at each read. + private static readonly T? ParsedFootLambertToCandelaPerSquareMeter = StorageLiteral.Parse("3.42625909963539052691674596165021859423458362052428959800814578422615229026362399099348746319287476397054052755987227192765515199749284098513821314634"); /// Percent to ratio: 0.01 (exact by definition) - internal static readonly T PercentToRatio = StorageLiteral.Parse("0.01", ConversionConstants.PercentToRatio); + internal static T PercentToRatio => ParsedPercentToRatio ?? T.CreateChecked(ConversionConstants.PercentToRatio); + + /// PercentToRatio parsed into , or when the is converted at each read. + private static readonly T? ParsedPercentToRatio = StorageLiteral.Parse("0.01"); /// Parts per million to ratio: 1e-6 (exact by definition) - internal static readonly T PartPerMillionToRatio = StorageLiteral.Parse("1e-6", ConversionConstants.PartPerMillionToRatio); + internal static T PartPerMillionToRatio => ParsedPartPerMillionToRatio ?? T.CreateChecked(ConversionConstants.PartPerMillionToRatio); + + /// PartPerMillionToRatio parsed into , or when the is converted at each read. + private static readonly T? ParsedPartPerMillionToRatio = StorageLiteral.Parse("1e-6"); /// Parts per billion to ratio: 1e-9 (exact by definition) - internal static readonly T PartPerBillionToRatio = StorageLiteral.Parse("1e-9", ConversionConstants.PartPerBillionToRatio); + internal static T PartPerBillionToRatio => ParsedPartPerBillionToRatio ?? T.CreateChecked(ConversionConstants.PartPerBillionToRatio); + + /// PartPerBillionToRatio parsed into , or when the is converted at each read. + private static readonly T? ParsedPartPerBillionToRatio = StorageLiteral.Parse("1e-9"); /// Percent by weight to mass-fraction ratio: 0.01 (exact by definition) - internal static readonly T PercentByWeightToRatio = StorageLiteral.Parse("0.01", ConversionConstants.PercentByWeightToRatio); + internal static T PercentByWeightToRatio => ParsedPercentByWeightToRatio ?? T.CreateChecked(ConversionConstants.PercentByWeightToRatio); + + /// PercentByWeightToRatio parsed into , or when the is converted at each read. + private static readonly T? ParsedPercentByWeightToRatio = StorageLiteral.Parse("0.01"); } } diff --git a/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.MagnitudesGenerator/MetricMagnitudes.g.cs b/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.MagnitudesGenerator/MetricMagnitudes.g.cs index c100586a..27ceecf8 100644 --- a/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.MagnitudesGenerator/MetricMagnitudes.g.cs +++ b/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.MagnitudesGenerator/MetricMagnitudes.g.cs @@ -80,67 +80,130 @@ internal static class Values where T : struct, INumber { /// Yotta magnitude (Y): 10^24 - internal static readonly T Yotta = StorageLiteral.Parse("1e24", MetricMagnitudes.Yotta); + internal static T Yotta => ParsedYotta ?? T.CreateChecked(MetricMagnitudes.Yotta); + + /// Yotta parsed into , or when the is converted at each read. + private static readonly T? ParsedYotta = StorageLiteral.Parse("1e24"); /// Zetta magnitude (Z): 10^21 - internal static readonly T Zetta = StorageLiteral.Parse("1e21", MetricMagnitudes.Zetta); + internal static T Zetta => ParsedZetta ?? T.CreateChecked(MetricMagnitudes.Zetta); + + /// Zetta parsed into , or when the is converted at each read. + private static readonly T? ParsedZetta = StorageLiteral.Parse("1e21"); /// Exa magnitude (E): 10^18 - internal static readonly T Exa = StorageLiteral.Parse("1e18", MetricMagnitudes.Exa); + internal static T Exa => ParsedExa ?? T.CreateChecked(MetricMagnitudes.Exa); + + /// Exa parsed into , or when the is converted at each read. + private static readonly T? ParsedExa = StorageLiteral.Parse("1e18"); /// Peta magnitude (P): 10^15 - internal static readonly T Peta = StorageLiteral.Parse("1e15", MetricMagnitudes.Peta); + internal static T Peta => ParsedPeta ?? T.CreateChecked(MetricMagnitudes.Peta); + + /// Peta parsed into , or when the is converted at each read. + private static readonly T? ParsedPeta = StorageLiteral.Parse("1e15"); /// Tera magnitude (T): 10^12 - internal static readonly T Tera = StorageLiteral.Parse("1e12", MetricMagnitudes.Tera); + internal static T Tera => ParsedTera ?? T.CreateChecked(MetricMagnitudes.Tera); + + /// Tera parsed into , or when the is converted at each read. + private static readonly T? ParsedTera = StorageLiteral.Parse("1e12"); /// Giga magnitude (G): 10^9 - internal static readonly T Giga = StorageLiteral.Parse("1e9", MetricMagnitudes.Giga); + internal static T Giga => ParsedGiga ?? T.CreateChecked(MetricMagnitudes.Giga); + + /// Giga parsed into , or when the is converted at each read. + private static readonly T? ParsedGiga = StorageLiteral.Parse("1e9"); /// Mega magnitude (M): 10^6 - internal static readonly T Mega = StorageLiteral.Parse("1e6", MetricMagnitudes.Mega); + internal static T Mega => ParsedMega ?? T.CreateChecked(MetricMagnitudes.Mega); + + /// Mega parsed into , or when the is converted at each read. + private static readonly T? ParsedMega = StorageLiteral.Parse("1e6"); /// Kilo magnitude (k): 10^3 - internal static readonly T Kilo = StorageLiteral.Parse("1e3", MetricMagnitudes.Kilo); + internal static T Kilo => ParsedKilo ?? T.CreateChecked(MetricMagnitudes.Kilo); + + /// Kilo parsed into , or when the is converted at each read. + private static readonly T? ParsedKilo = StorageLiteral.Parse("1e3"); /// Hecto magnitude (h): 10^2 - internal static readonly T Hecto = StorageLiteral.Parse("1e2", MetricMagnitudes.Hecto); + internal static T Hecto => ParsedHecto ?? T.CreateChecked(MetricMagnitudes.Hecto); + + /// Hecto parsed into , or when the is converted at each read. + private static readonly T? ParsedHecto = StorageLiteral.Parse("1e2"); /// Deka magnitude (da): 10^1 - internal static readonly T Deka = StorageLiteral.Parse("1e1", MetricMagnitudes.Deka); + internal static T Deka => ParsedDeka ?? T.CreateChecked(MetricMagnitudes.Deka); + + /// Deka parsed into , or when the is converted at each read. + private static readonly T? ParsedDeka = StorageLiteral.Parse("1e1"); /// Unity magnitude (): 10^0 - internal static readonly T Unity = StorageLiteral.Parse("1.0", MetricMagnitudes.Unity); + internal static T Unity => ParsedUnity ?? T.CreateChecked(MetricMagnitudes.Unity); + + /// Unity parsed into , or when the is converted at each read. + private static readonly T? ParsedUnity = StorageLiteral.Parse("1.0"); /// Deci magnitude (d): 10^-1 - internal static readonly T Deci = StorageLiteral.Parse("1e-1", MetricMagnitudes.Deci); + internal static T Deci => ParsedDeci ?? T.CreateChecked(MetricMagnitudes.Deci); + + /// Deci parsed into , or when the is converted at each read. + private static readonly T? ParsedDeci = StorageLiteral.Parse("1e-1"); /// Centi magnitude (c): 10^-2 - internal static readonly T Centi = StorageLiteral.Parse("1e-2", MetricMagnitudes.Centi); + internal static T Centi => ParsedCenti ?? T.CreateChecked(MetricMagnitudes.Centi); + + /// Centi parsed into , or when the is converted at each read. + private static readonly T? ParsedCenti = StorageLiteral.Parse("1e-2"); /// Milli magnitude (m): 10^-3 - internal static readonly T Milli = StorageLiteral.Parse("1e-3", MetricMagnitudes.Milli); + internal static T Milli => ParsedMilli ?? T.CreateChecked(MetricMagnitudes.Milli); + + /// Milli parsed into , or when the is converted at each read. + private static readonly T? ParsedMilli = StorageLiteral.Parse("1e-3"); /// Micro magnitude (μ): 10^-6 - internal static readonly T Micro = StorageLiteral.Parse("1e-6", MetricMagnitudes.Micro); + internal static T Micro => ParsedMicro ?? T.CreateChecked(MetricMagnitudes.Micro); + + /// Micro parsed into , or when the is converted at each read. + private static readonly T? ParsedMicro = StorageLiteral.Parse("1e-6"); /// Nano magnitude (n): 10^-9 - internal static readonly T Nano = StorageLiteral.Parse("1e-9", MetricMagnitudes.Nano); + internal static T Nano => ParsedNano ?? T.CreateChecked(MetricMagnitudes.Nano); + + /// Nano parsed into , or when the is converted at each read. + private static readonly T? ParsedNano = StorageLiteral.Parse("1e-9"); /// Pico magnitude (p): 10^-12 - internal static readonly T Pico = StorageLiteral.Parse("1e-12", MetricMagnitudes.Pico); + internal static T Pico => ParsedPico ?? T.CreateChecked(MetricMagnitudes.Pico); + + /// Pico parsed into , or when the is converted at each read. + private static readonly T? ParsedPico = StorageLiteral.Parse("1e-12"); /// Femto magnitude (f): 10^-15 - internal static readonly T Femto = StorageLiteral.Parse("1e-15", MetricMagnitudes.Femto); + internal static T Femto => ParsedFemto ?? T.CreateChecked(MetricMagnitudes.Femto); + + /// Femto parsed into , or when the is converted at each read. + private static readonly T? ParsedFemto = StorageLiteral.Parse("1e-15"); /// Atto magnitude (a): 10^-18 - internal static readonly T Atto = StorageLiteral.Parse("1e-18", MetricMagnitudes.Atto); + internal static T Atto => ParsedAtto ?? T.CreateChecked(MetricMagnitudes.Atto); + + /// Atto parsed into , or when the is converted at each read. + private static readonly T? ParsedAtto = StorageLiteral.Parse("1e-18"); /// Zepto magnitude (z): 10^-21 - internal static readonly T Zepto = StorageLiteral.Parse("1e-21", MetricMagnitudes.Zepto); + internal static T Zepto => ParsedZepto ?? T.CreateChecked(MetricMagnitudes.Zepto); + + /// Zepto parsed into , or when the is converted at each read. + private static readonly T? ParsedZepto = StorageLiteral.Parse("1e-21"); /// Yocto magnitude (y): 10^-24 - internal static readonly T Yocto = StorageLiteral.Parse("1e-24", MetricMagnitudes.Yocto); + internal static T Yocto => ParsedYocto ?? T.CreateChecked(MetricMagnitudes.Yocto); + + /// Yocto parsed into , or when the is converted at each read. + private static readonly T? ParsedYocto = StorageLiteral.Parse("1e-24"); } } diff --git a/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.PhysicalConstantsGenerator/PhysicalConstants.g.cs b/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.PhysicalConstantsGenerator/PhysicalConstants.g.cs index 531ce96d..5d01a603 100644 --- a/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.PhysicalConstantsGenerator/PhysicalConstants.g.cs +++ b/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.PhysicalConstantsGenerator/PhysicalConstants.g.cs @@ -54,13 +54,13 @@ private static class Values /// public static class AngularMechanics { - /// Degrees per radian: 180/π ≈ 57.29577951308232 + /// Degrees per radian: 180/π ≈ 57.29577951308232 (correctly rounded to 150 significant digits) public static T DegreesPerRadian() where T : struct, INumber => Values.DegreesPerRadian; - /// Radians per degree: π/180 ≈ 0.017453292519943295 + /// Radians per degree: π/180 ≈ 0.017453292519943295 (correctly rounded to 150 significant digits) public static T RadiansPerDegree() where T : struct, INumber => Values.RadiansPerDegree; - /// 2π - Full rotation in radians: 6.283185307179586 + /// 2π, a full rotation in radians: 6.283185307179586 (correctly rounded to 150 significant digits) public static T TwoPi() where T : struct, INumber => Values.TwoPi; /// @@ -69,14 +69,14 @@ public static class AngularMechanics private static class Values where T : struct, INumber { - /// Degrees per radian: 180/π ≈ 57.29577951308232 - internal static readonly T DegreesPerRadian = T.Parse("57.29577951308232087679815481410517033240547246656432154916024386120284714832155263244096899585111094418897585567892854596978524038074810298080734906", NumberStyles.Float, CultureInfo.InvariantCulture); + /// Degrees per radian: 180/π ≈ 57.29577951308232 (correctly rounded to 150 significant digits) + internal static readonly T DegreesPerRadian = T.Parse("57.2957795130823208767981548141051703324054724665643215491602438612028471483215526324409689958511109441862233816328648932814482646012483150360682678634", NumberStyles.Float, CultureInfo.InvariantCulture); - /// Radians per degree: π/180 ≈ 0.017453292519943295 - internal static readonly T RadiansPerDegree = T.Parse("0.017453292519943295769236907684886127134428718885417254560971914401710091146034494436822415696345097379101040706699150667990539631694451077627806983", NumberStyles.Float, CultureInfo.InvariantCulture); + /// Radians per degree: π/180 ≈ 0.017453292519943295 (correctly rounded to 150 significant digits) + internal static readonly T RadiansPerDegree = T.Parse("0.0174532925199432957692369076848861271344287188854172545609719144017100911460344944368224156963450948221230449250737905924838546922752810123984742189340", NumberStyles.Float, CultureInfo.InvariantCulture); - /// 2π - Full rotation in radians: 6.283185307179586 - internal static readonly T TwoPi = T.Parse("6.283185307179586476925286766559005768394338798750211641949889184615632812572417997256069650684234135964735462226659258240820374631042607435096896808248", NumberStyles.Float, CultureInfo.InvariantCulture); + /// 2π, a full rotation in radians: 6.283185307179586 (correctly rounded to 150 significant digits) + internal static readonly T TwoPi = T.Parse("6.28318530717958647692528676655900576839433879875021164194988918461563281257241799725606965068423413596429617302656461329418768921910116446345071881626", NumberStyles.Float, CultureInfo.InvariantCulture); } } @@ -338,7 +338,7 @@ public static class Generic /// Gets boltzmann constant: 1.380649 × 10⁻²³ j/k (exact, si defining constant) as type T. public static T BoltzmannConstant() where T : struct, INumber => Fundamental.BoltzmannConstant(); - /// Gets degrees per radian: 180/π ≈ 57.29577951308232 as type T. + /// Gets degrees per radian: 180/π ≈ 57.29577951308232 (correctly rounded to 150 significant digits) as type T. public static T DegreesPerRadian() where T : struct, INumber => AngularMechanics.DegreesPerRadian(); /// Gets elementary charge: 1.602176634 × 10⁻¹⁹ c (exact, si defining constant) as type T. @@ -377,7 +377,7 @@ public static class Generic /// Gets planck constant: 6.62607015 × 10⁻³⁴ j·s (exact, si defining constant) as type T. public static T PlanckConstant() where T : struct, INumber => Fundamental.PlanckConstant(); - /// Gets radians per degree: π/180 ≈ 0.017453292519943295 as type T. + /// Gets radians per degree: π/180 ≈ 0.017453292519943295 (correctly rounded to 150 significant digits) as type T. public static T RadiansPerDegree() where T : struct, INumber => AngularMechanics.RadiansPerDegree(); /// Gets reference sound intensity: 1 × 10⁻¹² w/m² (threshold of hearing) as type T. @@ -407,7 +407,7 @@ public static class Generic /// Gets standard temperature (stp): 273.15 k (0°c) as type T. public static T StandardTemperature() where T : struct, INumber => Thermodynamics.StandardTemperature(); - /// Gets 2π - full rotation in radians: 6.283185307179586 as type T. + /// Gets 2π, a full rotation in radians: 6.283185307179586 (correctly rounded to 150 significant digits) as type T. public static T TwoPi() where T : struct, INumber => AngularMechanics.TwoPi(); /// Gets water boiling point at 1 atm: 373.15 k (100°c) as type T. diff --git a/Semantics.Quantities/StorageLiteral.cs b/Semantics.Quantities/StorageLiteral.cs index c2f41d68..56326949 100644 --- a/Semantics.Quantities/StorageLiteral.cs +++ b/Semantics.Quantities/StorageLiteral.cs @@ -2,6 +2,7 @@ namespace ktsu.Semantics.Quantities; +using System; using System.Globalization; using System.Numerics; @@ -17,43 +18,100 @@ namespace ktsu.Semantics.Quantities; /// value it can represent, and a fraction is divided in the storage type itself. /// /// -/// An integer storage type keeps the old route. Parsing "0.3048" into an -/// fails, and dividing 5 by 9 in one answers zero for a different reason than truncating 0.5555… -/// does, so integers convert the exactly as they did before. A type that -/// cannot parse the literal falls back the same way. +/// Both methods answer when a storage type keeps the old route, and the +/// generated holder then converts the constant with T.CreateChecked each +/// time the value is read. An integer type keeps it, because parsing "0.3048" into an +/// fails, and dividing 5 by 9 in one answers zero for a different reason than +/// truncating 0.5555… does. A type that cannot parse the literal keeps it too. +/// +/// +/// Nothing here converts the , and that is deliberate. The generated holder +/// evaluates every value for a storage type in one static initializer, so a conversion that overflowed +/// there, such as 10^12 into , would make every value for that type throw +/// , which is what 5.2.0 shipped. Converting at the read lets +/// each value succeed or throw on its own, as the generated factories +/// did before 5.2.0. /// /// internal static class StorageLiteral { /// - /// Converts a decimal literal to . + /// Parses a decimal literal into . /// /// The numeric storage type. /// The literal as written in the metadata, for example "0.3048" or "1e-10". - /// The same value as a , used when is an integer or cannot parse the literal. - /// The nearest value represents. - internal static T Parse(string literal, double fallback) + /// + /// The nearest value represents, or when + /// is an integer or cannot parse the literal. + /// + internal static T? Parse(string literal) where T : struct, INumber - => !IsIntegral() && T.TryParse(literal, NumberStyles.Float, CultureInfo.InvariantCulture, out T value) - ? value - : T.CreateChecked(fallback); + => !IsIntegral() && TryParseLiteral(literal, out T value) ? value : null; /// - /// Converts an exact fraction of two decimal literals to , dividing in . + /// Divides one decimal literal by another in . /// /// The numeric storage type. /// The numerator literal, for example "5". /// The denominator literal, for example "9". - /// The quotient as a , used when is an integer or cannot parse either literal. - /// The quotient at the precision divides to. - internal static T Divide(string numerator, string denominator, double fallback) + /// + /// The quotient at the precision divides to, or when + /// is an integer, cannot parse either literal, or overflows dividing them. + /// + internal static T? Divide(string numerator, string denominator) where T : struct, INumber - => !IsIntegral() - && T.TryParse(numerator, NumberStyles.Float, CultureInfo.InvariantCulture, out T dividend) - && T.TryParse(denominator, NumberStyles.Float, CultureInfo.InvariantCulture, out T divisor) - && !T.IsZero(divisor) - ? dividend / divisor - : T.CreateChecked(fallback); + { + if (IsIntegral() + || !TryParseLiteral(numerator, out T dividend) + || !TryParseLiteral(denominator, out T divisor) + || T.IsZero(divisor)) + { + return null; + } + + try + { + return dividend / divisor; + } + catch (OverflowException) + { + // Converting the double quotient reports the same overflow, at the read that needs the value. + return null; + } + } + + /// + /// Parses a literal with , treating a parse that throws for that style + /// the same as one that returns . + /// + /// The numeric storage type. + /// The literal. + /// The parsed value, or the default when parsing failed. + /// when parsed the literal. + /// + /// A numeric type outside the base library may throw or + /// for a style it does not handle, rather than return + /// . Only those two are caught, so any other failure still surfaces. + /// + private static bool TryParseLiteral(string literal, out T value) + where T : struct, INumber + { + try + { + return T.TryParse(literal, NumberStyles.Float, CultureInfo.InvariantCulture, out value); + } + catch (NotSupportedException) + { + // The type does not parse this style. The double constant still converts. + } + catch (ArgumentException) + { + // The type rejects this style as an argument. The double constant still converts. + } + + value = default; + return false; + } /// /// Reports whether discards fractions, which is to say whether a half is zero in it. diff --git a/Semantics.Quantities/StorageMath.cs b/Semantics.Quantities/StorageMath.cs index ee305460..561b8ae6 100644 --- a/Semantics.Quantities/StorageMath.cs +++ b/Semantics.Quantities/StorageMath.cs @@ -11,12 +11,13 @@ namespace ktsu.Semantics.Quantities; internal static class StorageMath { /// - /// The most Newton steps taken before the current estimate is returned as it stands. + /// The most Newton steps taken before the root is reported as not converging. /// /// - /// From a seed each step roughly doubles the number of correct digits, so a - /// handful suffice. The cap matters only when no seed could be taken and the estimate starts at - /// the value itself, where the early steps halve it rather than refine it. + /// The seed is always within a factor of two of the root, and from there each step roughly doubles + /// the number of correct digits, so a type would need far more digits than any real one holds to come + /// near the cap. Reaching it means the estimate is cycling rather than settling, and the estimate at + /// that point is not the root, so it is an error rather than an answer. /// private const int MaximumIterations = 256; @@ -25,10 +26,13 @@ internal static class StorageMath /// /// The numeric storage type. /// The value to take the root of. Generated callers pass a sum of squares, which is never negative. - /// The square root of . + /// The square root of , or its floor for an integer type. /// /// is negative and cannot represent the that results. /// + /// + /// The Newton steps do not settle on a root within . + /// /// /// /// The binary floating point and integer primitives take the route every generated @@ -37,12 +41,11 @@ internal static class StorageMath /// already as precise as the type. /// /// - /// Any other type is refined in its own arithmetic: seeded from that same - /// root, then Newton steps x = (x + value / x) / 2 until the estimate stops changing. A - /// root therefore has 28 significant digits rather than the 15 its - /// conversion from keeps, and no constraint beyond - /// is needed, which could not meet had this required - /// . + /// Any other type is refined in its own arithmetic: seeded near the root, then Newton steps + /// x = (x + value / x) / 2 until the estimate stops changing. A root + /// therefore has 28 significant digits rather than the 15 its conversion from + /// keeps, and no constraint beyond is needed, which + /// could not meet had this required . /// /// /// A negative input keeps its old behaviour too, where the type has @@ -63,7 +66,7 @@ internal static T Sqrt(T value) } T two = T.One + T.One; - T estimate = Seed(value); + T estimate = Seed(value, two); T previous = estimate; for (int iteration = 0; iteration < MaximumIterations; iteration++) @@ -85,7 +88,7 @@ internal static T Sqrt(T value) estimate = next; } - return estimate; + throw new ArithmeticException($"The square root did not settle within {MaximumIterations} Newton steps."); } /// @@ -121,36 +124,94 @@ private static bool IsRoundedThroughDouble() /// /// The numeric storage type. /// The positive value whose root is wanted. - /// The root converted to when both conversions succeed, otherwise the larger of and one. + /// Two, in . + /// An estimate within a factor of two of the root. /// - /// Any positive start converges, because Newton's method for a square root approaches from above - /// after its first step. The fallback exists for a type that will not convert to or from - /// , or whose value is outside its range. + /// + /// The root is used directly when converts to a normal + /// . Otherwise the value is outside the range of , or too + /// small for a to hold with any precision, or the type does not convert at all. + /// It is then scaled by powers of four into [1, 4), which every type holds, the root is taken there, + /// and that root is scaled back by the matching power of two. + /// + /// + /// This used to start from the value itself. Newton's method still converges from there, but each + /// early step only halves the estimate, so a of 2^2048 used + /// every step on halving and returned about 2^1792 as its root. + /// /// - private static T Seed(T value) + private static T Seed(T value, T two) + where T : struct, INumber + { + if (TryRootThroughDouble(value, out T direct)) + { + return direct; + } + + T four = two * two; + T scaled = value; + int powerOfTwo = 0; + + while (scaled >= four) + { + scaled /= four; + powerOfTwo++; + } + + while (scaled < T.One) + { + scaled *= four; + powerOfTwo--; + } + + T root = TryRootThroughDouble(scaled, out T scaledRoot) ? scaledRoot : T.One; + + for (; powerOfTwo > 0; powerOfTwo--) + { + root *= two; + } + + for (; powerOfTwo < 0; powerOfTwo++) + { + root /= two; + } + + return root; + } + + /// + /// Takes the root of a value through when the value converts to a normal + /// and the root converts back to something other than zero. + /// + /// The numeric storage type. + /// The positive value. + /// The root in , or the default when this fails. + /// when holds a usable estimate. + private static bool TryRootThroughDouble(T value, out T root) where T : struct, INumber { try { - double root = Math.Sqrt(double.CreateChecked(value)); - if (double.IsFinite(root) && root > 0d) + double asDouble = double.CreateChecked(value); + if (double.IsNormal(asDouble)) { - T seed = T.CreateChecked(root); - if (!T.IsZero(seed)) + root = T.CreateChecked(Math.Sqrt(asDouble)); + if (!T.IsZero(root)) { - return seed; + return true; } } } catch (NotSupportedException) { - // The type does not convert through double. The fallback below still converges. + // The type does not convert through double. The caller scales into a range that needs no conversion. } catch (OverflowException) { - // The value or its root is outside the range of double or of the type. The fallback below still converges. + // The value or its root is outside the range of double or of the type. The caller scales it. } - return value > T.One ? value : T.One; + root = default; + return false; } } diff --git a/Semantics.SourceGenerators/AnalyzerReleases.Unshipped.md b/Semantics.SourceGenerators/AnalyzerReleases.Unshipped.md index ed61c588..18be11ef 100644 --- a/Semantics.SourceGenerators/AnalyzerReleases.Unshipped.md +++ b/Semantics.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -13,4 +13,4 @@ SEM005 | Semantics.SourceGenerators | Warning | Reports schema-level validation SEM006 | Semantics.SourceGenerators | Warning | Reports a metadata file a generator declared that was not supplied as an AdditionalFile. SEM007 | Semantics.SourceGenerators | Error | Reports a metadata file that could not be parsed. Replaces the base generator's CONV001. SEM008 | Semantics.SourceGenerators | Warning | Reports a relationship whose declared result does not follow from the dimensions of its operands, or whose signed value cannot land in a magnitude result. -SEM009 | Semantics.SourceGenerators | Error | Reports a conversions.json factor whose value is neither a decimal literal nor a fraction of two decimal literals with a non-zero denominator. +SEM009 | Semantics.SourceGenerators | Error | Reports a conversions.json factor whose value is neither a decimal literal nor a fraction of two decimal literals with a non-zero denominator, or that a double cannot hold. diff --git a/Semantics.SourceGenerators/Generators/ConversionValue.cs b/Semantics.SourceGenerators/Generators/ConversionValue.cs index 2f3d6d48..e024ff27 100644 --- a/Semantics.SourceGenerators/Generators/ConversionValue.cs +++ b/Semantics.SourceGenerators/Generators/ConversionValue.cs @@ -2,15 +2,27 @@ namespace Semantics.SourceGenerators; +using System.Globalization; + /// /// The value of one conversion factor in conversions.json: a decimal literal, or an exact /// fraction of two decimal literals. /// /// +/// /// A repeating ratio such as 5/9 has no finite decimal literal, so writing it as one rounds it before /// any storage type sees it. Keeping the fraction lets each storage type divide at its own precision: /// gets the correctly rounded quotient, and gets 28 digits /// rather than the 17 a -length literal carried. +/// +/// +/// Writing a factor as its exact definition can move its constant to the adjacent +/// representable value, because a rounded 17-digit literal is not always the +/// nearest the true value. 5.2.0 moved two constants this way: PsiToPascals from +/// 6894.757293168361 to 6894.757293168362, and RevolutionPerMinuteToRadianPerSecond from +/// 0.10471975511965977 to 0.10471975511965978. The public IUnit.ToBaseFactor of Psi and +/// RevolutionPerMinute moved with them. +/// /// /// The literal, or the numerator of the fraction. /// The denominator of the fraction, or for a plain literal. @@ -26,29 +38,36 @@ internal sealed class ConversionValue(string numerator, string? denominator) /// Gets a C# constant expression of type for the value. /// /// - /// A plain literal is written exactly as the metadata spells it, so the committed constants for - /// existing factors do not change. Each operand of a fraction carries a d suffix, because - /// 5 / 9 would be integer division and evaluate to zero. + /// Every operand carries a d suffix. Without it a fraction such as 5 / 9 is integer + /// division and evaluates to zero, and a literal with no point or exponent that is too large for + /// , such as 100000000000000000000, is an integer literal the compiler + /// rejects (CS1021). The suffix changes no value that compiled without it. /// public string DoubleExpression => Denominator is null - ? Numerator + ? $"{Numerator}d" : $"{Numerator}d / {Denominator}d"; /// - /// Builds the expression that materialises the value into the storage type T. + /// Gets the expression that parses the value into the storage type T. /// - /// An expression for the same value as a , for storage types that cannot parse it. - /// A call to StorageLiteral.Parse or StorageLiteral.Divide. - public string StorageExpression(string fallback) => Denominator is null - ? $"StorageLiteral.Parse(\"{Numerator}\", {fallback})" - : $"StorageLiteral.Divide(\"{Numerator}\", \"{Denominator}\", {fallback})"; + /// + /// The call answers when T keeps converting the + /// constant, and the generated holder does that conversion at each read. + /// + public string StorageExpression => Denominator is null + ? $"StorageLiteral.Parse(\"{Numerator}\")" + : $"StorageLiteral.Divide(\"{Numerator}\", \"{Denominator}\")"; /// /// Reads a factor value, accepting a decimal literal such as "0.3048" or "1e-10", or a - /// fraction of two such as "5/9". + /// fraction of two such as "5/9", when a can hold it. /// /// The value as written in the metadata. - /// The value, or when is neither form or the denominator is zero. + /// + /// The value, or when is neither form, the denominator + /// is zero, or a literal or the quotient is beyond the range of or is non-zero + /// and rounds to zero in it. + /// public static ConversionValue? Parse(string? text) { if (text is null) @@ -59,17 +78,47 @@ public string StorageExpression(string fallback) => Denominator is null int slash = text.IndexOf('/'); if (slash < 0) { - return IsDecimalLiteral(text) ? new ConversionValue(text, null) : null; + return IsDecimalLiteral(text) && TryReadDouble(text, out _) + ? new ConversionValue(text, null) + : null; } string top = text.Substring(0, slash); string bottom = text.Substring(slash + 1); - return IsDecimalLiteral(top) && IsDecimalLiteral(bottom) && !IsZero(bottom) - ? new ConversionValue(top, bottom) - : null; + return IsDecimalLiteral(top) + && IsDecimalLiteral(bottom) + && !IsZero(bottom) + && TryReadDouble(top, out double dividend) + && TryReadDouble(bottom, out double divisor) + && IsHeldByDouble(dividend / divisor, IsZero(top)) + ? new ConversionValue(top, bottom) + : null; } + /// + /// Reads a decimal literal as a , failing when the cannot hold it. + /// + /// A literal that accepted. + /// The value as a . + /// when the literal is within the finite range of and does not round a non-zero value to zero. + /// + /// Parsing a literal beyond the range fails on .NET Framework and answers infinity on .NET, and the + /// generator can run on either, so both outcomes are rejected. + /// + private static bool TryReadDouble(string literal, out double value) + => double.TryParse(literal, NumberStyles.Float, CultureInfo.InvariantCulture, out value) + && IsHeldByDouble(value, IsZero(literal)); + + /// + /// Reports whether a faithfully holds a value: finite, and zero only when the value is zero. + /// + /// The value as a . + /// Whether the exact value is zero. + /// when is usable as the constant. + private static bool IsHeldByDouble(double value, bool isZero) + => !double.IsInfinity(value) && !double.IsNaN(value) && (isZero || value != 0d); + /// /// Reports whether is a decimal literal that is valid both in C# and in /// Parse with NumberStyles.Float: an optional sign, digits with an optional diff --git a/Semantics.SourceGenerators/Generators/ConversionsGenerator.cs b/Semantics.SourceGenerators/Generators/ConversionsGenerator.cs index 725b18cc..83347baa 100644 --- a/Semantics.SourceGenerators/Generators/ConversionsGenerator.cs +++ b/Semantics.SourceGenerators/Generators/ConversionsGenerator.cs @@ -23,13 +23,25 @@ namespace Semantics.SourceGenerators; /// significant digits of a 17-digit literal. /// /// -/// A value is a decimal literal or an exact fraction of two, "5/9". Anything else is reported -/// as SEM009 and generates no constant. +/// Each factor in the holder is a property over a nullable parsed value. A storage type that does not +/// parse it, such as an integer, converts the at each read, so a factor too +/// large for the type throws from the factory that uses it and +/// leaves the others working, as before 5.2.0. Converting in the static initializer instead made one +/// overflow, CurieToBecquerels into , fail every factor for the type. +/// +/// +/// A value is a decimal literal or an exact fraction of two, "5/9", that a +/// can hold. Anything else is reported as SEM009 and generates no constant. /// /// [Generator] public class ConversionsGenerator : SemanticsGenerator { + /// + /// Prefix of the private field holding each factor parsed into the storage type. + /// + private const string ParsedPrefix = "Parsed"; + /// /// Name of the nested holder that caches each factor materialised into a storage type. /// @@ -122,7 +134,7 @@ protected override void Generate(SourceProductionContext context, ConversionsMet DefaultValue = value.DoubleExpression, }); - // Qualified, because inside the holder the bare name is the field being declared. + // Qualified, because inside the holder the bare name is the property being declared. holderClass.Members.Add(new FieldTemplate() { Comments = @@ -133,11 +145,26 @@ protected override void Generate(SourceProductionContext context, ConversionsMet { "internal", Emit.Static, - "readonly", "T", }, - Name = factor.Name, - DefaultValue = value.StorageExpression($"ConversionConstants.{factor.Name}"), + Name = $"{factor.Name} => {ParsedPrefix}{factor.Name} ?? T.CreateChecked(ConversionConstants.{factor.Name})", + }); + + holderClass.Members.Add(new FieldTemplate() + { + Comments = + { + $"/// {factor.Name} parsed into , or when the is converted at each read.", + }, + Keywords = + { + "private", + Emit.Static, + "readonly", + "T?", + }, + Name = $"{ParsedPrefix}{factor.Name}", + DefaultValue = value.StorageExpression, }); } } diff --git a/Semantics.SourceGenerators/Generators/MagnitudesGenerator.cs b/Semantics.SourceGenerators/Generators/MagnitudesGenerator.cs index 2627598f..8eb8f08d 100644 --- a/Semantics.SourceGenerators/Generators/MagnitudesGenerator.cs +++ b/Semantics.SourceGenerators/Generators/MagnitudesGenerator.cs @@ -13,14 +13,28 @@ namespace Semantics.SourceGenerators; /// Source generator that creates the MetricMagnitudes.cs file from JSON metadata. /// /// +/// /// The public constants are unchanged. Alongside them an internal /// Values<T> holder parses each power of ten into each storage type once, which is what /// the generated factories multiply by. A such as 1e-2 is not exactly a /// hundredth, and converting it gave a quantity its rounding. +/// +/// +/// Each magnitude is a property over a nullable parsed value. A storage type that does not parse it, +/// such as an integer, converts the at each read, so a magnitude too large +/// for the type throws where it is used and leaves the others +/// working. Converting in the static initializer instead made one overflow, 10^12 into +/// , fail every magnitude for the type. +/// /// [Generator] public class MagnitudesGenerator : SemanticsGenerator { + /// + /// Prefix of the private field holding each value parsed into the storage type. + /// + private const string ParsedPrefix = "Parsed"; + public MagnitudesGenerator() : base("magnitudes.json") { } protected override void Generate(SourceProductionContext context, MagnitudesMetadata metadata, CodeBlocker codeBlocker) @@ -85,13 +99,20 @@ protected override void Generate(SourceProductionContext context, MagnitudesMeta DefaultValue = valueString, }); - // Qualified, because inside the holder the bare name is the field being declared. + // Qualified, because inside the holder the bare name is the property being declared. holderClass.Members.Add(new FieldTemplate() { Comments = {comment}, - Keywords = {"internal", Emit.Static, "readonly", "T"}, - Name = magnitude.Name, - DefaultValue = $"StorageLiteral.Parse(\"{valueString}\", MetricMagnitudes.{magnitude.Name})", + Keywords = {"internal", Emit.Static, "T"}, + Name = $"{magnitude.Name} => {ParsedPrefix}{magnitude.Name} ?? T.CreateChecked(MetricMagnitudes.{magnitude.Name})", + }); + + holderClass.Members.Add(new FieldTemplate() + { + Comments = {$"/// {magnitude.Name} parsed into , or when the is converted at each read."}, + Keywords = {"private", Emit.Static, "readonly", "T?"}, + Name = $"{ParsedPrefix}{magnitude.Name}", + DefaultValue = $"StorageLiteral.Parse(\"{valueString}\")", }); } diff --git a/Semantics.SourceGenerators/Generators/QuantitiesGenerator.cs b/Semantics.SourceGenerators/Generators/QuantitiesGenerator.cs index 3d6d615d..525134b4 100644 --- a/Semantics.SourceGenerators/Generators/QuantitiesGenerator.cs +++ b/Semantics.SourceGenerators/Generators/QuantitiesGenerator.cs @@ -633,7 +633,8 @@ private static void AddUnitFactories( /// /// Builds the C# expression converting value in to the SI /// base unit. Honours magnitude (Kilo, Centi, …), conversionFactor (lookup in - /// ), and offset (additive, after scaling). + /// ), their product when a unit declares both, and offset + /// (additive, after scaling). /// /// /// Each factor is read from the Values<T> holder that @@ -656,7 +657,13 @@ private static string BuildToBaseExpression(string unitName, IReadOnlyDictionary bool hasMagnitude = !string.IsNullOrEmpty(unit.Magnitude) && unit.Magnitude != "1"; bool hasFactor = !string.IsNullOrEmpty(unit.ConversionFactor) && unit.ConversionFactor != "1"; - if (hasMagnitude) + // Both, multiplied together first, so the factory scales by exactly the factor the unit's + // IUnit.ToBaseFactorAs() reports (UnitsGenerator.BuildStorageFactorExpression). + if (hasMagnitude && hasFactor) + { + scaled = $"(value * (MetricMagnitudes.Values.{unit.Magnitude} * Units.ConversionConstants.Values.{unit.ConversionFactor}))"; + } + else if (hasMagnitude) { scaled = $"(value * MetricMagnitudes.Values.{unit.Magnitude})"; } diff --git a/Semantics.SourceGenerators/Metadata/conversions.json b/Semantics.SourceGenerators/Metadata/conversions.json index 4bf1eb9f..280bc0ed 100644 --- a/Semantics.SourceGenerators/Metadata/conversions.json +++ b/Semantics.SourceGenerators/Metadata/conversions.json @@ -186,18 +186,18 @@ "factors": [ { "name": "DegreeToRadians", - "description": "Degree to radian conversion: π/180 rad/° (exact)", - "value": "0.017453292519943295769236907684886127134428718885417254560971914401710091146034494436822415696345097379101040706699150667990539631694451077627806983" + "description": "Degree to radian conversion: π/180 rad/°, correctly rounded to 150 significant digits", + "value": "0.0174532925199432957692369076848861271344287188854172545609719144017100911460344944368224156963450948221230449250737905924838546922752810123984742189340" }, { "name": "GradianToRadians", - "description": "Gradian to radian conversion: π/200 rad/grad (π to 150 significant digits, taken as DegreeToRadians × 180)", - "value": "0.0157079632679489661923132169163975144209858469968755291048747229615390820314310449931401741267105876411909366360292356011914856685250059698650262847" + "description": "Gradian to radian conversion: π/200 rad/grad, correctly rounded to 150 significant digits", + "value": "0.0157079632679489661923132169163975144209858469968755291048747229615390820314310449931401741267105853399107404325664115332354692230477529111586267970406" }, { "name": "RevolutionToRadians", - "description": "Revolution to radian conversion: 2π rad/rev (π to 150 significant digits, taken as DegreeToRadians × 180)", - "value": "6.28318530717958647692528676655900576839433879875021164194988918461563281257241799725606965068423505647637465441169424047659426741000238794601051388" + "description": "Revolution to radian conversion: 2π rad/rev, correctly rounded to 150 significant digits", + "value": "6.28318530717958647692528676655900576839433879875021164194988918461563281257241799725606965068423413596429617302656461329418768921910116446345071881626" } ] }, @@ -351,8 +351,8 @@ "factors": [ { "name": "RevolutionPerMinuteToRadianPerSecond", - "description": "RPM to rad/s conversion: 2π/60 = π/30 rad/s per rpm (π to 150 significant digits, taken as DegreeToRadians × 180)", - "value": "0.104719755119659774615421446109316762806572313312503527365831486410260546876206966620934494178070584274606244240194904007943237790166706465766841898" + "description": "RPM to rad/s conversion: 2π/60 = π/30 rad/s per rpm, correctly rounded to 150 significant digits", + "value": "0.104719755119659774615421446109316762806572313312503527365831486410260546876206966620934494178070568932738269550442743554903128153651686074390845313604" } ] }, @@ -535,8 +535,8 @@ }, { "name": "FootLambertToCandelaPerSquareMeter", - "description": "Foot-lambert to candela per square meter: 1/(π × 0.09290304) cd/m² per fL (π to 150 significant digits, taken as DegreeToRadians × 180)", - "value": "3.42625909963539052691674596165021859423458362052428959800814578422615229026362399099348746319287426200971476339509417994666710818534309005070458689906" + "description": "Foot-lambert to candela per square meter: 1/(π × 0.09290304) cd/m² per fL, correctly rounded to 150 significant digits", + "value": "3.42625909963539052691674596165021859423458362052428959800814578422615229026362399099348746319287476397054052755987227192765515199749284098513821314634" } ] }, diff --git a/Semantics.SourceGenerators/Metadata/domains.json b/Semantics.SourceGenerators/Metadata/domains.json index 00bf43d2..84097a95 100644 --- a/Semantics.SourceGenerators/Metadata/domains.json +++ b/Semantics.SourceGenerators/Metadata/domains.json @@ -170,18 +170,18 @@ "constants": [ { "name": "TwoPi", - "description": "2π - Full rotation in radians: 6.283185307179586", - "value": "6.283185307179586476925286766559005768394338798750211641949889184615632812572417997256069650684234135964735462226659258240820374631042607435096896808248" + "description": "2π, a full rotation in radians: 6.283185307179586 (correctly rounded to 150 significant digits)", + "value": "6.28318530717958647692528676655900576839433879875021164194988918461563281257241799725606965068423413596429617302656461329418768921910116446345071881626" }, { "name": "DegreesPerRadian", - "description": "Degrees per radian: 180/π ≈ 57.29577951308232", - "value": "57.29577951308232087679815481410517033240547246656432154916024386120284714832155263244096899585111094418897585567892854596978524038074810298080734906" + "description": "Degrees per radian: 180/π ≈ 57.29577951308232 (correctly rounded to 150 significant digits)", + "value": "57.2957795130823208767981548141051703324054724665643215491602438612028471483215526324409689958511109441862233816328648932814482646012483150360682678634" }, { "name": "RadiansPerDegree", - "description": "Radians per degree: π/180 ≈ 0.017453292519943295", - "value": "0.017453292519943295769236907684886127134428718885417254560971914401710091146034494436822415696345097379101040706699150667990539631694451077627806983" + "description": "Radians per degree: π/180 ≈ 0.017453292519943295 (correctly rounded to 150 significant digits)", + "value": "0.0174532925199432957692369076848861271344287188854172545609719144017100911460344944368224156963450948221230449250737905924838546922752810123984742189340" } ] }, diff --git a/Semantics.SourceGenerators/SemanticsDiagnostics.cs b/Semantics.SourceGenerators/SemanticsDiagnostics.cs index 60328a29..14d09474 100644 --- a/Semantics.SourceGenerators/SemanticsDiagnostics.cs +++ b/Semantics.SourceGenerators/SemanticsDiagnostics.cs @@ -107,16 +107,19 @@ public static class SemanticsDiagnostics "Relationship {0} {1} No operator is generated for it; fix dimensions.json."); /// - /// SEM009: a conversion factor's value is neither a decimal literal nor a fraction of two. + /// SEM009: a conversion factor's value is neither a decimal literal nor a fraction of two, or a + /// cannot hold it. /// /// - /// A factor is emitted as a C# constant and parsed into every storage type, so a value that is - /// neither form would otherwise surface as a compile error in generated code, far from the line in - /// conversions.json that caused it. An error rather than a warning, because every unit using - /// the factor fails to compile without its constant. + /// A factor is emitted as a C# constant and parsed into every storage type, so a + /// value that is neither form would otherwise surface as a compile error in generated code, far from + /// the line in conversions.json that caused it. So would a literal beyond the range of + /// (CS0594), and a non-zero value that rounds to zero or a quotient that + /// overflows would compile into a factor that is silently wrong. An error rather than a warning, + /// because every unit using the factor fails to compile without its constant. /// public static DiagnosticDescriptor InvalidConversionFactor { get; } = Catalog.Error( 9, "conversions.json factor value is malformed", - "Conversion factor '{0}' has value '{1}', which is neither a decimal literal such as \"0.3048\" nor a fraction of two such as \"5/9\" with a non-zero denominator. No constant is generated for it. Fix conversions.json."); + "Conversion factor '{0}' has value '{1}', which is not a decimal literal such as \"0.3048\" or a fraction of two such as \"5/9\" with a non-zero denominator, that a double can hold without overflowing or rounding to zero. No constant is generated for it. Fix conversions.json."); } diff --git a/Semantics.Test/Quantities/GeneratorDiagnosticTests.cs b/Semantics.Test/Quantities/GeneratorDiagnosticTests.cs index 342cde00..d9fce564 100644 --- a/Semantics.Test/Quantities/GeneratorDiagnosticTests.cs +++ b/Semantics.Test/Quantities/GeneratorDiagnosticTests.cs @@ -5,6 +5,7 @@ namespace ktsu.Semantics.Test.Quantities; using System.Collections.Generic; using System.Linq; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.TestTools.UnitTesting; using global::Semantics.SourceGenerators; @@ -467,11 +468,12 @@ public void AFractionIsDividedInTheStorageTypeRatherThanRoundedFirst() string source = result.GeneratedSources.Single().SourceText.ToString(); Assert.Contains("internal const double Factor = 5d / 9d;", source); - Assert.Contains("StorageLiteral.Divide(\"5\", \"9\", ConversionConstants.Factor)", source); + Assert.Contains("StorageLiteral.Divide(\"5\", \"9\")", source); } /// - /// A plain literal is written into the double constant exactly as the metadata spells it, and parsed into each storage type. + /// A plain literal is written into the double constant with a d suffix, parsed into each storage + /// type, and converted from the double at each read by a type that does not parse it. /// [TestMethod] public void ALiteralIsParsedIntoTheStorageType() @@ -482,8 +484,113 @@ public void ALiteralIsParsedIntoTheStorageType() string source = result.GeneratedSources.Single().SourceText.ToString(); - Assert.Contains("internal const double Factor = 0.3048;", source); - Assert.Contains("StorageLiteral.Parse(\"0.3048\", ConversionConstants.Factor)", source); + Assert.Contains("internal const double Factor = 0.3048d;", source); + Assert.Contains("private static readonly T? ParsedFactor = StorageLiteral.Parse(\"0.3048\");", source); + Assert.Contains("internal static T Factor => ParsedFactor ?? T.CreateChecked(ConversionConstants.Factor);", source); + } + + /// + /// SEM009 fires for a value a constant cannot hold: a literal or a fraction's + /// operand beyond its range, a quotient that overflows, or a non-zero value that rounds to zero. + /// + /// A value outside the finite range of . + [TestMethod] + [DataRow("1e400")] + [DataRow("-1e400")] + [DataRow("1e-400")] + [DataRow("1e400/2")] + [DataRow("2/1e400")] + [DataRow("1e300/1e-300")] + [DataRow("1e-300/1e300")] + public void Sem009_IsReportedForAConversionValueOutsideTheRangeOfDouble(string value) => + AssertReports(Run(ConversionsDocument(value), new ConversionsGenerator(), "conversions.json"), "SEM009"); + + /// + /// Every value SEM009 accepts becomes a constant that compiles. A long integer + /// literal without a suffix is an integer literal too large for any integer type (CS1021). + /// + /// A well-formed value. + [TestMethod] + [DataRow("100000000000000000000")] + [DataRow("1e308")] + [DataRow("0.3048")] + [DataRow("-273.15")] + [DataRow("1000.0")] + [DataRow("0")] + [DataRow("5/9")] + [DataRow("1e-6/60")] + public void TheDoubleConstantCompilesForEveryAcceptedValue(string value) + { + GeneratorRunResult result = Harness.Run( + new ConversionsGenerator(), + new Dictionary { ["conversions.json"] = ConversionsDocument(value) }); + + const string Declaration = "internal const double Factor = "; + string source = result.GeneratedSources.Single().SourceText.ToString(); + int start = source.IndexOf(Declaration, StringComparison.Ordinal); + Assert.IsGreaterThanOrEqualTo(0, start, $"No double constant was generated for {value}."); + + start += Declaration.Length; + List errors = CompileDoubleConstant(source[start..source.IndexOf(';', start)]); + + Assert.IsEmpty(errors, $"The constant for {value} does not compile: {string.Join("; ", errors)}"); + } + + /// + /// Compiles a constant declaration on its own. + /// + /// The constant's initializer, as generated. + /// The compiler errors, empty when the declaration compiles. + private static List CompileDoubleConstant(string expression) + { + CSharpCompilation compilation = CSharpCompilation.Create( + "ConstantProbe", + [CSharpSyntaxTree.ParseText($"internal static class Probe {{ internal const double Factor = {expression}; }}")], + [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)], + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + return + [ + .. compilation.GetDiagnostics() + .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .Select(static diagnostic => diagnostic.ToString()), + ]; + } + + /// + /// A unit declaring both a metric magnitude and a conversion factor scales by their product in its + /// generated factory, exactly as its IUnit implementation does, so the two cannot disagree. + /// + [TestMethod] + public void AUnitWithAMagnitudeAndAFactorAppliesBothInItsFactoryAndItsUnit() + { + const string UnitsDocument = + """ + { + "unitCategories": [ + { + "name": "Test", + "description": "A category.", + "units": [ + { "name": "Meter", "symbol": "m", "description": "Meter.", "system": "SIBase" }, + { "name": "Kilofoot", "symbol": "kft", "description": "A thousand feet.", "system": "Imperial", "magnitude": "Kilo", "conversionFactor": "FeetToMeters" } + ] + } + ] + } + """; + + Dictionary metadata = new() + { + ["dimensions.json"] = DimensionsDocument(availableUnits: "\"Meter\", \"Kilofoot\""), + ["units.json"] = UnitsDocument, + }; + + string quantities = string.Join("\n", Harness.Run(new QuantitiesGenerator(), metadata).GeneratedSources.Select(static source => source.SourceText.ToString())); + string units = string.Join("\n", Harness.Run(new UnitsGenerator(), metadata).GeneratedSources.Select(static source => source.SourceText.ToString())); + + Assert.Contains("(value * (MetricMagnitudes.Values.Kilo * Units.ConversionConstants.Values.FeetToMeters))", quantities); + Assert.Contains("=> MetricMagnitudes.Values.Kilo * ConversionConstants.Values.FeetToMeters;", units); } /// diff --git a/Semantics.Test/Quantities/IntegerStorageConversionTests.cs b/Semantics.Test/Quantities/IntegerStorageConversionTests.cs new file mode 100644 index 00000000..e9a44612 --- /dev/null +++ b/Semantics.Test/Quantities/IntegerStorageConversionTests.cs @@ -0,0 +1,109 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Semantics.Test.Quantities; + +using System; +using ktsu.Semantics.Quantities; +using ktsu.Semantics.Quantities.Units; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Integer storage converts exactly as it did before factors were materialised per storage type. +/// +/// +/// +/// Before 5.2.0 every generated factory multiplied by T.CreateChecked(double) at the call, so +/// a factor that did not fit an integer threw from that one factory +/// and every other factory kept working. 5.2.0 converted every factor for a storage type in one static +/// initializer, so a single factor too large for made every conversion for +/// throw . +/// +/// +/// Each expected value is the expression the 5.1 generated code evaluated, with the +/// constants it declared, so these tests compare against that code rather than against a restatement of it. +/// +/// +[TestClass] +public sealed class IntegerStorageConversionTests +{ + // The constants as the 5.1 ConversionConstants.g.cs declared them. + private const double FeetToMeters = 0.3048; + private const double MileToMeters = 1609.344; + private const double HourToSeconds = 3600; + private const double CelsiusToKelvinOffset = 273.15; + private const double FahrenheitScale = 0.5555555555555556; + private const double FahrenheitToKelvinOffset = 255.37222222222223; + private const double CurieToBecquerels = 3.7e10; + + /// + /// A factor too large for the storage type throws from its own factory, + /// and the factories around it still convert. + /// + [TestMethod] + public void AnOverflowingFactorFailsOnlyItsOwnFactory() + { + Assert.ThrowsExactly(static () => int.CreateChecked(CurieToBecquerels)); + Assert.ThrowsExactly(static () => RadioactiveActivity.FromCurie(1)); + Assert.ThrowsExactly(static () => RadioactiveActivity.FromCurie(1)); + + Assert.AreEqual(1 * int.CreateChecked(MetricMagnitudes.Kilo), Length.FromKilometer(1).Value); + Assert.AreEqual(5000 * int.CreateChecked(MileToMeters), Length.FromMile(5000).Value); + Assert.AreEqual(2 * int.CreateChecked(HourToSeconds), Duration.FromHour(2).Value); + } + + /// + /// A metric magnitude too large for the storage type fails when it is read, and the smaller ones still read. + /// + [TestMethod] + public void AnOverflowingMagnitudeFailsOnlyWhereItIsRead() + { + Assert.ThrowsExactly(static () => MetricMagnitudes.Values.Tera); + Assert.ThrowsExactly(static () => MetricMagnitudes.Values.Yotta); + + Assert.AreEqual(1000, MetricMagnitudes.Values.Kilo); + Assert.AreEqual(1000000000000L, MetricMagnitudes.Values.Tera); + Assert.AreEqual(1 * long.CreateChecked(MetricMagnitudes.Kilo), Length.FromKilometer(1L).Value); + } + + /// + /// factories give what the 5.1 expressions gave, including the truncation of a fractional factor. + /// + [TestMethod] + public void IntFactoriesMatchTheExpressionsTheyReplaced() + { + Assert.AreEqual(1000, Length.FromKilometer(1).Value); + Assert.AreEqual(36 * int.CreateChecked(MetricMagnitudes.Kilo), Length.FromKilometer(36).Value); + Assert.AreEqual(250 * int.CreateChecked(MetricMagnitudes.Centi), Length.FromCentimeter(250).Value); + Assert.AreEqual(10 * int.CreateChecked(FeetToMeters), Length.FromFoot(10).Value); + Assert.AreEqual(20 + int.CreateChecked(CelsiusToKelvinOffset), Temperature.FromCelsius(20).Value); + Assert.AreEqual( + (212 * int.CreateChecked(FahrenheitScale)) + int.CreateChecked(FahrenheitToKelvinOffset), + Temperature.FromFahrenheit(212).Value); + } + + /// + /// factories give what the 5.1 expressions gave, including a factor too large for . + /// + [TestMethod] + public void LongFactoriesMatchTheExpressionsTheyReplaced() + { + Assert.AreEqual(36L * long.CreateChecked(MetricMagnitudes.Kilo), Length.FromKilometer(36L).Value); + Assert.AreEqual(10L * long.CreateChecked(FeetToMeters), Length.FromFoot(10L).Value); + Assert.AreEqual(3L * long.CreateChecked(CurieToBecquerels), RadioactiveActivity.FromCurie(3L).Value); + Assert.AreEqual( + (212L * long.CreateChecked(FahrenheitScale)) + long.CreateChecked(FahrenheitToKelvinOffset), + Temperature.FromFahrenheit(212L).Value); + } + + /// + /// In(unit) on integer storage matches the 5.1 default IUnit.FromBase, which divided by the + /// truncated factor, including the a fractional scale truncated to zero gave. + /// + [TestMethod] + public void InMatchesTheDefaultFromBaseItReplaced() + { + Assert.AreEqual((5000 - int.CreateChecked(0d)) / int.CreateChecked(MetricMagnitudes.Kilo), Length.FromMeter(5000).In(Units.Kilometer)); + Assert.AreEqual((7200L - long.CreateChecked(0d)) / long.CreateChecked(HourToSeconds), Duration.FromSecond(7200L).In(Units.Hour)); + Assert.ThrowsExactly(static () => Temperature.FromKelvin(300).In(Units.Fahrenheit)); + } +} diff --git a/Semantics.Test/Quantities/ParseThrowingNumber.cs b/Semantics.Test/Quantities/ParseThrowingNumber.cs new file mode 100644 index 00000000..4dce2406 --- /dev/null +++ b/Semantics.Test/Quantities/ParseThrowingNumber.cs @@ -0,0 +1,170 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Semantics.Test.Quantities; + +using System; +using System.Globalization; +using System.Numerics; + +/// +/// A storage type backed by whose parse methods throw +/// instead of returning , as a custom numeric type may for a +/// value it does not support. +/// +/// The exception every parse method throws. +/// The value. +internal readonly record struct ParseThrowingNumber(double Inner) : INumber> + where TException : Exception, new() +{ + public static ParseThrowingNumber One => new(1d); + + public static int Radix => 2; + + public static ParseThrowingNumber Zero => new(0d); + + public static ParseThrowingNumber AdditiveIdentity => Zero; + + public static ParseThrowingNumber MultiplicativeIdentity => One; + + public static ParseThrowingNumber Abs(ParseThrowingNumber value) => new(Math.Abs(value.Inner)); + + public static bool IsCanonical(ParseThrowingNumber value) => true; + + public static bool IsComplexNumber(ParseThrowingNumber value) => false; + + public static bool IsEvenInteger(ParseThrowingNumber value) => double.IsEvenInteger(value.Inner); + + public static bool IsFinite(ParseThrowingNumber value) => double.IsFinite(value.Inner); + + public static bool IsImaginaryNumber(ParseThrowingNumber value) => false; + + public static bool IsInfinity(ParseThrowingNumber value) => double.IsInfinity(value.Inner); + + public static bool IsInteger(ParseThrowingNumber value) => double.IsInteger(value.Inner); + + public static bool IsNaN(ParseThrowingNumber value) => double.IsNaN(value.Inner); + + public static bool IsNegative(ParseThrowingNumber value) => double.IsNegative(value.Inner); + + public static bool IsNegativeInfinity(ParseThrowingNumber value) => double.IsNegativeInfinity(value.Inner); + + public static bool IsNormal(ParseThrowingNumber value) => double.IsNormal(value.Inner); + + public static bool IsOddInteger(ParseThrowingNumber value) => double.IsOddInteger(value.Inner); + + public static bool IsPositive(ParseThrowingNumber value) => double.IsPositive(value.Inner); + + public static bool IsPositiveInfinity(ParseThrowingNumber value) => double.IsPositiveInfinity(value.Inner); + + public static bool IsRealNumber(ParseThrowingNumber value) => double.IsRealNumber(value.Inner); + + public static bool IsSubnormal(ParseThrowingNumber value) => double.IsSubnormal(value.Inner); + + public static bool IsZero(ParseThrowingNumber value) => value.Inner == 0d; + + public static ParseThrowingNumber MaxMagnitude(ParseThrowingNumber x, ParseThrowingNumber y) => new(double.MaxMagnitude(x.Inner, y.Inner)); + + public static ParseThrowingNumber MaxMagnitudeNumber(ParseThrowingNumber x, ParseThrowingNumber y) => new(double.MaxMagnitudeNumber(x.Inner, y.Inner)); + + public static ParseThrowingNumber MinMagnitude(ParseThrowingNumber x, ParseThrowingNumber y) => new(double.MinMagnitude(x.Inner, y.Inner)); + + public static ParseThrowingNumber MinMagnitudeNumber(ParseThrowingNumber x, ParseThrowingNumber y) => new(double.MinMagnitudeNumber(x.Inner, y.Inner)); + + public static ParseThrowingNumber Parse(ReadOnlySpan s, NumberStyles style, IFormatProvider? provider) => throw new TException(); + + public static ParseThrowingNumber Parse(string s, NumberStyles style, IFormatProvider? provider) => throw new TException(); + + public static ParseThrowingNumber Parse(ReadOnlySpan s, IFormatProvider? provider) => throw new TException(); + + public static ParseThrowingNumber Parse(string s, IFormatProvider? provider) => throw new TException(); + + public static bool TryParse(ReadOnlySpan s, NumberStyles style, IFormatProvider? provider, out ParseThrowingNumber result) => throw new TException(); + + public static bool TryParse(string? s, NumberStyles style, IFormatProvider? provider, out ParseThrowingNumber result) => throw new TException(); + + public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out ParseThrowingNumber result) => throw new TException(); + + public static bool TryParse(string? s, IFormatProvider? provider, out ParseThrowingNumber result) => throw new TException(); + + public static bool TryConvertFromChecked(TOther value, out ParseThrowingNumber result) + where TOther : INumberBase + { + result = new(double.CreateChecked(value)); + return true; + } + + public static bool TryConvertFromSaturating(TOther value, out ParseThrowingNumber result) + where TOther : INumberBase + { + result = new(double.CreateSaturating(value)); + return true; + } + + public static bool TryConvertFromTruncating(TOther value, out ParseThrowingNumber result) + where TOther : INumberBase + { + result = new(double.CreateTruncating(value)); + return true; + } + + public static bool TryConvertToChecked(ParseThrowingNumber value, out TOther result) + where TOther : INumberBase + { + result = TOther.CreateChecked(value.Inner); + return true; + } + + public static bool TryConvertToSaturating(ParseThrowingNumber value, out TOther result) + where TOther : INumberBase + { + result = TOther.CreateSaturating(value.Inner); + return true; + } + + public static bool TryConvertToTruncating(ParseThrowingNumber value, out TOther result) + where TOther : INumberBase + { + result = TOther.CreateTruncating(value.Inner); + return true; + } + + public static ParseThrowingNumber operator +(ParseThrowingNumber value) => value; + + public static ParseThrowingNumber operator -(ParseThrowingNumber value) => new(-value.Inner); + + public static ParseThrowingNumber operator ++(ParseThrowingNumber value) => new(value.Inner + 1d); + + public static ParseThrowingNumber operator --(ParseThrowingNumber value) => new(value.Inner - 1d); + + public static ParseThrowingNumber operator +(ParseThrowingNumber left, ParseThrowingNumber right) => new(left.Inner + right.Inner); + + public static ParseThrowingNumber operator -(ParseThrowingNumber left, ParseThrowingNumber right) => new(left.Inner - right.Inner); + + public static ParseThrowingNumber operator *(ParseThrowingNumber left, ParseThrowingNumber right) => new(left.Inner * right.Inner); + + public static ParseThrowingNumber operator /(ParseThrowingNumber left, ParseThrowingNumber right) => new(left.Inner / right.Inner); + + public static ParseThrowingNumber operator %(ParseThrowingNumber left, ParseThrowingNumber right) => new(left.Inner % right.Inner); + + public static bool operator <(ParseThrowingNumber left, ParseThrowingNumber right) => left.Inner < right.Inner; + + public static bool operator >(ParseThrowingNumber left, ParseThrowingNumber right) => left.Inner > right.Inner; + + public static bool operator <=(ParseThrowingNumber left, ParseThrowingNumber right) => left.Inner <= right.Inner; + + public static bool operator >=(ParseThrowingNumber left, ParseThrowingNumber right) => left.Inner >= right.Inner; + + public int CompareTo(object? obj) => obj switch + { + null => 1, + ParseThrowingNumber other => CompareTo(other), + _ => throw new ArgumentException("The object is not a ParseThrowingNumber of the same exception type.", nameof(obj)), + }; + + public int CompareTo(ParseThrowingNumber other) => Inner.CompareTo(other.Inner); + + public string ToString(string? format, IFormatProvider? formatProvider) => Inner.ToString(format, formatProvider); + + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) + => Inner.TryFormat(destination, out charsWritten, format, provider); +} diff --git a/Semantics.Test/Quantities/PiLiteralTests.cs b/Semantics.Test/Quantities/PiLiteralTests.cs new file mode 100644 index 00000000..7bfcc5a2 --- /dev/null +++ b/Semantics.Test/Quantities/PiLiteralTests.cs @@ -0,0 +1,172 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Semantics.Test.Quantities; + +using System.Collections.Generic; +using System.Numerics; +using System.Text.Json; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Checks every long literal built on π in the metadata against π computed here, to its last digit. +/// +/// +/// The literals were once derived from one another, and an error at the 98th significant digit of +/// DegreeToRadians spread into every factor taken from it. A comparison against a hardcoded π of +/// 63 digits could not see that, so π is computed from Machin's formula in integer arithmetic instead. +/// +[TestClass] +public sealed class PiLiteralTests +{ + /// The number of decimal places π is computed to. + private const int Places = 220; + + /// Extra places computed and then discarded, so truncation in the series cannot reach the places kept. + private const int GuardPlaces = 20; + + /// The fewest significant digits a literal built on π must carry. + private const int MinimumSignificantDigits = 100; + + private static string MetadataDirectory => Path.Combine(AppContext.BaseDirectory, "GeneratorMetadata"); + + /// + /// π scaled by 10 to the power of , correct to within a unit in its last place. + /// + private static readonly BigInteger ScaledPi = ComputeScaledPi(); + + /// + /// The Machin computation agrees with the first 50 decimal places of π, which is what makes it a + /// trustworthy reference for the rest. + /// + [TestMethod] + public void MachinFormulaReproducesTheKnownDigitsOfPi() + { + const string KnownDigits = "314159265358979323846264338327950288419716939937510"; + + Assert.StartsWith(KnownDigits, ScaledPi.ToString(System.Globalization.CultureInfo.InvariantCulture)); + } + + /// + /// A literal built on π matches π to its last written digit and carries at least + /// significant digits. + /// + /// The metadata file declaring the literal. + /// The factor or constant name. + /// Whether the value is / (π × ) rather than π × / . + /// The integer multiplier. + /// The integer divisor. + [TestMethod] + [DataRow("conversions.json", "DegreeToRadians", false, 1L, 180L)] + [DataRow("conversions.json", "GradianToRadians", false, 1L, 200L)] + [DataRow("conversions.json", "RevolutionToRadians", false, 2L, 1L)] + [DataRow("conversions.json", "RevolutionPerMinuteToRadianPerSecond", false, 1L, 30L)] + [DataRow("conversions.json", "FootLambertToCandelaPerSquareMeter", true, 100000000L, 9290304L)] + [DataRow("domains.json", "TwoPi", false, 2L, 1L)] + [DataRow("domains.json", "RadiansPerDegree", false, 1L, 180L)] + [DataRow("domains.json", "DegreesPerRadian", true, 180L, 1L)] + public void APiLiteralMatchesPiToItsLastDigit(string fileName, string name, bool inverse, long numerator, long denominator) + { + string literal = ReadLiteral(fileName, name); + (BigInteger mantissa, int scale) = SplitLiteral(literal); + string digits = mantissa.ToString(System.Globalization.CultureInfo.InvariantCulture); + + Assert.IsGreaterThanOrEqualTo(MinimumSignificantDigits, digits.Length, $"{name} has only {digits.Length} significant digits."); + Assert.IsLessThan(Places - GuardPlaces, scale, $"{name} has more places than this test computes π to."); + + BigInteger placeValue = BigInteger.Pow(10, scale); + BigInteger piPlaceValue = BigInteger.Pow(10, Places); + BigInteger expected = inverse + ? RoundedQuotient(numerator * placeValue * piPlaceValue, ScaledPi * denominator) + : RoundedQuotient(ScaledPi * numerator * placeValue, piPlaceValue * denominator); + + BigInteger error = BigInteger.Abs(mantissa - expected); + Assert.IsLessThanOrEqualTo(BigInteger.One, error, $"{name} is wrong before its last digit: {literal}"); + } + + /// + /// Computes π × 10^ from π = 16 arctan(1/5) − 4 arctan(1/239). + /// + /// The scaled value. + private static BigInteger ComputeScaledPi() + { + BigInteger unity = BigInteger.Pow(10, Places + GuardPlaces); + BigInteger pi = (16 * ArctanOfReciprocal(5, unity)) - (4 * ArctanOfReciprocal(239, unity)); + return pi / BigInteger.Pow(10, GuardPlaces); + } + + /// + /// Sums the Taylor series of arctan(1/) in fixed point. + /// + /// The reciprocal of the argument. + /// The fixed point representation of one. + /// arctan(1/) × , truncated term by term. + private static BigInteger ArctanOfReciprocal(int x, BigInteger unity) + { + BigInteger xSquared = new(x * x); + BigInteger power = unity / x; + BigInteger sum = power; + bool subtract = true; + + for (int n = 3; !power.IsZero; n += 2) + { + power /= xSquared; + BigInteger term = power / n; + sum = subtract ? sum - term : sum + term; + subtract = !subtract; + } + + return sum; + } + + /// + /// Divides and rounds to the nearest integer, for positive operands. + /// + /// The dividend. + /// The divisor. + /// The rounded quotient. + private static BigInteger RoundedQuotient(BigInteger dividend, BigInteger divisor) + => ((2 * dividend) + divisor) / (2 * divisor); + + /// + /// Splits a plain decimal literal into its digits and the number of places after the point. + /// + /// A literal such as "0.0174", with no sign or exponent. + /// The significand without leading zeros, and the scale. + private static (BigInteger Mantissa, int Scale) SplitLiteral(string literal) + { + int point = literal.IndexOf('.', StringComparison.Ordinal); + string digits = point < 0 ? literal : literal.Remove(point, 1); + int scale = point < 0 ? 0 : literal.Length - point - 1; + return (BigInteger.Parse(digits, System.Globalization.CultureInfo.InvariantCulture), scale); + } + + /// + /// Reads the value of a named entry from conversions.json or domains.json. + /// + /// The metadata file. + /// The entry name. + /// The value as written. + private static string ReadLiteral(string fileName, string name) + { + using JsonDocument document = JsonDocument.Parse(File.ReadAllText(Path.Combine(MetadataDirectory, fileName))); + (string groups, string entries) = fileName == "domains.json" ? ("domains", "constants") : ("conversions", "factors"); + + Dictionary values = []; + foreach (JsonElement group in document.RootElement.GetProperty(groups).EnumerateArray()) + { + // Not every domain declares constants. + if (!group.TryGetProperty(entries, out JsonElement members)) + { + continue; + } + + foreach (JsonElement entry in members.EnumerateArray()) + { + values[entry.GetProperty("name").GetString()!] = entry.GetProperty("value").GetString()!; + } + } + + Assert.IsTrue(values.TryGetValue(name, out string? literal), $"{fileName} declares no {name}."); + return literal!; + } +} diff --git a/Semantics.Test/Quantities/StorageConversionTests.cs b/Semantics.Test/Quantities/StorageConversionTests.cs index d8b6831d..b398c3cd 100644 --- a/Semantics.Test/Quantities/StorageConversionTests.cs +++ b/Semantics.Test/Quantities/StorageConversionTests.cs @@ -102,10 +102,20 @@ public void PressureAndPowerUseTheirExactDefinitions() AssertValue("101325", Pressure.FromTorr(Of("760")).Value, terminates: false); } + /// + /// A metric magnitude combined with a factor of more significant digits than a + /// conversion keeps is exact. + /// + /// + /// A power of ten alone cannot tell the two routes apart, because converting 1e-2 from + /// to already gives exactly 0.01. The 17 significant digits + /// of these factors are what the old route rounded to 15. + /// [TestMethod] - public void AMetricMagnitudeIsExact() + public void AMetricMagnitudeCombinedWithALongFactorIsExact() { - AssertValue("0.01", Length.FromCentimeter(T.One).Value, terminates: true); + AssertValue("1355.8179483314004", TorqueMagnitude.FromPoundFoot(Of("1000")).Value, terminates: true); + AssertValue("0.00000074569987158227022", Power.FromHorsepower(Of("0.000001")).In(Units.Kilowatt), terminates: true); AssertValue("36000", Length.FromKilometer(Of("36")).Value, terminates: true); } diff --git a/Semantics.Test/Quantities/StorageLiteralTests.cs b/Semantics.Test/Quantities/StorageLiteralTests.cs new file mode 100644 index 00000000..91f1fcef --- /dev/null +++ b/Semantics.Test/Quantities/StorageLiteralTests.cs @@ -0,0 +1,33 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Semantics.Test.Quantities; + +using System; +using ktsu.Semantics.Quantities; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// A storage type whose parse throws for NumberStyles.Float rather than returning +/// falls back to converting the constant. +/// +[TestClass] +public sealed class StorageLiteralTests +{ + [TestMethod] + public void AParseThatThrowsNotSupportedFallsBackToTheDoubleFactor() + { + ParseThrowingNumber foot = Length>.FromFoot(ParseThrowingNumber.One).Value; + ParseThrowingNumber kilometer = Length>.FromKilometer(ParseThrowingNumber.One).Value; + + Assert.AreEqual(new ParseThrowingNumber(0.3048), foot); + Assert.AreEqual(new ParseThrowingNumber(1000d), kilometer); + } + + [TestMethod] + public void AParseThatThrowsArgumentExceptionFallsBackToTheDoubleFactor() + { + ParseThrowingNumber knot = Speed>.FromKnot(ParseThrowingNumber.One).Value; + + Assert.AreEqual(new ParseThrowingNumber(1852d / 3600d), knot); + } +} diff --git a/Semantics.Test/Quantities/StorageMathTests.cs b/Semantics.Test/Quantities/StorageMathTests.cs index 7d13cacc..ee0e0a48 100644 --- a/Semantics.Test/Quantities/StorageMathTests.cs +++ b/Semantics.Test/Quantities/StorageMathTests.cs @@ -97,4 +97,35 @@ public void BigIntegerSettlesOnTheFloorOfTheRoot() Assert.AreEqual(tenToTheTwenty, StorageMath.Sqrt(tenToTheForty)); Assert.AreEqual(tenToTheTwenty, StorageMath.Sqrt(tenToTheForty + BigInteger.One)); } + + /// + /// A too large for still gets its exact root, where a + /// start from the value itself used to run out of Newton steps and return a far larger number. + /// + [TestMethod] + public void BigIntegerBeyondTheRangeOfDoubleHasItsExactRoot() + { + BigInteger tenToTheTwoHundred = BigInteger.Pow(10, 200); + + Assert.AreEqual(BigInteger.Pow(2, 1024), StorageMath.Sqrt(BigInteger.Pow(2, 2048))); + Assert.AreEqual(tenToTheTwoHundred, StorageMath.Sqrt(BigInteger.Pow(10, 400))); + Assert.AreEqual(tenToTheTwoHundred, StorageMath.Sqrt(BigInteger.Pow(10, 400) + BigInteger.One)); + Assert.AreEqual(tenToTheTwoHundred - BigInteger.One, StorageMath.Sqrt(BigInteger.Pow(10, 400) - BigInteger.One)); + } + + [TestMethod] + public void BigIntegerZeroIsZero() => Assert.AreEqual(BigInteger.Zero, StorageMath.Sqrt(BigInteger.Zero)); + + /// + /// The smallest positive has an exact root, and the largest has one correct to the last place the type holds. + /// + [TestMethod] + public void DecimalExtremesHaveTheirRoots() + { + // The root of decimal.MaxValue is 281474976710655.9999999999999982236..., within 2e-15 of this. + const decimal RootOfMaxValue = 281474976710656m; + + Assert.AreEqual(0.00000000000001m, StorageMath.Sqrt(0.0000000000000000000000000001m)); + Assert.IsLessThanOrEqualTo(0.00000000000002m, Math.Abs(StorageMath.Sqrt(decimal.MaxValue) - RootOfMaxValue)); + } } diff --git a/docs/physics-generator.md b/docs/physics-generator.md index 2865a9a2..88446fdc 100644 --- a/docs/physics-generator.md +++ b/docs/physics-generator.md @@ -170,25 +170,50 @@ Each factor in `conversions.json` has a `value` in one of two forms: | Decimal literal | `"0.3048"`, `"1e-10"`, `"745.69987158227022"` | A factor with a terminating decimal definition, or a long literal for one built on π. | | Fraction of two decimal literals | `"5/9"`, `"20265/152"` | A repeating ratio. Write the exact fraction rather than its rounded decimal. | -`ConversionsGenerator` emits each factor twice. The `double` constant (`5d / 9d` for a fraction) -backs the public `IUnit.ToBaseFactor` and `ToBaseOffset` properties. The `Values` holder calls -`StorageLiteral.Parse` or `StorageLiteral.Divide` once per closed generic type, and the -generated factories and `In(unit)` read that. So each storage type gets the factor at its own -precision: `double` gets the correctly rounded value, and `decimal` gets 28 significant digits -where `T.CreateChecked(double)` used to leave it 15. An integer storage type, or one that cannot -parse the literal, falls back to converting the `double` constant, which is what every type did -before. +Either form must be something a `double` holds: SEM009 rejects a literal, operand, or quotient beyond +the range of `double`, and a non-zero value that rounds to zero in it. + +Compute a literal built on π from π itself, correctly rounded to 150 significant digits, rather than +from another literal. The π literals in `conversions.json` and `domains.json` are written that way, and +`PiLiteralTests` checks each one to its last digit against π computed by Machin's formula. + +`ConversionsGenerator` emits each factor twice. The `double` constant (`0.3048d`, or `5d / 9d` for a +fraction) backs the public `IUnit.ToBaseFactor` and `ToBaseOffset` properties. Every operand carries +the `d` suffix, because a literal such as `100000000000000000000` is otherwise an integer literal the +compiler rejects. The `Values` holder parses the value with `StorageLiteral.Parse` or +`StorageLiteral.Divide` once per closed generic type into a private nullable field, and the +generated factories and `In(unit)` read it through a property. So each storage type gets the factor at +its own precision: `double` gets the correctly rounded value, and `decimal` gets 28 significant digits +where `T.CreateChecked(double)` used to leave it 15. + +An integer storage type, or one that cannot parse the literal, leaves the field null, and the property +converts the `double` constant each time it is read, which is what every type did before 5.2.0. The +conversion stays out of the static initializer so that each value succeeds or fails on its own: a +factor too large for the type throws `OverflowException` from the factory that uses it, and the rest +keep working. 5.2.0 did convert in the initializer, and one overflow (`CurieToBecquerels` for `int`, +`Yotta` for `long`) made every factor or magnitude for that type throw `TypeInitializationException`. +A parse that throws `NotSupportedException` or `ArgumentException` for `NumberStyles.Float` counts as +one that cannot parse the literal. `MagnitudesGenerator` does the same for the SI prefixes, so `1e-2` reaches `decimal` as exactly a hundredth. +Writing a factor as its exact definition can move its `double` constant to the adjacent representable +value, because a rounded 17-digit literal is not always the `double` nearest the true value. 5.2.0 +moved two constants this way, each closer to the true value than before: `PsiToPascals` from +6894.757293168361 to 6894.757293168362, and `RevolutionPerMinuteToRadianPerSecond` from +0.10471975511965977 to 0.10471975511965978. The public `IUnit.ToBaseFactor` of `Psi` and +`RevolutionPerMinute` moved with them. Every other constant kept its value. + A fraction is not always the better choice. `PsiToPascals` is written as a 150-digit literal rather than `8896443230521/1290320000`, because `float` storage rounds a numerator that large before dividing and lands further from the true value than parsing the literal does. Vector `Length()` and `Distance()` take their root through `StorageMath.Sqrt`. The binary floating point and integer primitives keep the `Math.Sqrt` round trip they always had. Other types refine -that root with Newton steps in their own arithmetic. +that root with Newton steps in their own arithmetic. A value a `double` cannot hold is scaled by powers +of four into [1, 4) for the seed, and a root that does not settle throws `ArithmeticException` rather +than returning an estimate. ## Validation, diagnostics, and gotchas @@ -205,7 +230,7 @@ that root with Newton steps in their own arithmetic. | SEM006 | A metadata file a generator declared that was not supplied as an `AdditionalFile`. | | SEM007 | A metadata file that could not be parsed. | | SEM008 | A relationship whose declared result does not follow from the dimensions of its operands, or whose signed value cannot land in a magnitude result. No operator is generated for it. | - | SEM009 | A `conversions.json` factor whose `value` is neither a decimal literal nor a fraction of two with a non-zero denominator. An error, and no constant is generated for it. | + | SEM009 | A `conversions.json` factor whose `value` is neither a decimal literal nor a fraction of two with a non-zero denominator, or that a `double` cannot hold. An error, and no constant is generated for it. | Adding one means adding it to `SemanticsDiagnostics` and to `AnalyzerReleases.Unshipped.md`; `AnalyzerReleaseTrackingTests` fails if the second step is forgotten. `GeneratorDiagnosticTests` proves each one still fires on the input it is meant to catch. - `availableUnits` order matters: the first entry is treated as the SI base unit by `UnitsGenerator`. From 5a1c87968a0f93441e5373aa9a075e4bda1b9d48 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 14 Sep 2026 12:52:11 +0000 Subject: [PATCH 2/2] [patch] Address code quality findings on the storage conversion fixes Both CodeQL findings are exact-zero comparisons, and both are kept exact rather than turned into a tolerance: - ConversionValue.IsHeldByDouble tested `value != 0d` to catch a non-zero literal that underflowed. It now asks whether the magnitude is above zero, which is the same test once NaN is already excluded. The framework predicate CodeQL suggests, `double.IsZero`, is a static abstract on INumberBase and does not exist on netstandard2.0, which this generator targets. - ParseThrowingNumber.IsZero now routes through INumberBase.IsZero via a constrained type parameter, which is the only way to reach a static abstract interface member. Also clears the Sonar findings in the code this branch adds, none of which change any generated output: - PiLiteralTests.ArctanOfReciprocal advanced `n` in the incrementer while testing `power` in the condition (S1994). It is a while loop now. - ConversionValue.IsDecimalLiteral was over the cognitive complexity limit (S3776), split into SkipSign, TryPassFraction and TryPassExponent. - The fourth `"internal"` literal in ConversionsGenerator tripped S1192. Added Emit.Internal and used it across the three generators that spell the modifier. Solution builds with no warnings, all 1237 tests in Semantics.Test pass, and regenerating the committed generator output leaves no diff. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01MLjFwwiUGZ6i5yZ7rj1WAU --- Semantics.SourceGenerators/Emit.cs | 6 ++ .../Generators/ConversionValue.cs | 81 ++++++++++++------- .../Generators/ConversionsGenerator.cs | 8 +- .../Generators/MagnitudesGenerator.cs | 4 +- .../Generators/PhysicalConstantsGenerator.cs | 2 +- .../Quantities/ParseThrowingNumber.cs | 9 ++- Semantics.Test/Quantities/PiLiteralTests.cs | 4 +- 7 files changed, 78 insertions(+), 36 deletions(-) diff --git a/Semantics.SourceGenerators/Emit.cs b/Semantics.SourceGenerators/Emit.cs index 0521b58b..4cc8e151 100644 --- a/Semantics.SourceGenerators/Emit.cs +++ b/Semantics.SourceGenerators/Emit.cs @@ -18,6 +18,12 @@ internal static class Emit /// The public modifier. internal const string Public = CSharpKeywords.Public; + /// + /// The internal modifier. Spelled out here rather than taken from CSharpKeywords, + /// which does not carry it; the generators need one name for it rather than a literal per use. + /// + internal const string Internal = "internal"; + /// The static modifier. internal const string Static = CSharpKeywords.Static; diff --git a/Semantics.SourceGenerators/Generators/ConversionValue.cs b/Semantics.SourceGenerators/Generators/ConversionValue.cs index e024ff27..682f6ef5 100644 --- a/Semantics.SourceGenerators/Generators/ConversionValue.cs +++ b/Semantics.SourceGenerators/Generators/ConversionValue.cs @@ -2,6 +2,7 @@ namespace Semantics.SourceGenerators; +using System; using System.Globalization; /// @@ -116,8 +117,13 @@ private static bool TryReadDouble(string literal, out double value) /// The value as a . /// Whether the exact value is zero. /// when is usable as the constant. + /// + /// The underflow test is written as a magnitude above zero rather than as an inequality against zero: + /// it is the same test once NaN is excluded, and comparing a for equality is + /// flagged wherever it appears, however exact the intent. + /// private static bool IsHeldByDouble(double value, bool isZero) - => !double.IsInfinity(value) && !double.IsNaN(value) && (isZero || value != 0d); + => !double.IsInfinity(value) && !double.IsNaN(value) && (isZero || Math.Abs(value) > 0d); /// /// Reports whether is a decimal literal that is valid both in C# and in @@ -129,44 +135,65 @@ private static bool IsHeldByDouble(double value, bool isZero) private static bool IsDecimalLiteral(string text) { int index = 0; - if (index < text.Length && (text[index] == '+' || text[index] == '-')) - { - index++; - } + SkipSign(text, ref index); + + int digits = CountDigits(text, ref index); - int integerDigits = CountDigits(text, ref index); - int fractionDigits = 0; + return TryPassFraction(text, ref index, ref digits) + && digits != 0 + && TryPassExponent(text, ref index) + && index == text.Length; + } - if (index < text.Length && text[index] == '.') + /// + /// Advances past an optional leading sign. + /// + /// The text being scanned. + /// The position to start at, moved past the sign when there is one. + private static void SkipSign(string text, ref int index) + { + if (index < text.Length && (text[index] == '+' || text[index] == '-')) { index++; - fractionDigits = CountDigits(text, ref index); - if (fractionDigits == 0) - { - return false; - } } + } - if (integerDigits + fractionDigits == 0) + /// + /// Advances past an optional fractional part, counting its digits. + /// + /// The text being scanned. + /// The position to start at, moved past the fractional part when there is one. + /// The running digit count, increased by the digits after the point. + /// when a point is present with no digits after it. + private static bool TryPassFraction(string text, ref int index, ref int digits) + { + if (index == text.Length || text[index] != '.') { - return false; + return true; } - if (index < text.Length && (text[index] == 'e' || text[index] == 'E')) - { - index++; - if (index < text.Length && (text[index] == '+' || text[index] == '-')) - { - index++; - } + index++; + int fractionDigits = CountDigits(text, ref index); + digits += fractionDigits; + return fractionDigits != 0; + } - if (CountDigits(text, ref index) == 0) - { - return false; - } + /// + /// Advances past an optional exponent. + /// + /// The text being scanned. + /// The position to start at, moved past the exponent when there is one. + /// when an exponent marker is present with no digits after it. + private static bool TryPassExponent(string text, ref int index) + { + if (index == text.Length || (text[index] != 'e' && text[index] != 'E')) + { + return true; } - return index == text.Length; + index++; + SkipSign(text, ref index); + return CountDigits(text, ref index) != 0; } /// diff --git a/Semantics.SourceGenerators/Generators/ConversionsGenerator.cs b/Semantics.SourceGenerators/Generators/ConversionsGenerator.cs index 83347baa..bd605bfc 100644 --- a/Semantics.SourceGenerators/Generators/ConversionsGenerator.cs +++ b/Semantics.SourceGenerators/Generators/ConversionsGenerator.cs @@ -83,7 +83,7 @@ protected override void Generate(SourceProductionContext context, ConversionsMet Kind = TypeKind.Class, Keywords = { - "internal", + Emit.Internal, Emit.Static, }, Name = "ConversionConstants", @@ -100,7 +100,7 @@ protected override void Generate(SourceProductionContext context, ConversionsMet Kind = TypeKind.Class, Keywords = { - "internal", + Emit.Internal, Emit.Static, }, Name = $"{HolderName}", @@ -126,7 +126,7 @@ protected override void Generate(SourceProductionContext context, ConversionsMet }, Keywords = { - "internal", + Emit.Internal, "const", "double", }, @@ -143,7 +143,7 @@ protected override void Generate(SourceProductionContext context, ConversionsMet }, Keywords = { - "internal", + Emit.Internal, Emit.Static, "T", }, diff --git a/Semantics.SourceGenerators/Generators/MagnitudesGenerator.cs b/Semantics.SourceGenerators/Generators/MagnitudesGenerator.cs index 8eb8f08d..12cc2568 100644 --- a/Semantics.SourceGenerators/Generators/MagnitudesGenerator.cs +++ b/Semantics.SourceGenerators/Generators/MagnitudesGenerator.cs @@ -76,7 +76,7 @@ protected override void Generate(SourceProductionContext context, MagnitudesMeta Emit.SummaryClose, }, Kind = TypeKind.Class, - Keywords = {"internal", Emit.Static}, + Keywords = {Emit.Internal, Emit.Static}, Name = "Values", Constraints = {"where T : struct, INumber"}, }; @@ -103,7 +103,7 @@ protected override void Generate(SourceProductionContext context, MagnitudesMeta holderClass.Members.Add(new FieldTemplate() { Comments = {comment}, - Keywords = {"internal", Emit.Static, "T"}, + Keywords = {Emit.Internal, Emit.Static, "T"}, Name = $"{magnitude.Name} => {ParsedPrefix}{magnitude.Name} ?? T.CreateChecked(MetricMagnitudes.{magnitude.Name})", }); diff --git a/Semantics.SourceGenerators/Generators/PhysicalConstantsGenerator.cs b/Semantics.SourceGenerators/Generators/PhysicalConstantsGenerator.cs index f770e96f..6141b865 100644 --- a/Semantics.SourceGenerators/Generators/PhysicalConstantsGenerator.cs +++ b/Semantics.SourceGenerators/Generators/PhysicalConstantsGenerator.cs @@ -128,7 +128,7 @@ protected override void Generate(SourceProductionContext context, DomainsMetadat holderClass.Members.Add(new FieldTemplate() { Comments = {$"/// {constant.Description}"}, - Keywords = {"internal", Emit.Static, "readonly", "T"}, + Keywords = {Emit.Internal, Emit.Static, "readonly", "T"}, Name = constant.Name, DefaultValue = $"T.Parse(\"{constant.Value}\", {ParseStyles}, CultureInfo.InvariantCulture)", }); diff --git a/Semantics.Test/Quantities/ParseThrowingNumber.cs b/Semantics.Test/Quantities/ParseThrowingNumber.cs index 4dce2406..06fc883d 100644 --- a/Semantics.Test/Quantities/ParseThrowingNumber.cs +++ b/Semantics.Test/Quantities/ParseThrowingNumber.cs @@ -26,6 +26,13 @@ internal readonly record struct ParseThrowingNumber(double Inner) : public static ParseThrowingNumber MultiplicativeIdentity => One; + /// + /// Invokes on the backing type. A static abstract interface + /// member is reachable only through a constrained type parameter, so the framework's zero predicate + /// needs this hop; the alternative is comparing a for equality. + /// + private static bool IsZeroValue(T value) where T : INumberBase => T.IsZero(value); + public static ParseThrowingNumber Abs(ParseThrowingNumber value) => new(Math.Abs(value.Inner)); public static bool IsCanonical(ParseThrowingNumber value) => true; @@ -60,7 +67,7 @@ internal readonly record struct ParseThrowingNumber(double Inner) : public static bool IsSubnormal(ParseThrowingNumber value) => double.IsSubnormal(value.Inner); - public static bool IsZero(ParseThrowingNumber value) => value.Inner == 0d; + public static bool IsZero(ParseThrowingNumber value) => IsZeroValue(value.Inner); public static ParseThrowingNumber MaxMagnitude(ParseThrowingNumber x, ParseThrowingNumber y) => new(double.MaxMagnitude(x.Inner, y.Inner)); diff --git a/Semantics.Test/Quantities/PiLiteralTests.cs b/Semantics.Test/Quantities/PiLiteralTests.cs index 7bfcc5a2..e19c0172 100644 --- a/Semantics.Test/Quantities/PiLiteralTests.cs +++ b/Semantics.Test/Quantities/PiLiteralTests.cs @@ -106,13 +106,15 @@ private static BigInteger ArctanOfReciprocal(int x, BigInteger unity) BigInteger power = unity / x; BigInteger sum = power; bool subtract = true; + int n = 3; - for (int n = 3; !power.IsZero; n += 2) + while (!power.IsZero) { power /= xSquared; BigInteger term = power / n; sum = subtract ? sum - term : sum + term; subtract = !subtract; + n += 2; } return sum;