diff --git a/.github/scripts/check-package.py b/.github/scripts/check-package.py
index 7c2d837..8098e27 100644
--- a/.github/scripts/check-package.py
+++ b/.github/scripts/check-package.py
@@ -8,7 +8,7 @@
assert len(packages) == 1, packages
with zipfile.ZipFile(packages[0]) as package:
names = package.namelist()
- assert "lib/net10.0/DimensionalQuantities.dll" in names
+ assert "lib/netstandard2.0/DimensionalQuantities.dll" in names
assert "analyzers/dotnet/cs/DimensionalQuantities.Roslyn.dll" in names
assert not any("Microsoft.CodeAnalysis" in name for name in names)
specification = ET.fromstring(package.read("DimensionalQuantities.nuspec"))
diff --git a/README.md b/README.md
index 115dc0f..7418e4f 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@ A C# library for dimensional quantities, with generated type aliases, SI unit co
## Usage
-Requires .NET 10 and C# 14. The `DimensionalQuantities` NuGet package includes both the runtime library and its source generator.
+The runtime library targets .NET Standard 2.0. Consumers need a C# 14 compiler for the generated extension blocks. The `DimensionalQuantities` NuGet package includes both the runtime library and its source generator.
```csharp
Voltage voltage = Voltage.FromVolts(12);
diff --git a/src/DimensionalQuantities.IntegrationTests/DimensionalQuantities.IntegrationTests.csproj b/src/DimensionalQuantities.IntegrationTests/DimensionalQuantities.IntegrationTests.csproj
index b6ddf5d..7e67257 100644
--- a/src/DimensionalQuantities.IntegrationTests/DimensionalQuantities.IntegrationTests.csproj
+++ b/src/DimensionalQuantities.IntegrationTests/DimensionalQuantities.IntegrationTests.csproj
@@ -13,7 +13,7 @@
-
+
diff --git a/src/DimensionalQuantities.IntegrationTests/GeneratedQuantityTests/PackagedGeneratorTests.cs b/src/DimensionalQuantities.IntegrationTests/GeneratedQuantityTests/PackagedGeneratorTests.cs
index defcdd0..284b559 100644
--- a/src/DimensionalQuantities.IntegrationTests/GeneratedQuantityTests/PackagedGeneratorTests.cs
+++ b/src/DimensionalQuantities.IntegrationTests/GeneratedQuantityTests/PackagedGeneratorTests.cs
@@ -23,7 +23,7 @@ public void PackagedGeneratorConvertsVoltageInConsumer()
{
string sourceProject = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "DimensionalQuantities", "DimensionalQuantities.csproj"));
FileInfo nupkg = PackLibrary(sourceProject, _scratch);
- using PackageRepository repository = PackageRepository.Create(_scratch);
+ using PackageRepository repository = PackageRepository.Create(_scratch, new Uri("https://api.nuget.org/v3/index.json"));
repository.Package(nupkg, out Package package);
string consumerDirectory = Path.Combine(_scratch, "consumer");
@@ -39,9 +39,18 @@ public static double ConvertVoltage(double volts)
Voltage voltage = Voltage.FromVolts(volts);
return voltage.AsVolts();
}
+
+ public static double MultiplyVoltageCurrent(double volts, double amps)
+ {
+ Voltage voltage = Voltage.FromVolts(volts);
+ Current current = Current.FromAmps(amps);
+ Power power = voltage * current;
+ return power.AsWatts();
+ }
}
""");
- ProjectCreator creator = ProjectCreator.Templates.SdkCsproj(path: consumerProject, targetFramework: "net10.0")
+ ProjectCreator creator = ProjectCreator.Templates.SdkCsproj(path: consumerProject, targetFramework: "netstandard2.0")
+ .Property("LangVersion", "14.0")
.ItemPackageReference(package)
.Save();
@@ -51,13 +60,16 @@ public static double ConvertVoltage(double volts)
Assert.True(built, $"Consumer build failed.{Environment.NewLine}{output.GetConsoleLog()}");
}
- string assemblyFile = Path.Combine(consumerDirectory, "bin", "Debug", "net10.0", "Consumer.dll");
+ string assemblyFile = Path.Combine(consumerDirectory, "bin", "Debug", "netstandard2.0", "Consumer.dll");
Assembly assembly = Assembly.LoadFrom(assemblyFile);
Type consumerType = assembly.GetType("ConsumerProject.Consumer") ?? throw new InvalidOperationException("Consumer type was not built.");
MethodInfo convertVoltage = consumerType.GetMethod("ConvertVoltage") ?? throw new InvalidOperationException("Consumer Voltage conversion was not built.");
+ MethodInfo multiplyVoltageCurrent = consumerType.GetMethod("MultiplyVoltageCurrent") ?? throw new InvalidOperationException("Consumer arithmetic was not built.");
double result = Assert.IsType(convertVoltage.Invoke(null, [12.5]));
- double expected = 12.5;
- Assert.Equal(expected, result);
+ double product = Assert.IsType(multiplyVoltageCurrent.Invoke(null, [12.5, 2.0]));
+ Assert.Multiple(
+ () => Assert.Equal(12.5, result),
+ () => Assert.Equal(25.0, product));
}
public void Dispose()
diff --git a/src/DimensionalQuantities.Roslyn/DimensionalQuantities.Roslyn.csproj b/src/DimensionalQuantities.Roslyn/DimensionalQuantities.Roslyn.csproj
index 8a8490d..cfdb6db 100644
--- a/src/DimensionalQuantities.Roslyn/DimensionalQuantities.Roslyn.csproj
+++ b/src/DimensionalQuantities.Roslyn/DimensionalQuantities.Roslyn.csproj
@@ -1,6 +1,6 @@
- net10.0
+ netstandard2.0
true
true
false
@@ -8,6 +8,7 @@
+
diff --git a/src/DimensionalQuantities.Roslyn/Generators/QuantitySourceEmitter.cs b/src/DimensionalQuantities.Roslyn/Generators/QuantitySourceEmitter.cs
index c26b4a0..6fe0de1 100644
--- a/src/DimensionalQuantities.Roslyn/Generators/QuantitySourceEmitter.cs
+++ b/src/DimensionalQuantities.Roslyn/Generators/QuantitySourceEmitter.cs
@@ -55,7 +55,10 @@ private static IReadOnlyDictionary CollectConversions(IReadOnl
throw new InvalidOperationException($"Unit {name} has conflicting SI scales.");
}
- conversions.TryAdd(name, scaleExponent);
+ if (!conversions.ContainsKey(name))
+ {
+ conversions.Add(name, scaleExponent);
+ }
}
return conversions;
@@ -142,7 +145,7 @@ private static string RenderAccessor(string variableName, ConversionDefinition c
return $"public double {conversion.AccessorName}({parameters}) => {variableName}.AsArbitrarySiComponents(overallScaleExponent: {conversion.ScaleExponentExpression});";
}
- private static string FormatExponent(Rational exponent) => double.CreateChecked(exponent).ToString("R", CultureInfo.InvariantCulture) + "d";
+ private static string FormatExponent(Rational exponent) => ((double)exponent).ToString("R", CultureInfo.InvariantCulture) + "d";
private static Type[] GetExponentTypes(Dimension dimension) =>
[
diff --git a/src/DimensionalQuantities.Tests/Numerics/RationalTests.Conversions.cs b/src/DimensionalQuantities.Tests/Numerics/RationalTests.Conversions.cs
index 4107871..a4cf567 100644
--- a/src/DimensionalQuantities.Tests/Numerics/RationalTests.Conversions.cs
+++ b/src/DimensionalQuantities.Tests/Numerics/RationalTests.Conversions.cs
@@ -7,87 +7,71 @@ namespace DimensionalQuantities.Tests.Numerics;
public sealed partial class RationalTests
{
[Fact]
- public void DecimalConversionPreservesExactBaseTenValue()
+ public void DecimalCastPreservesExactBaseTenValue()
{
decimal original = 0.1m;
- Rational result = Rational.CreateChecked(original);
- Rational expected = new(1, 10);
+ Rational result = (Rational)original;
Assert.Multiple(
- () => Assert.Equal(expected, result),
- () => Assert.Equal(original, decimal.CreateChecked(result)));
+ () => Assert.Equal(new Rational(1, 10), result),
+ () => Assert.Equal(original, (decimal)result));
}
[Fact]
- public void DoubleConversionPreservesExactBinaryValue()
+ public void DoubleCastPreservesExactBinaryValue()
{
double original = 0.1d;
- Rational result = Rational.CreateChecked(original);
- Rational expected = new(3602879701896397, 36028797018963968);
+ Rational result = (Rational)original;
Assert.Multiple(
- () => Assert.Equal(expected, result),
- () => Assert.Equal(original, double.CreateChecked(result)));
- }
-
- [Fact]
- public void GenericInputModesPreserveFiniteFractions()
- {
- Rational checkedResult = Rational.CreateChecked(0.125m);
- Rational saturatingResult = Rational.CreateSaturating(0.125m);
- Rational truncatingResult = Rational.CreateTruncating(0.125m);
- Rational expected = new(1, 8);
-
- Assert.Multiple(
- () => Assert.Equal(expected, checkedResult),
- () => Assert.Equal(expected, saturatingResult),
- () => Assert.Equal(expected, truncatingResult));
+ () => Assert.Equal(new Rational(3602879701896397, 36028797018963968), result),
+ () => Assert.Equal(original, (double)result));
}
[Theory]
[InlineData(double.NaN)]
[InlineData(double.PositiveInfinity)]
[InlineData(double.NegativeInfinity)]
- public void NonFiniteNumbersHaveNoRationalRepresentation(double value)
- {
- Assert.Multiple(
- () => Assert.Throws(() => Rational.CreateChecked(value)),
- () => Assert.Throws(() => Rational.CreateSaturating(value)),
- () => Assert.Throws(() => Rational.CreateTruncating(value)));
- }
+ public void NonFiniteNumbersHaveNoRationalRepresentation(double value) =>
+ Assert.Throws(() => (Rational)value);
[Fact]
- public void IntegralConversionDiscardsFractionTowardsZero()
+ public void IntegerCastsTruncateTowardsZeroAndCheckRange()
{
- Rational original = new(-7, 3);
- int result = int.CreateChecked(original);
- int expected = -2;
+ Rational fraction = new(-7, 3);
+ Rational tooLarge = new(601, 2);
- Assert.Equal(expected, result);
+ Assert.Multiple(
+ () => Assert.Equal(-2, (int)fraction),
+ () => Assert.Equal((byte)42, (byte)new Rational(85, 2)),
+ () => Assert.Throws(() => (byte)tooLarge));
}
[Fact]
- public void IntegralOutputModesUseTargetConversionRules()
+ public void IntegerCastsCoverSignedAndUnsignedRanges()
{
- Rational original = new(601, 2);
- byte saturatingResult = byte.CreateSaturating(original);
- byte truncatingResult = byte.CreateTruncating(original);
+ Rational value = new(42);
+ Rational negative = new(-1);
Assert.Multiple(
- () => Assert.Equal(byte.MaxValue, saturatingResult),
- () => Assert.Equal((byte)44, truncatingResult),
- () => Assert.Throws(() => byte.CreateChecked(original)));
+ () => Assert.Equal((sbyte)42, (sbyte)value),
+ () => Assert.Equal((short)42, (short)value),
+ () => Assert.Equal((ushort)42, (ushort)value),
+ () => Assert.Equal(42u, (uint)value),
+ () => Assert.Equal(42L, (long)value),
+ () => Assert.Equal(42UL, (ulong)value),
+ () => Assert.Throws(() => (uint)negative),
+ () => Assert.Throws(() => (sbyte)new Rational(128)));
}
[Fact]
public void BigIntegerRoundTripPreservesWholeValue()
{
BigInteger original = BigInteger.One << 128;
- Rational converted = Rational.CreateChecked(original);
- BigInteger result = BigInteger.CreateChecked(converted);
- BigInteger expected = original;
+ Rational converted = original;
+ BigInteger result = (BigInteger)converted;
- Assert.Equal(expected, result);
+ Assert.Equal(original, result);
}
[Fact]
@@ -96,18 +80,14 @@ public void PrimitiveIntegerInputsPreserveValues()
Rational expected = new(42);
Assert.Multiple(
- () => Assert.Equal(expected, Rational.CreateChecked((byte)42)),
- () => Assert.Equal(expected, Rational.CreateChecked((sbyte)42)),
- () => Assert.Equal(expected, Rational.CreateChecked((short)42)),
- () => Assert.Equal(expected, Rational.CreateChecked((ushort)42)),
- () => Assert.Equal(expected, Rational.CreateChecked(42)),
- () => Assert.Equal(expected, Rational.CreateChecked(42u)),
- () => Assert.Equal(expected, Rational.CreateChecked(42L)),
- () => Assert.Equal(expected, Rational.CreateChecked(42UL)),
- () => Assert.Equal(expected, Rational.CreateChecked((nint)42)),
- () => Assert.Equal(expected, Rational.CreateChecked((nuint)42)),
- () => Assert.Equal(expected, Rational.CreateChecked((Int128)42)),
- () => Assert.Equal(expected, Rational.CreateChecked((UInt128)42)));
+ () => Assert.Equal(expected, (Rational)(byte)42),
+ () => Assert.Equal(expected, (Rational)(sbyte)42),
+ () => Assert.Equal(expected, (Rational)(short)42),
+ () => Assert.Equal(expected, (Rational)(ushort)42),
+ () => Assert.Equal(expected, (Rational)42),
+ () => Assert.Equal(expected, (Rational)42u),
+ () => Assert.Equal(expected, (Rational)42L),
+ () => Assert.Equal(expected, (Rational)42UL));
}
[Theory]
@@ -118,11 +98,8 @@ public void PrimitiveIntegerInputsPreserveValues()
[InlineData(double.MinValue)]
public void DoubleRoundTripPreservesRepresentableValues(double original)
{
- Rational converted = Rational.CreateChecked(original);
- double result = double.CreateChecked(converted);
- double expected = original;
-
- Assert.Equal(expected, result);
+ Rational converted = (Rational)original;
+ Assert.Equal(original, (double)converted);
}
[Theory]
@@ -131,32 +108,15 @@ public void DoubleRoundTripPreservesRepresentableValues(double original)
[InlineData(-0.1f)]
public void SingleRoundTripPreservesRepresentableValues(float original)
{
- Rational converted = Rational.CreateChecked(original);
- float result = float.CreateChecked(converted);
- float expected = original;
-
- Assert.Equal(expected, result);
- }
-
- [Fact]
- public void HalfRoundTripPreservesRepresentableValues()
- {
- Half original = Half.Epsilon;
- Rational converted = Rational.CreateChecked(original);
- Half result = Half.CreateChecked(converted);
- Half expected = original;
-
- Assert.Equal(expected, result);
+ Rational converted = (Rational)original;
+ Assert.Equal(original, (float)converted);
}
[Fact]
public void BinaryConversionRoundsMidpointToEven()
{
Rational original = new((BigInteger.One << 53) + 1, BigInteger.One << 53);
- double result = double.CreateChecked(original);
- double expected = 1.0;
-
- Assert.Equal(expected, result);
+ Assert.Equal(1.0, (double)original);
}
[Fact]
@@ -164,35 +124,34 @@ public void BinaryConversionDividesBeforeLosingLargeComponents()
{
BigInteger scale = BigInteger.One << 2000;
Rational original = new(scale + 1, 2 * scale + 1);
- double result = double.CreateChecked(original);
- double expected = 0.5;
+ Assert.Equal(0.5, (double)original);
+ }
- Assert.Equal(expected, result);
+ [Fact]
+ public void BinaryConversionRoundsAtSubnormalBoundary()
+ {
+ Rational halfwayToMinimum = new(1, BigInteger.One << 1075);
+ Rational halfwayToSecond = new(3, BigInteger.One << 1075);
+
+ Assert.Multiple(
+ () => Assert.Equal(0.0, (double)halfwayToMinimum),
+ () => Assert.Equal(2 * double.Epsilon, (double)halfwayToSecond));
}
[Fact]
public void DecimalConversionRoundsRepeatingFractions()
{
Rational original = new(1, 3);
- decimal result = decimal.CreateChecked(original);
- decimal expected = 1m / 3m;
-
- Assert.Equal(expected, result);
+ Assert.Equal(1m / 3m, (decimal)original);
}
[Fact]
- public void DecimalOutputModesRespectTargetRange()
+ public void DecimalCastRejectsOutOfRangeValue()
{
Rational original = new(new BigInteger(decimal.MaxValue) + 1);
- decimal saturatingResult = decimal.CreateSaturating(original);
- decimal truncatingResult = decimal.CreateTruncating(original);
- decimal expected = decimal.MaxValue;
-
- Assert.Multiple(
- () => Assert.Equal(expected, saturatingResult),
- () => Assert.Equal(expected, truncatingResult),
- () => Assert.Throws(() => decimal.CreateChecked(original)));
+ Assert.Throws(() => (decimal)original);
}
+
[Theory]
[InlineData(1.5)]
[InlineData(-0.125)]
@@ -200,30 +159,10 @@ public void DecimalOutputModesRespectTargetRange()
public void RealComplexInputsPreserveFractions(double real)
{
Complex original = new(real, 0);
- Rational checkedResult = Rational.CreateChecked(original);
- Rational saturatingResult = Rational.CreateSaturating(original);
- Rational truncatingResult = Rational.CreateTruncating(original);
- Rational expected = Rational.CreateChecked(real);
-
+ Rational converted = (Rational)original;
Assert.Multiple(
- () => Assert.Equal(expected, checkedResult),
- () => Assert.Equal(expected, saturatingResult),
- () => Assert.Equal(expected, truncatingResult));
- }
-
- [Fact]
- public void ComplexOutputsPreserveFractionalRealPart()
- {
- Rational original = new(3, 2);
- Complex checkedResult = Complex.CreateChecked(original);
- Complex saturatingResult = Complex.CreateSaturating(original);
- Complex truncatingResult = Complex.CreateTruncating(original);
- Complex expected = new(1.5, 0);
-
- Assert.Multiple(
- () => Assert.Equal(expected, checkedResult),
- () => Assert.Equal(expected, saturatingResult),
- () => Assert.Equal(expected, truncatingResult));
+ () => Assert.Equal((Rational)real, converted),
+ () => Assert.Equal(original, (Complex)converted));
}
[Theory]
@@ -232,10 +171,6 @@ public void ComplexOutputsPreserveFractionalRealPart()
public void NonRealComplexInputsAreRejected(double imaginary)
{
Complex original = new(1.5, imaginary);
-
- Assert.Multiple(
- () => Assert.Throws(() => Rational.CreateChecked(original)),
- () => Assert.Throws(() => Rational.CreateSaturating(original)),
- () => Assert.Throws(() => Rational.CreateTruncating(original)));
+ Assert.Throws(() => (Rational)original);
}
}
diff --git a/src/DimensionalQuantities.Tests/Numerics/RationalTests.Text.cs b/src/DimensionalQuantities.Tests/Numerics/RationalTests.Text.cs
index 73bc42a..c830d75 100644
--- a/src/DimensionalQuantities.Tests/Numerics/RationalTests.Text.cs
+++ b/src/DimensionalQuantities.Tests/Numerics/RationalTests.Text.cs
@@ -1,5 +1,4 @@
using System.Globalization;
-using System.Text;
using DimensionalQuantities.Numerics;
using Xunit;
@@ -73,54 +72,31 @@ public void FormattingWholeNumbersOmitsDenominator()
}
[Fact]
- public void CharacterSpanFormattingAndParsingRoundTrip()
+ public void StringFormattingAndParsingRoundTrip()
{
Rational original = new(-3, 4);
- Span buffer = stackalloc char[20];
- bool formatted = original.TryFormat(buffer, out int written, default, CultureInfo.InvariantCulture);
- bool parsed = Rational.TryParse(buffer[..written], CultureInfo.InvariantCulture, out Rational result);
- string formattedText = new(buffer[..written]);
+ string formattedText = original.ToString(null, CultureInfo.InvariantCulture);
+ bool parsed = Rational.TryParse(formattedText, CultureInfo.InvariantCulture, out Rational result);
Rational expected = new(-3, 4);
Assert.Multiple(
- () => Assert.True(formatted),
() => Assert.True(parsed),
() => Assert.Equal("-3/4", formattedText),
() => Assert.Equal(expected, result));
}
[Fact]
- public void Utf8FormattingAndParsingHonourCulture()
+ public void FormattingAndParsingHonourCulture()
{
Rational original = new(-3, 4);
NumberFormatInfo provider = new() { NegativeSign = "−" };
- Span buffer = stackalloc byte[20];
- bool formatted = original.TryFormat(buffer, out int written, default, provider);
- bool parsed = Rational.TryParse(buffer[..written], provider, out Rational result);
- string formattedText = Encoding.UTF8.GetString(buffer[..written]);
+ string formattedText = original.ToString(null, provider);
+ bool parsed = Rational.TryParse(formattedText, provider, out Rational result);
Rational expected = new(-3, 4);
Assert.Multiple(
- () => Assert.True(formatted),
() => Assert.True(parsed),
() => Assert.Equal("−3/4", formattedText),
() => Assert.Equal(expected, result));
}
-
- [Fact]
- public void FormattingReportsInsufficientDestination()
- {
- Rational original = new(3, 4);
- Span characters = stackalloc char[2];
- Span bytes = stackalloc byte[2];
-
- bool charactersFormatted = original.TryFormat(characters, out int charsWritten, default, CultureInfo.InvariantCulture);
- bool bytesFormatted = original.TryFormat(bytes, out int bytesWritten, default, CultureInfo.InvariantCulture);
-
- Assert.Multiple(
- () => Assert.False(charactersFormatted),
- () => Assert.False(bytesFormatted),
- () => Assert.Equal(0, charsWritten),
- () => Assert.Equal(0, bytesWritten));
- }
}
diff --git a/src/DimensionalQuantities.Tests/Numerics/RationalTests.cs b/src/DimensionalQuantities.Tests/Numerics/RationalTests.cs
index 02dbfab..2ca5a74 100644
--- a/src/DimensionalQuantities.Tests/Numerics/RationalTests.cs
+++ b/src/DimensionalQuantities.Tests/Numerics/RationalTests.cs
@@ -149,17 +149,6 @@ public void DivisionByZeroIsRejected()
Assert.Throws(() => original / zero);
}
- [Fact]
- public void GenericMathAcceptsRational()
- {
- Rational original = new(1, 3);
- Rational operand = new(1, 6);
- Rational result = Add(original, operand);
- Rational expected = new(1, 2);
-
- Assert.Equal(expected, result);
- }
-
[Fact]
public void ComparisonUsesFractionValues()
{
@@ -285,5 +274,4 @@ public void AbsoluteValueMakesNegativeFractionPositive()
Assert.Equal(expected, result);
}
- private static T Add(T left, T right) where T : INumber => left + right;
}
diff --git a/src/DimensionalQuantities/DimensionalQuantities.csproj b/src/DimensionalQuantities/DimensionalQuantities.csproj
index b80e131..00d9058 100644
--- a/src/DimensionalQuantities/DimensionalQuantities.csproj
+++ b/src/DimensionalQuantities/DimensionalQuantities.csproj
@@ -1,6 +1,6 @@
- net10.0
+ netstandard2.0
GoSim
MIT
README.md
@@ -19,13 +19,14 @@
-
-
+
diff --git a/src/DimensionalQuantities/Dimensions/Dimension.cs b/src/DimensionalQuantities/Dimensions/Dimension.cs
index 8ca3f86..f2ef2dd 100644
--- a/src/DimensionalQuantities/Dimensions/Dimension.cs
+++ b/src/DimensionalQuantities/Dimensions/Dimension.cs
@@ -92,7 +92,10 @@ public IEnumerable GetComponents()
///
public Dimension Root(int degree)
{
- ArgumentOutOfRangeException.ThrowIfNegativeOrZero(degree);
+ if (degree <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(degree));
+ }
return Pow(new(1, degree));
}
diff --git a/src/DimensionalQuantities/Naming/DimensionNameGenerator.cs b/src/DimensionalQuantities/Naming/DimensionNameGenerator.cs
index 906b53e..bbc7aa6 100644
--- a/src/DimensionalQuantities/Naming/DimensionNameGenerator.cs
+++ b/src/DimensionalQuantities/Naming/DimensionNameGenerator.cs
@@ -1,4 +1,3 @@
-using System.Collections.Immutable;
using System.Globalization;
using System.Numerics;
using System.Text;
@@ -15,8 +14,8 @@ internal static class DimensionNameGenerator
public static string GetUnitExpression(Dimension dimension)
{
- ImmutableArray components = dimension.GetComponents().ToImmutableArray();
- if (components.IsEmpty)
+ DimensionComponent[] components = dimension.GetComponents().ToArray();
+ if (components.Length == 0)
{
throw new NotSupportedException("Dimensionless names are not defined.");
}
@@ -36,8 +35,8 @@ public static string GetUnitExpression(Dimension dimension)
private static string GetName(Dimension dimension, Func getFactorName)
{
- ImmutableArray components = dimension.GetComponents().ToImmutableArray();
- if (components.IsEmpty)
+ DimensionComponent[] components = dimension.GetComponents().ToArray();
+ if (components.Length == 0)
{
throw new NotSupportedException("Dimensionless names are not defined.");
}
diff --git a/src/DimensionalQuantities/Numerics/Rational.Conversions.cs b/src/DimensionalQuantities/Numerics/Rational.Conversions.cs
index e873f35..a758299 100644
--- a/src/DimensionalQuantities/Numerics/Rational.Conversions.cs
+++ b/src/DimensionalQuantities/Numerics/Rational.Conversions.cs
@@ -4,68 +4,43 @@ namespace DimensionalQuantities.Numerics;
internal readonly partial record struct Rational
{
- public static Rational CreateChecked(TOther value) where TOther : INumberBase =>
- TryConvertFrom(value, out Rational result) || TOther.TryConvertToChecked(value, out result) ? result : throw new NotSupportedException($"Cannot convert {typeof(TOther)} to Rational.");
-
- public static Rational CreateSaturating(TOther value) where TOther : INumberBase =>
- TryConvertFrom(value, out Rational result) || TOther.TryConvertToSaturating(value, out result) ? result : throw new NotSupportedException($"Cannot convert {typeof(TOther)} to Rational.");
-
- public static Rational CreateTruncating(TOther value) where TOther : INumberBase =>
- TryConvertFrom(value, out Rational result) || TOther.TryConvertToTruncating(value, out result) ? result : throw new NotSupportedException($"Cannot convert {typeof(TOther)} to Rational.");
-
- // All finite built-in inputs are represented exactly, so incoming conversion
- // modes coincide. Infinity and NaN have no rational representation.
- static bool INumberBase.TryConvertFromChecked(TOther value, out Rational result) => TryConvertFrom(value, out result);
- static bool INumberBase.TryConvertFromSaturating(TOther value, out Rational result) => TryConvertFrom(value, out result);
- static bool INumberBase.TryConvertFromTruncating(TOther value, out Rational result) => TryConvertFrom(value, out result);
-
- private static bool TryConvertFrom(TOther value, out Rational result) where TOther : INumberBase
+ public static implicit operator Rational(byte value) => new(value);
+ public static implicit operator Rational(sbyte value) => new(value);
+ public static implicit operator Rational(short value) => new(value);
+ public static implicit operator Rational(ushort value) => new(value);
+ public static implicit operator Rational(uint value) => new(value);
+ public static implicit operator Rational(long value) => new(value);
+ public static implicit operator Rational(ulong value) => new(value);
+ public static implicit operator Rational(BigInteger value) => new(value);
+
+ public static explicit operator Rational(float value) => FromDouble(value);
+ public static explicit operator Rational(double value) => FromDouble(value);
+ public static explicit operator Rational(decimal value) => FromDecimal(value);
+ public static explicit operator Rational(Complex value)
{
- switch (value)
- {
- case Rational rational:
- result = rational;
- return true;
- case Complex number:
- if (number.Imaginary != 0)
- {
- throw new RationalNumberCantHaveComplexComponentsException();
- }
-
- result = FromDouble(number.Real);
- return true;
- case decimal number:
- result = FromDecimal(number);
- return true;
- case double number:
- result = FromDouble(number);
- return true;
- case float number:
- result = FromDouble(number);
- return true;
- case Half number:
- result = FromDouble((double)number);
- return true;
- }
-
- if (IsBuiltInInteger())
- {
- result = new(BigInteger.CreateChecked(value));
- return true;
- }
-
- result = Zero;
- return false;
+ if (value.Imaginary != 0)
+ {
+ throw new RationalNumberCantHaveComplexComponentsException();
+ }
+
+ return FromDouble(value.Real);
}
- private static bool IsBuiltInInteger() =>
- typeof(T) == typeof(byte) || typeof(T) == typeof(sbyte) ||
- typeof(T) == typeof(short) || typeof(T) == typeof(ushort) ||
- typeof(T) == typeof(int) || typeof(T) == typeof(uint) ||
- typeof(T) == typeof(long) || typeof(T) == typeof(ulong) ||
- typeof(T) == typeof(nint) || typeof(T) == typeof(nuint) ||
- typeof(T) == typeof(Int128) || typeof(T) == typeof(UInt128) ||
- typeof(T) == typeof(char) || typeof(T) == typeof(BigInteger);
+ public static explicit operator byte(Rational value) => checked((byte)value.WholePart);
+ public static explicit operator sbyte(Rational value) => checked((sbyte)value.WholePart);
+ public static explicit operator short(Rational value) => checked((short)value.WholePart);
+ public static explicit operator ushort(Rational value) => checked((ushort)value.WholePart);
+ public static explicit operator int(Rational value) => checked((int)value.WholePart);
+ public static explicit operator uint(Rational value) => checked((uint)value.WholePart);
+ public static explicit operator long(Rational value) => checked((long)value.WholePart);
+ public static explicit operator ulong(Rational value) => checked((ulong)value.WholePart);
+ public static explicit operator BigInteger(Rational value) => value.WholePart;
+ public static explicit operator double(Rational value) => value.ToBinaryFloat(53, -1022, 1023);
+ public static explicit operator float(Rational value) => (float)value.ToBinaryFloat(24, -126, 127);
+ public static explicit operator decimal(Rational value) => value.ToDecimal();
+ public static explicit operator Complex(Rational value) => new((double)value, 0);
+
+ private BigInteger WholePart => Numerator / Denominator;
private static Rational FromDecimal(decimal value)
{
@@ -82,12 +57,12 @@ private static Rational FromDecimal(decimal value)
private static Rational FromDouble(double value)
{
- if (!double.IsFinite(value))
+ if (double.IsNaN(value) || double.IsInfinity(value))
{
throw new OverflowException("NaN and infinity cannot be represented as a Rational.");
}
- ulong bits = BitConverter.DoubleToUInt64Bits(value);
+ ulong bits = unchecked((ulong)BitConverter.DoubleToInt64Bits(value));
int exponentBits = (int)((bits >> 52) & 0x7ff);
BigInteger significand = bits & 0x000f_ffff_ffff_ffff;
int exponent = -1074;
@@ -104,63 +79,6 @@ private static Rational FromDouble(double value)
return exponent >= 0 ? new(significand << exponent) : new(significand, BigInteger.One << -exponent);
}
- static bool INumberBase.TryConvertToChecked(Rational value, out TOther result) => TryConvertTo(value, ConversionMode.Checked, out result);
- static bool INumberBase.TryConvertToSaturating(Rational value, out TOther result) => TryConvertTo(value, ConversionMode.Saturating, out result);
- static bool INumberBase.TryConvertToTruncating(Rational value, out TOther result) => TryConvertTo(value, ConversionMode.Truncating, out result);
-
- private enum ConversionMode { Checked, Saturating, Truncating }
-
- private static bool TryConvertTo(Rational value, ConversionMode mode, out TOther result) where TOther : INumberBase
- {
- if (typeof(TOther) == typeof(Rational))
- {
- result = (TOther)(object)value;
- return true;
- }
- if (typeof(TOther) == typeof(Complex))
- {
- result = (TOther)(object)new Complex(value.ToBinaryFloat(53, -1022, 1023), 0);
- return true;
- }
- if (typeof(TOther) == typeof(double))
- {
- result = (TOther)(object)value.ToBinaryFloat(53, -1022, 1023);
- return true;
- }
- if (typeof(TOther) == typeof(float))
- {
- result = (TOther)(object)(float)value.ToBinaryFloat(24, -126, 127);
- return true;
- }
- if (typeof(TOther) == typeof(Half))
- {
- result = (TOther)(object)(Half)value.ToBinaryFloat(11, -14, 15);
- return true;
- }
- if (typeof(TOther) == typeof(decimal))
- {
- result = (TOther)(object)value.ToDecimal(mode);
- return true;
- }
-
- if (!IsBuiltInInteger())
- {
- result = TOther.Zero;
- return false;
- }
-
- // Integral targets discard the fractional part towards zero; the target
- // type owns overflow, saturation and truncation of the resulting integer.
- BigInteger whole = value.Numerator / value.Denominator;
- result = mode switch
- {
- ConversionMode.Checked => TOther.CreateChecked(whole),
- ConversionMode.Saturating => TOther.CreateSaturating(whole),
- _ => TOther.CreateTruncating(whole)
- };
- return true;
- }
-
// Round the fraction directly to the target precision. Converting numerator
// and denominator separately would lose accuracy or produce infinity/infinity.
private double ToBinaryFloat(int precision, int minimumExponent, int maximumExponent)
@@ -172,7 +90,7 @@ private double ToBinaryFloat(int precision, int minimumExponent, int maximumExpo
BigInteger numerator = BigInteger.Abs(Numerator);
BigInteger denominator = Denominator;
- long exponent = numerator.GetBitLength() - denominator.GetBitLength();
+ long exponent = GetBitLength(numerator) - GetBitLength(denominator);
if (exponent > maximumExponent + 1L)
{
return Numerator.Sign * double.PositiveInfinity;
@@ -192,26 +110,59 @@ private double ToBinaryFloat(int precision, int minimumExponent, int maximumExpo
int shift = (int)Math.Max(exponent - precision + 1, minimumExponent - precision + 1);
BigInteger rounded = shift >= 0 ? RoundQuotient(numerator, denominator << shift) : RoundQuotient(numerator << -shift, denominator);
- if (rounded.GetBitLength() + shift - 1 > maximumExponent)
+ if (GetBitLength(rounded) + shift - 1 > maximumExponent)
{
return Numerator.Sign * double.PositiveInfinity;
}
- return Numerator.Sign * Math.ScaleB((double)rounded, shift);
+ return Numerator.Sign * ScaleRoundedBinary(rounded, shift);
+ }
+
+ private static long GetBitLength(BigInteger value)
+ {
+ byte[] bytes = value.ToByteArray();
+ int top = bytes.Length - 1;
+ while (top > 0 && bytes[top] == 0)
+ {
+ top--;
+ }
+
+ byte mostSignificant = bytes[top];
+ int bits = 0;
+ while (mostSignificant != 0)
+ {
+ bits++;
+ mostSignificant >>= 1;
+ }
+
+ return 8L * top + bits;
+ }
+
+ private static double ScaleRoundedBinary(BigInteger rounded, int shift)
+ {
+ long bitLength = GetBitLength(rounded);
+ long exponent = bitLength - 1 + shift;
+ ulong bits;
+ if (exponent >= -1022)
+ {
+ BigInteger significand = bitLength <= 53 ? rounded << (int)(53 - bitLength) : rounded >> (int)(bitLength - 53);
+ bits = ((ulong)(exponent + 1023) << 52) | ((ulong)significand & 0x000f_ffff_ffff_ffff);
+ }
+ else
+ {
+ bits = (ulong)(rounded << (shift + 1074));
+ }
+
+ return BitConverter.Int64BitsToDouble(unchecked((long)bits));
}
- private decimal ToDecimal(ConversionMode mode)
+ private decimal ToDecimal()
{
BigInteger numerator = BigInteger.Abs(Numerator);
BigInteger maximum = new(decimal.MaxValue);
if (numerator > maximum * Denominator)
{
- if (mode == ConversionMode.Checked)
- {
- throw new OverflowException("Rational exceeds the decimal range.");
- }
-
- return Numerator.Sign < 0 ? decimal.MinValue : decimal.MaxValue;
+ throw new OverflowException("Rational exceeds the decimal range.");
}
for (int scale = 28; scale >= 0; scale--)
diff --git a/src/DimensionalQuantities/Numerics/Rational.Text.cs b/src/DimensionalQuantities/Numerics/Rational.Text.cs
index d1bf823..a1bbe3b 100644
--- a/src/DimensionalQuantities/Numerics/Rational.Text.cs
+++ b/src/DimensionalQuantities/Numerics/Rational.Text.cs
@@ -1,6 +1,5 @@
using System.Globalization;
using System.Numerics;
-using System.Text;
namespace DimensionalQuantities.Numerics;
@@ -16,68 +15,38 @@ public string ToString(string? format, IFormatProvider? formatProvider)
return Denominator.IsOne ? numerator : numerator + "/" + Denominator.ToString(format, formatProvider);
}
- public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider)
- {
- string text = ToString(format.IsEmpty ? null : new(format), provider);
- if (!text.AsSpan().TryCopyTo(destination))
- {
- charsWritten = 0;
- return false;
- }
-
- charsWritten = text.Length;
- return true;
- }
-
- public bool TryFormat(Span utf8Destination, out int bytesWritten, ReadOnlySpan format, IFormatProvider? provider)
- {
- string text = ToString(format.IsEmpty ? null : new(format), provider);
- if (Encoding.UTF8.GetByteCount(text) > utf8Destination.Length)
- {
- bytesWritten = 0;
- return false;
- }
-
- bytesWritten = Encoding.UTF8.GetBytes(text, utf8Destination);
- return true;
- }
-
public static Rational Parse(string s, IFormatProvider? provider) => Parse(s, NumberStyles.Integer, provider);
- public static Rational Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s, NumberStyles.Integer, provider);
- public static Rational Parse(ReadOnlySpan utf8Text, IFormatProvider? provider) => Parse(Encoding.UTF8.GetString(utf8Text), provider);
public static Rational Parse(string s, NumberStyles style, IFormatProvider? provider)
{
- ArgumentNullException.ThrowIfNull(s);
- return Parse(s.AsSpan(), style, provider);
- }
+ if (s is null)
+ {
+ throw new ArgumentNullException(nameof(s));
+ }
- public static Rational Parse(ReadOnlySpan s, NumberStyles style, IFormatProvider? provider)
- {
int separator = s.IndexOf('/');
- return separator < 0 ? new(BigInteger.Parse(s, style, provider)) : new(BigInteger.Parse(s[..separator], style, provider), BigInteger.Parse(s[(separator + 1)..], style, provider));
+ return separator < 0 ? new(BigInteger.Parse(s, style, provider)) : new(BigInteger.Parse(s.Substring(0, separator), style, provider), BigInteger.Parse(s.Substring(separator + 1), style, provider));
}
- public static bool TryParse(string? s, IFormatProvider? provider, out Rational result) => TryParse(s.AsSpan(), NumberStyles.Integer, provider, out result);
-
- public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out Rational result) => TryParse(s, NumberStyles.Integer, provider, out result);
+ public static bool TryParse(string? s, IFormatProvider? provider, out Rational result) => TryParse(s, NumberStyles.Integer, provider, out result);
- public static bool TryParse(ReadOnlySpan utf8Text, IFormatProvider? provider, out Rational result) => TryParse(Encoding.UTF8.GetString(utf8Text), provider, out result);
-
- public static bool TryParse(string? s, NumberStyles style, IFormatProvider? provider, out Rational result) => TryParse(s.AsSpan(), style, provider, out result);
-
- public static bool TryParse(ReadOnlySpan s, NumberStyles style, IFormatProvider? provider, out Rational result)
+ public static bool TryParse(string? s, NumberStyles style, IFormatProvider? provider, out Rational result)
{
result = Zero;
+ if (s is null)
+ {
+ return false;
+ }
+
int separator = s.IndexOf('/');
- ReadOnlySpan numeratorText = separator < 0 ? s : s[..separator];
+ string numeratorText = separator < 0 ? s : s.Substring(0, separator);
if (!BigInteger.TryParse(numeratorText, style, provider, out BigInteger numerator))
{
return false;
}
BigInteger denominator = BigInteger.One;
- if (separator >= 0 && (!BigInteger.TryParse(s[(separator + 1)..], style, provider, out denominator) || denominator.IsZero))
+ if (separator >= 0 && (!BigInteger.TryParse(s.Substring(separator + 1), style, provider, out denominator) || denominator.IsZero))
{
return false;
}
diff --git a/src/DimensionalQuantities/Numerics/Rational.cs b/src/DimensionalQuantities/Numerics/Rational.cs
index c9f5e1e..9fde60d 100644
--- a/src/DimensionalQuantities/Numerics/Rational.cs
+++ b/src/DimensionalQuantities/Numerics/Rational.cs
@@ -3,7 +3,7 @@
namespace DimensionalQuantities.Numerics;
/// An exact, reduced fraction with a positive denominator. Default is zero.
-internal readonly partial record struct Rational : INumber
+internal readonly partial record struct Rational
{
// Offset storage makes the zero-initialised struct represent 0/1.
private readonly BigInteger _denominatorMinusOne;
@@ -107,4 +107,4 @@ public static Rational MinMagnitude(Rational x, Rational y)
public static Rational MaxMagnitudeNumber(Rational x, Rational y) => MaxMagnitude(x, y);
public static Rational MinMagnitudeNumber(Rational x, Rational y) => MinMagnitude(x, y);
-}
\ No newline at end of file
+}
diff --git a/src/DimensionalQuantities/TypeSystem/Quantity.cs b/src/DimensionalQuantities/TypeSystem/Quantity.cs
index 8625408..ef490ea 100644
--- a/src/DimensionalQuantities/TypeSystem/Quantity.cs
+++ b/src/DimensionalQuantities/TypeSystem/Quantity.cs
@@ -73,7 +73,7 @@ private static double GetScaleExponent(int length, int mass, int time, int elect
+ (time - SiBasePrefixExponents.Time) * dimension.Time + (electricCurrent - SiBasePrefixExponents.ElectricCurrent) * dimension.ElectricCurrent
+ (temperature - SiBasePrefixExponents.Temperature) * dimension.Temperature + (amountOfSubstance - SiBasePrefixExponents.AmountOfSubstance) * dimension.AmountOfSubstance
+ (luminousIntensity - SiBasePrefixExponents.LuminousIntensity) * dimension.LuminousIntensity;
- return double.CreateChecked(exponent) + overall;
+ return (double)exponent + overall;
}
private static MathematicalDimension ParseDimension()
diff --git a/src/DimensionalQuantities/TypeSystem/TypeSystemNumberCodec.cs b/src/DimensionalQuantities/TypeSystem/TypeSystemNumberCodec.cs
index 2993188..ed26dde 100644
--- a/src/DimensionalQuantities/TypeSystem/TypeSystemNumberCodec.cs
+++ b/src/DimensionalQuantities/TypeSystem/TypeSystemNumberCodec.cs
@@ -28,7 +28,10 @@ public static Type CreateType(Rational value)
public static Rational Parse(Type type)
{
- ArgumentNullException.ThrowIfNull(type);
+ if (type is null)
+ {
+ throw new ArgumentNullException(nameof(type));
+ }
int digit = Array.IndexOf(_terminalDigits, type);
if (digit >= 0)
diff --git a/src/DimensionalQuantities/Units/UnitExpression.cs b/src/DimensionalQuantities/Units/UnitExpression.cs
index 7852a20..a7710cd 100644
--- a/src/DimensionalQuantities/Units/UnitExpression.cs
+++ b/src/DimensionalQuantities/Units/UnitExpression.cs
@@ -93,13 +93,13 @@ public string GetSymbol()
public override int GetHashCode()
{
- HashCode hash = new();
+ int hash = 17;
foreach (UnitTerm term in Terms ?? [])
{
- hash.Add(term);
+ hash = unchecked(hash * 31 + term.GetHashCode());
}
- return hash.ToHashCode();
+ return hash;
}
private static void AppendRootName(StringBuilder name, Rational power)