From 8b48f2e284ca5d22f4aee693730b111c76844274 Mon Sep 17 00:00:00 2001 From: James McMahon Date: Sun, 25 Oct 2026 12:28:41 +0000 Subject: [PATCH] fix: remove affine conversions and add Rankine --- README.md | 4 +-- .../ImperialConversionsTests.cs | 6 ++-- .../ImperialSymbols.cs | 4 +-- .../ImperialUnits.cs | 3 +- .../Packaging/PackagedGeneratorTests.cs | 2 +- .../Generators/UnitProviderGenerationTests.cs | 29 ---------------- .../Generators/UnitConversionSourceEmitter.cs | 34 +++++++++---------- .../Generators/UnitProviderReader.cs | 12 +------ .../Declarations/UnitConversionAttribute.cs | 9 ----- .../Units/UnitDefinition.cs | 4 +-- 10 files changed, 28 insertions(+), 79 deletions(-) delete mode 100644 src/DimensionalQuantities/Units/Declarations/UnitConversionAttribute.cs diff --git a/README.md b/README.md index 741a819..a7c86bb 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ using DimensionalQuantities.Extensions.Metric; Metric depends on the matching core version, so referencing Metric also supplies core and its compiler tooling. Quantity names and dimensional arithmetic are available with core alone. -Add `DimensionalQuantities.Extensions.Imperial` for British and US customary conversions. It includes feet, miles, stones, pounds-force, US and Imperial liquid measures, Fahrenheit, mechanical horsepower and the International Table British thermal unit. Import `DimensionalQuantities.Extensions.Imperial` to use its helpers: +Add `DimensionalQuantities.Extensions.Imperial` for British and US customary conversions. It includes feet, miles, stones, pounds-force, US and Imperial liquid measures, Rankine, mechanical horsepower and the International Table British thermal unit. Import `DimensionalQuantities.Extensions.Imperial` to use its helpers: ```csharp Length depth = Length.FromFeet(6); @@ -246,7 +246,7 @@ internal static class CustomSymbols Consumers can then use `Length.FromFeet(6)` and `length.AsFeet()` with `using Example.Units;`. The Pascal declaration associates the same unit with Pressure, Stress and Young’s modulus; their shared underlying type receives one set of conversion helpers. -`Scale` converts each atomic unit to SI. Ordered `UnitFactor` attributes describe compound units, with `Power`, `PowerDenominator` and `PrefixExponent` controlling each factor. An optional `[UnitConversion(Offset = ...)]` on the unit declaration supplies an offset added after scaling into SI and subtracted before converting back; it applies to every associated quantity. A provider can opt into named SI prefixes and parameterised prefix conversions with `GenerateSiPrefixes` and `GeneratePrefixConversions`; both default to false. `AdditionalUnit` references another factor declaration for an alternative conversion. +`Scale` converts each atomic unit to SI. Ordered `UnitFactor` attributes describe compound units, with `Power`, `PowerDenominator` and `PrefixExponent` controlling each factor. A provider can opt into named SI prefixes and parameterised prefix conversions with `GenerateSiPrefixes` and `GeneratePrefixConversions`; both default to false. `AdditionalUnit` references another factor declaration for an alternative conversion. ### Current scope diff --git a/src/DimensionalQuantities.Extensions.Imperial.Tests/ImperialConversionsTests.cs b/src/DimensionalQuantities.Extensions.Imperial.Tests/ImperialConversionsTests.cs index d61d0e5..b8b2989 100644 --- a/src/DimensionalQuantities.Extensions.Imperial.Tests/ImperialConversionsTests.cs +++ b/src/DimensionalQuantities.Extensions.Imperial.Tests/ImperialConversionsTests.cs @@ -70,11 +70,11 @@ public void EnergyAndPowerUseExplicitBritishThermalUnitDefinition() } [Fact] - public void FahrenheitUsesAffineConversion() + public void RankineUsesFiveNinthsKelvinPerDegree() { Assert.Multiple( - () => Assert.Equal(273.15, Temperature.FromFahrenheit(32).AsKelvin(), 10), - () => Assert.Equal(212, Temperature.FromKelvin(373.15).AsFahrenheit(), 10)); + () => Assert.Equal(273.15, Temperature.FromRankine(491.67).AsKelvin(), 10), + () => Assert.Equal(491.67, Temperature.FromKelvin(273.15).AsRankine(), 10)); } [Fact] diff --git a/src/DimensionalQuantities.Extensions.Imperial/ImperialSymbols.cs b/src/DimensionalQuantities.Extensions.Imperial/ImperialSymbols.cs index bd468b0..7ccb032 100644 --- a/src/DimensionalQuantities.Extensions.Imperial/ImperialSymbols.cs +++ b/src/DimensionalQuantities.Extensions.Imperial/ImperialSymbols.cs @@ -54,6 +54,6 @@ internal sealed class Psi { } internal sealed class BritishThermalUnitIT { } [UnitSymbol("MechanicalHorsepower", "MechanicalHorsepower", "hp", Scale = 745.69987158227022)] internal sealed class MechanicalHorsepower { } - [UnitSymbol("Fahrenheit", "Fahrenheit", "°F", Scale = 5d / 9d)] - internal sealed class Fahrenheit { } + [UnitSymbol("Rankine", "Rankine", "°R", Scale = 5d / 9d)] + internal sealed class Rankine { } } diff --git a/src/DimensionalQuantities.Extensions.Imperial/ImperialUnits.cs b/src/DimensionalQuantities.Extensions.Imperial/ImperialUnits.cs index 5f2aa61..ae44321 100644 --- a/src/DimensionalQuantities.Extensions.Imperial/ImperialUnits.cs +++ b/src/DimensionalQuantities.Extensions.Imperial/ImperialUnits.cs @@ -118,7 +118,6 @@ private sealed class PowerUnit { } private sealed class BritishThermalUnitsITPerHourUnit { } [QuantityUnit(typeof(Temperature))] - [UnitConversion(Offset = 459.67d * 5d / 9d)] - [UnitFactor(typeof(ImperialSymbols.Fahrenheit))] + [UnitFactor(typeof(ImperialSymbols.Rankine))] private sealed class TemperatureUnit { } } diff --git a/src/DimensionalQuantities.IntegrationTests/Packaging/PackagedGeneratorTests.cs b/src/DimensionalQuantities.IntegrationTests/Packaging/PackagedGeneratorTests.cs index 8fe1cd2..db69ada 100644 --- a/src/DimensionalQuantities.IntegrationTests/Packaging/PackagedGeneratorTests.cs +++ b/src/DimensionalQuantities.IntegrationTests/Packaging/PackagedGeneratorTests.cs @@ -86,7 +86,7 @@ public void ImperialPackageProvidesConversionsThroughCoreDependency() public static class Consumer { public static double ConvertLength() => Length.FromFeet(6).AsSiBaseUnit(); - public static double ConvertTemperature() => Temperature.FromFahrenheit(32).AsSiBaseUnit(); + public static double ConvertTemperature() => Temperature.FromRankine(491.67).AsSiBaseUnit(); } """, imperial); Build(consumer); diff --git a/src/DimensionalQuantities.Roslyn.Tests/Generators/UnitProviderGenerationTests.cs b/src/DimensionalQuantities.Roslyn.Tests/Generators/UnitProviderGenerationTests.cs index 7030cfa..5bfe6e5 100644 --- a/src/DimensionalQuantities.Roslyn.Tests/Generators/UnitProviderGenerationTests.cs +++ b/src/DimensionalQuantities.Roslyn.Tests/Generators/UnitProviderGenerationTests.cs @@ -190,35 +190,6 @@ public static double[] Values() => Assert.Equal([2d, 3d], Evaluate(compilation, "Independent.Units.Consumer")); } - [Fact] - public void AffineConversionsApplyScaleAndOffsetInBothDirections() - { - const string source = """ - using DimensionalQuantities.Units.Declarations; - namespace Independent.Units; - [UnitProvider] - internal static class CustomUnits - { - [QuantityUnit(typeof(Temperature))] - [UnitConversion(Offset = 255.3722222222222)] - [UnitFactor(typeof(Fahrenheit))] - private sealed class TemperatureUnit; - [UnitSymbol("Fahrenheit", "Fahrenheit", "°F", Scale = 5d / 9d)] - private sealed class Fahrenheit; - } - public static class Consumer - { - public static double[] Values() => [Temperature.FromFahrenheit(32).AsKelvin(), Temperature.FromKelvin(373.15).AsFahrenheit()]; - } - """; - (Compilation compilation, ImmutableArray diagnostics) = Generate(source, ""); - Assert.Empty(diagnostics); - double[] result = Evaluate(compilation, "Independent.Units.Consumer"); - Assert.Multiple( - () => Assert.Equal(273.15, result[0], 10), - () => Assert.Equal(212, result[1], 10)); - } - [Fact] public void ProviderFactoriesCanBeUsedInArithmeticInTheirOwnAssembly() { diff --git a/src/DimensionalQuantities.Roslyn/Generators/UnitConversionSourceEmitter.cs b/src/DimensionalQuantities.Roslyn/Generators/UnitConversionSourceEmitter.cs index 4c5086f..bd78079 100644 --- a/src/DimensionalQuantities.Roslyn/Generators/UnitConversionSourceEmitter.cs +++ b/src/DimensionalQuantities.Roslyn/Generators/UnitConversionSourceEmitter.cs @@ -29,11 +29,11 @@ private static string RenderQuantity(UnitConversionSpec quantity, int[] prefixes UnitExpression canonicalExpression = quantity.Units[0].Expression; ConversionDefinition generic = new("FromPrefixedUnits", "AsPrefixedUnits", GetTermParameters(canonicalExpression), GetTermScaleExponent(canonicalExpression), - canonicalExpression.GetScale(), quantity.Units[0].Offset); + canonicalExpression.GetScale()); ConversionDefinition[] named = quantity.IncludeNamed ? CollectConversions(quantity.Units, quantity.GenerateSiPrefixes ? prefixes : []) .Select(conversion => new ConversionDefinition($"From{conversion.Key}", $"As{conversion.Key}", [], - FormatExponent(conversion.Value.Exponent), conversion.Value.Scale, conversion.Value.Offset)) + FormatExponent(conversion.Value.Exponent), conversion.Value.Scale)) .ToArray() : []; ConversionDefinition[] conversions = quantity.GeneratePrefixConversions ? [generic, .. named] : named; @@ -66,46 +66,46 @@ private static string RenderQuantity(UnitConversionSpec quantity, int[] prefixes """; } - private static IReadOnlyDictionary CollectConversions( + private static IReadOnlyDictionary CollectConversions( IReadOnlyList units, int[] generatedPrefixes) { - Dictionary conversions = new(StringComparer.Ordinal); - foreach ((string name, Rational exponent, double scale, double offset) in units.SelectMany(unit => GetConversions(unit, generatedPrefixes))) + Dictionary conversions = new(StringComparer.Ordinal); + foreach ((string name, Rational exponent, double scale) in units.SelectMany(unit => GetConversions(unit, generatedPrefixes))) { - if (conversions.TryGetValue(name, out (Rational Exponent, double Scale, double Offset) existing) && existing != (exponent, scale, offset)) + if (conversions.TryGetValue(name, out (Rational Exponent, double Scale) existing) && existing != (exponent, scale)) { throw new InvalidOperationException($"Unit {name} has conflicting SI scales."); } if (!conversions.ContainsKey(name)) { - conversions.Add(name, (exponent, scale, offset)); + conversions.Add(name, (exponent, scale)); } } return conversions; } - private static IEnumerable<(string Name, Rational Exponent, double Scale, double Offset)> GetConversions( + private static IEnumerable<(string Name, Rational Exponent, double Scale)> GetConversions( UnitDefinition unit, int[] generatedPrefixes) => GetStandardConversions(unit, generatedPrefixes).Concat(unit.AdditionalConversions - .Select(expression => (expression.GetName(unit.Expression), expression.GetSiScaleExponent(), expression.GetScale(), unit.Offset))); + .Select(expression => (expression.GetName(unit.Expression), expression.GetSiScaleExponent(), expression.GetScale()))); - private static IEnumerable<(string Name, Rational Exponent, double Scale, double Offset)> GetStandardConversions( + private static IEnumerable<(string Name, Rational Exponent, double Scale)> GetStandardConversions( UnitDefinition unit, int[] generatedPrefixes) { UnitExpression expression = unit.Expression; int firstNumerator = Array.FindIndex(expression.Terms, term => term.Power > Rational.Zero); if (firstNumerator < 0 || generatedPrefixes.Length == 0) { - yield return (expression.GetName(), expression.GetSiScaleExponent(), expression.GetScale(), unit.Offset); + yield return (expression.GetName(), expression.GetSiScaleExponent(), expression.GetScale()); yield break; } foreach (int prefix in generatedPrefixes.Prepend(0)) { UnitExpression prefixed = expression.WithPrefix(firstNumerator, prefix); - yield return (prefixed.GetName(expression, firstNumerator), prefixed.GetSiScaleExponent(), prefixed.GetScale(), unit.Offset); + yield return (prefixed.GetName(expression, firstNumerator), prefixed.GetSiScaleExponent(), prefixed.GetScale()); } } @@ -139,30 +139,30 @@ private static string FormatTermScaleExponent(UnitTerm term) private static string RenderFactory(string quantityName, ConversionDefinition conversion) { string parameters = string.Concat(conversion.PrefixParameters.Select(parameter => $", int {parameter.Name} = {parameter.DefaultExponent.ToString(CultureInfo.InvariantCulture)}")); - if (conversion.Scale == 1 && conversion.Offset == 0) + if (conversion.Scale == 1) { return $"public static {quantityName} {conversion.FactoryName}(double value{parameters}) => {quantityName}.FromArbitrarySiComponents(value, overallScaleExponent: {conversion.ScaleExponentExpression});"; } - return $"public static {quantityName} {conversion.FactoryName}(double value{parameters}) => {quantityName}.FromSiBaseUnit(value * {FormatDouble(conversion.Scale)} * global::System.Math.Pow(10, {conversion.ScaleExponentExpression}) + {FormatDouble(conversion.Offset)});"; + return $"public static {quantityName} {conversion.FactoryName}(double value{parameters}) => {quantityName}.FromSiBaseUnit(value * {FormatDouble(conversion.Scale)} * global::System.Math.Pow(10, {conversion.ScaleExponentExpression}));"; } private static string RenderAccessor(string variableName, ConversionDefinition conversion) { string parameters = string.Join(", ", conversion.PrefixParameters.Select(parameter => $"int {parameter.Name} = {parameter.DefaultExponent.ToString(CultureInfo.InvariantCulture)}")); - if (conversion.Scale == 1 && conversion.Offset == 0) + if (conversion.Scale == 1) { return $"public double {conversion.AccessorName}({parameters}) => {variableName}.AsArbitrarySiComponents(overallScaleExponent: {conversion.ScaleExponentExpression});"; } - return $"public double {conversion.AccessorName}({parameters}) => ({variableName}.AsSiBaseUnit() - {FormatDouble(conversion.Offset)}) / ({FormatDouble(conversion.Scale)} * global::System.Math.Pow(10, {conversion.ScaleExponentExpression}));"; + return $"public double {conversion.AccessorName}({parameters}) => {variableName}.AsSiBaseUnit() / ({FormatDouble(conversion.Scale)} * global::System.Math.Pow(10, {conversion.ScaleExponentExpression}));"; } private static string FormatExponent(Rational exponent) => ((double)exponent).ToString("R", CultureInfo.InvariantCulture) + "d"; private static string FormatDouble(double value) => value.ToString("R", CultureInfo.InvariantCulture) + "d"; private sealed record ConversionDefinition(string FactoryName, string AccessorName, PrefixParameterDefinition[] PrefixParameters, - string ScaleExponentExpression, double Scale, double Offset); + string ScaleExponentExpression, double Scale); private sealed record PrefixParameterDefinition(string Name, int DefaultExponent); } diff --git a/src/DimensionalQuantities.Roslyn/Generators/UnitProviderReader.cs b/src/DimensionalQuantities.Roslyn/Generators/UnitProviderReader.cs index 4de97fe..2591c28 100644 --- a/src/DimensionalQuantities.Roslyn/Generators/UnitProviderReader.cs +++ b/src/DimensionalQuantities.Roslyn/Generators/UnitProviderReader.cs @@ -11,7 +11,6 @@ internal sealed class UnitProviderReader { private readonly INamedTypeSymbol _providerAttribute; private readonly INamedTypeSymbol _quantityAttribute; - private readonly INamedTypeSymbol _conversionAttribute; private readonly INamedTypeSymbol _factorAttribute; private readonly INamedTypeSymbol _symbolAttribute; private readonly INamedTypeSymbol _additionalAttribute; @@ -21,7 +20,6 @@ public UnitProviderReader(Compilation compilation) { _providerAttribute = compilation.GetRequiredType(typeof(UnitProviderAttribute)); _quantityAttribute = compilation.GetRequiredType(typeof(QuantityUnitAttribute)); - _conversionAttribute = compilation.GetRequiredType(typeof(UnitConversionAttribute)); _factorAttribute = compilation.GetRequiredType(typeof(UnitFactorAttribute)); _symbolAttribute = compilation.GetRequiredType(typeof(UnitSymbolAttribute)); _additionalAttribute = compilation.GetRequiredType(typeof(AdditionalUnitAttribute)); @@ -101,14 +99,6 @@ private bool TryReadUnitDefinition(INamedTypeSymbol declaration, INamedTypeSymbo return false; } - AttributeData? conversion = Find(declaration, _conversionAttribute); - double offset = conversion is null ? 0d : GetNamed(conversion, nameof(UnitConversionAttribute.Offset), 0d); - if (!IsFinite(offset)) - { - Report(report, conversion, $"{declaration.Name} has a non-finite offset."); - return false; - } - if (!TryReadAlternatives(declaration, provider, report, out UnitExpression[] alternatives)) { return false; @@ -122,7 +112,7 @@ private bool TryReadUnitDefinition(INamedTypeSymbol declaration, INamedTypeSymbo return false; } - unit = new(name, expression, alternatives, offset); + unit = new(name, expression, alternatives); return true; } diff --git a/src/DimensionalQuantities/Units/Declarations/UnitConversionAttribute.cs b/src/DimensionalQuantities/Units/Declarations/UnitConversionAttribute.cs deleted file mode 100644 index 31fab0f..0000000 --- a/src/DimensionalQuantities/Units/Declarations/UnitConversionAttribute.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace DimensionalQuantities.Units.Declarations; - -/// Sets conversion options shared by the quantity associations on a unit declaration. -[AttributeUsage(AttributeTargets.Class)] -public sealed class UnitConversionAttribute : Attribute -{ - /// Offset added after scaling a magnitude into coherent SI units. - public double Offset { get; set; } -} diff --git a/src/DimensionalQuantities/Units/UnitDefinition.cs b/src/DimensionalQuantities/Units/UnitDefinition.cs index 8665d52..103d3b4 100644 --- a/src/DimensionalQuantities/Units/UnitDefinition.cs +++ b/src/DimensionalQuantities/Units/UnitDefinition.cs @@ -2,12 +2,11 @@ namespace DimensionalQuantities.Units; internal readonly record struct UnitDefinition { - public UnitDefinition(string name, UnitExpression expression, UnitExpression[]? additionalConversions = null, double offset = 0) + public UnitDefinition(string name, UnitExpression expression, UnitExpression[]? additionalConversions = null) { Name = name; Expression = expression; AdditionalConversions = additionalConversions ?? []; - Offset = offset; } public UnitDefinition(string name, UnitTerm[] terms) : this(name, new UnitExpression(terms)) { } @@ -15,5 +14,4 @@ public UnitDefinition(string name, UnitExpression expression, UnitExpression[]? public string Name { get; } public UnitExpression Expression { get; } public UnitExpression[] AdditionalConversions { get; } - public double Offset { get; } }