Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 { }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 { }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Diagnostic> 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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -66,46 +66,46 @@ private static string RenderQuantity(UnitConversionSpec quantity, int[] prefixes
""";
}

private static IReadOnlyDictionary<string, (Rational Exponent, double Scale, double Offset)> CollectConversions(
private static IReadOnlyDictionary<string, (Rational Exponent, double Scale)> CollectConversions(
IReadOnlyList<UnitDefinition> units, int[] generatedPrefixes)
{
Dictionary<string, (Rational Exponent, double Scale, double Offset)> conversions = new(StringComparer.Ordinal);
foreach ((string name, Rational exponent, double scale, double offset) in units.SelectMany(unit => GetConversions(unit, generatedPrefixes)))
Dictionary<string, (Rational Exponent, double Scale)> 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());
}
}

Expand Down Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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));
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}

Expand Down

This file was deleted.

4 changes: 1 addition & 3 deletions src/DimensionalQuantities/Units/UnitDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@ 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)) { }

public string Name { get; }
public UnitExpression Expression { get; }
public UnitExpression[] AdditionalConversions { get; }
public double Offset { get; }
}
Loading