Skip to content

Commit 3b610bc

Browse files
authored
Move IQuantity conversion members to extensions (#1696)
## Why This extracts the independently useful `IQuantity.As()` / `ToUnit()` interface cleanup from #1544 so that the larger fractional quantity value PR carries less unrelated API and generated-code churn. Conversion is behavior rather than quantity state, and requiring every custom `IQuantity` implementation to implement these members adds unnecessary burden. Keeping the calls as extensions preserves normal source usage while removing those members from the interface contract. The generated concrete `As()` / `ToUnit()` methods and their conversion formulas deliberately remain in place. Removing those depends on the `UnitConverter` redesign in #1544 and is not a safe standalone change on current `master`. A conservative tree-to-tree comparison now reduces #1544 by about 3,098 changed lines. Of those, 3,096 lines are exact generated overlap (24 lines in each of 129 quantities); the generator, interface, and custom-fixture deletions overlap further. The final total depends on resolving the temporary current-master `UnitConverter` compatibility bridge when #1544 is synchronized. ## Changes - Remove `As()` and `ToUnit()` members from `IQuantity`, `IQuantity<TUnit>`, and the self-typed interface default implementation. - Add equivalent `QuantityExtensions` overloads for untyped, typed, and self-typed quantity references. - Support an explicitly supplied `UnitConverter` for self-typed generic conversions. - Route extension conversions through the current `UnitConverter`, including conversion through a quantity's base unit when no direct conversion is registered. - Remove the generated explicit interface shims while retaining all concrete generated conversion methods. - Regenerate all quantity sources. - Simplify the custom `HowMuch` test quantity now that interface implementations no longer need conversion members. - Align wrong-unit interface-call behavior with `UnitConverter` by throwing `UnitNotFoundException`. This intentionally changes the binary interface contract; ordinary source calls continue to resolve through extensions in the `UnitsNet` namespace. ## Tests Added or changed tests cover: - `As()` through both `IQuantity` and `IQuantity<TUnit>` references. - `ToUnit()` through an `IQuantity` reference starting from a non-base unit. - Self-typed generic `ToUnit()` with a custom `UnitConverter`. - Wrong-unit `As()` and `ToUnit()` calls reporting `UnitNotFoundException`. Validation performed: - `dotnet run --project CodeGen` - `dotnet build UnitsNet.slnx --configuration Release --no-restore` - Focused conversion/API tests on .NET 10: 1,854 passed - Focused conversion/API tests on .NET Framework 4.8: 1,851 passed - `IQuantityTests` on .NET 10: 20 passed - `IQuantityTests` on .NET Framework 4.8: 17 passed - Full core test suite on .NET 10: 42,876 passed, 20 skipped, 0 failed
1 parent 964bac1 commit 3b610bc

135 files changed

Lines changed: 315 additions & 3335 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CodeGen/Generators/UnitsNetGen/QuantityGenerator.cs

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,7 +1024,7 @@ public double As({_unitEnumName} unit)
10241024

10251025
Writer.WL( $@"
10261026
1027-
/// <inheritdoc cref=""IQuantity.As(UnitKey)""/>
1027+
/// <inheritdoc cref=""QuantityExtensions.As(IQuantity, UnitKey)""/>
10281028
public double As(UnitKey unitKey)
10291029
{{
10301030
return As(unitKey.ToUnit<{_unitEnumName}>());
@@ -1129,30 +1129,6 @@ private bool TryToUnit({_unitEnumName} unit, [NotNullWhen(true)] out {_quantity.
11291129
}}
11301130
");
11311131
Writer.WL($@"
1132-
#region Explicit implementations
1133-
1134-
double IQuantity.As(Enum unit)
1135-
{{
1136-
if (unit is not {_unitEnumName} typedUnit)
1137-
throw new ArgumentException($""The given unit is of type {{unit.GetType()}}. Only {{typeof({_unitEnumName})}} is supported."", nameof(unit));
1138-
1139-
return As(typedUnit);
1140-
}}
1141-
1142-
/// <inheritdoc />
1143-
IQuantity IQuantity.ToUnit(Enum unit)
1144-
{{
1145-
if (!(unit is {_unitEnumName} typedUnit))
1146-
throw new ArgumentException($""The given unit is of type {{unit.GetType()}}. Only {{typeof({_unitEnumName})}} is supported."", nameof(unit));
1147-
1148-
return ToUnit(typedUnit, DefaultConversionFunctions);
1149-
}}
1150-
1151-
/// <inheritdoc />
1152-
IQuantity<{_unitEnumName}> IQuantity<{_unitEnumName}>.ToUnit({_unitEnumName} unit) => ToUnit(unit);
1153-
1154-
#endregion
1155-
11561132
#endregion
11571133
");
11581134
}

Docs/upgrading-from-5.x-to-6.x.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ If there is sufficient demand for supporting any number type like `float`, `deci
2929
- Remove `IValueQuantity<out TValueType>`
3030
- Change `IQuantity<TSelf, TUnitType, out TValueType>` to `IQuantity<TSelf, TUnitType>`
3131
- Change `IArithmeticQuantity<TSelf, TUnitType, TValueType>` to `IArithmeticQuantity<TSelf, TUnitType>`
32+
- Move `IQuantity.As()` and `IQuantity.ToUnit()` from the quantity interfaces to `QuantityExtensions` #1696
3233
- Remove obsolete units #1372
3334
- `CoefficientOfThermalExpansion.InverseKelvin`, `InverseDegreeCelsius`, `InverseDegreeFahrenheit`
3435
- `HeatTransferCoefficient.BtuPerSquareFootDegreeFahrenheit`
@@ -43,10 +44,12 @@ If there is sufficient demand for supporting any number type like `float`, `deci
4344
### Source incompatible
4445

4546
- `IQuantity.UnitInfo` is now a interface default member on .NET5+, and may compete with any custom property implemented in third party quantities #1649
47+
- Custom quantities that explicitly implement `IQuantity.As()`, `IQuantity.ToUnit()`, `IQuantity<TUnitType>.As()` or `IQuantity<TUnitType>.ToUnit()` must remove those explicit interface implementations. The methods may remain as ordinary members if they are also part of the custom quantity's public API. #1696
4648

4749
### Behavioral change
4850

49-
None.
51+
- Calls to `.As()` and `.ToUnit()` through an `IQuantity` or `IQuantity<TUnitType>` reference now use the `QuantityExtensions` methods and `UnitConverter.Default`. They no longer dispatch to type-specific methods defined by a custom quantity. Custom quantities that need these calls to support conversion must register their conversion functions with `UnitConverter.Default`. #1696
52+
- Calling these extension methods with an incompatible unit type now throws `UnitNotFoundException` instead of `ArgumentException`. Code that catches `ArgumentException` around interface-based conversions may need to be updated. #1696
5053

5154
### Description of different kinds of incompatible changes
5255

UnitsNet.Tests/CustomCode/IQuantityTests.cs

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,60 @@ namespace UnitsNet.Tests;
77
public partial class IQuantityTests
88
{
99
[Fact]
10-
public void As_GivenWrongUnitType_ThrowsArgumentException()
10+
public void As_GivenWrongUnitType_ThrowsUnitNotFoundException()
1111
{
12-
Assert.All(Quantity.Infos.Select(x => x.Zero), quantity => { Assert.Throws<ArgumentException>(() => quantity.As(ComparisonType.Absolute)); });
12+
Assert.All(Quantity.Infos.Select(x => x.Zero), quantity => { Assert.Throws<UnitNotFoundException>(() => quantity.As(ComparisonType.Absolute)); });
1313
}
1414

1515
[Fact]
16-
public void ToUnit_GivenWrongUnitType_ThrowsArgumentException()
16+
public void ToUnit_GivenWrongUnitType_ThrowsUnitNotFoundException()
1717
{
18-
Assert.All(Quantity.Infos.Select(x => x.Zero), quantity => { Assert.Throws<ArgumentException>(() => quantity.ToUnit(ComparisonType.Absolute)); });
18+
Assert.All(Quantity.Infos.Select(x => x.Zero),
19+
quantity => { Assert.Throws<UnitNotFoundException>(() => quantity.ToUnit(ComparisonType.Absolute)); });
20+
}
21+
22+
[Fact]
23+
public void As_InterfaceReferences_ReturnConvertedValue()
24+
{
25+
var mass = Mass.FromKilograms(1);
26+
IQuantity quantity = mass;
27+
IQuantity<MassUnit> typedQuantity = mass;
28+
29+
Assert.Equal(1000, quantity.As(MassUnit.Gram));
30+
Assert.Equal(1000, typedQuantity.As(MassUnit.Gram));
31+
}
32+
33+
[Fact]
34+
public void ToUnit_IQuantityFromNonBaseUnit_ReturnsConvertedQuantity()
35+
{
36+
IQuantity quantity = Length.FromKilometers(1);
37+
38+
IQuantity convertedQuantity = quantity.ToUnit(LengthUnit.Centimeter);
39+
40+
Assert.Equal(100_000, convertedQuantity.Value);
41+
Assert.Equal(LengthUnit.Centimeter, convertedQuantity.Unit);
42+
}
43+
44+
[Fact]
45+
public void ToUnit_GenericConstraintWithCustomConverter_UsesProvidedConverter()
46+
{
47+
var converter = new UnitConverter();
48+
converter.SetConversionFunction<Length>(
49+
LengthUnit.Meter,
50+
LengthUnit.Centimeter,
51+
_ => Length.FromCentimeters(123));
52+
53+
Length convertedQuantity = ConvertToUnit(Length.FromMeters(1), LengthUnit.Centimeter, converter);
54+
55+
Assert.Equal(123, convertedQuantity.Value);
56+
Assert.Equal(LengthUnit.Centimeter, convertedQuantity.Unit);
57+
58+
static TQuantity ConvertToUnit<TQuantity, TUnit>(TQuantity quantity, TUnit unit, UnitConverter unitConverter)
59+
where TQuantity : IQuantity<TQuantity, TUnit>
60+
where TUnit : struct, Enum
61+
{
62+
return quantity.ToUnit(unit, unitConverter);
63+
}
1964
}
2065

2166
[Fact]

UnitsNet.Tests/CustomQuantities/HowMuch.cs

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,6 @@ public static HowMuch From(double value, HowMuchUnit unit)
2020
return new HowMuch(value, unit);
2121
}
2222

23-
public double As(HowMuchUnit unit)
24-
{
25-
throw new NotImplementedException();
26-
}
27-
2823
public HowMuchUnit Unit { get; }
2924

3025
public double Value { get; }
@@ -64,23 +59,6 @@ UnitKey IQuantity.UnitKey
6459
get => UnitKey.ForUnit(Unit);
6560
}
6661

67-
public double As(Enum unit) => Convert.ToDouble(unit);
68-
public double As(UnitKey unitKey)
69-
{
70-
return As(unitKey.ToUnit<HowMuchUnit>());
71-
}
72-
73-
public IQuantity ToUnit(Enum unit)
74-
{
75-
if (unit is HowMuchUnit howMuchUnit) return new HowMuch(As(unit), howMuchUnit);
76-
throw new ArgumentException("Must be of type HowMuchUnit.", nameof(unit));
77-
}
78-
79-
public IQuantity<HowMuchUnit> ToUnit(HowMuchUnit unit)
80-
{
81-
throw new NotImplementedException();
82-
}
83-
8462
public override string ToString()
8563
{
8664
return $"{Value} {Unit}";

UnitsNet/Extensions/QuantityExtensions.cs

Lines changed: 132 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,80 @@ public static UnitInfo<TQuantity, TUnit> GetUnitInfo<TQuantity, TUnit>(this IQua
7878
#endif
7979
}
8080

81-
/// <inheritdoc cref="IQuantity.As(UnitKey)" />
82-
/// <remarks>This should be using UnitConverter.Default.ConvertValue(quantity, toUnit) </remarks>
81+
/// <summary>
82+
/// Gets the value of a quantity in the specified unit.
83+
/// </summary>
8384
internal static double GetValue<TQuantity>(this TQuantity quantity, UnitKey toUnit)
8485
where TQuantity : IQuantity
8586
{
86-
return quantity.As(toUnit);
87+
return UnitConverter.Default.ConvertValue(quantity, toUnit);
88+
}
89+
90+
private static IQuantity ConvertTo(this UnitConverter converter, IQuantity quantity, UnitKey toUnit)
91+
{
92+
if (quantity.UnitKey == toUnit)
93+
{
94+
return quantity;
95+
}
96+
97+
var targetUnit = (Enum)toUnit;
98+
Type quantityType = quantity.GetType();
99+
if (converter.TryGetConversionFunction(quantityType, quantity.Unit, quantityType, targetUnit, out ConversionFunction? conversionFunction))
100+
{
101+
return conversionFunction(quantity);
102+
}
103+
104+
#pragma warning disable CS0618 // Type or member is obsolete
105+
UnitKey baseUnit = quantity.QuantityInfo.BaseUnitInfo.UnitKey;
106+
#pragma warning restore CS0618 // Type or member is obsolete
107+
if (quantity.UnitKey != baseUnit)
108+
{
109+
IQuantity quantityInBaseUnit = converter.ConvertTo(quantity, baseUnit);
110+
return converter.ConvertTo(quantityInBaseUnit, toUnit);
111+
}
112+
113+
throw new UnitNotFoundException($"Can't convert {quantity.Unit} to {targetUnit}.");
114+
}
115+
116+
/// <summary>
117+
/// Converts a quantity value to the specified unit.
118+
/// </summary>
119+
/// <param name="converter">The converter to use.</param>
120+
/// <param name="quantity">The quantity to convert.</param>
121+
/// <param name="toUnit">The target unit.</param>
122+
/// <returns>The converted value.</returns>
123+
internal static double ConvertValue<TQuantity>(this UnitConverter converter, TQuantity quantity, UnitKey toUnit)
124+
where TQuantity : IQuantity
125+
{
126+
return converter.ConvertTo(quantity, toUnit).Value;
127+
}
128+
129+
/// <summary>
130+
/// Converts a quantity to the specified unit while preserving its concrete type.
131+
/// </summary>
132+
/// <param name="converter">The converter to use.</param>
133+
/// <param name="quantity">The quantity to convert.</param>
134+
/// <param name="toUnit">The target unit.</param>
135+
/// <returns>The converted quantity.</returns>
136+
internal static TQuantity ConvertToUnit<TQuantity>(this UnitConverter converter, TQuantity quantity, UnitKey toUnit)
137+
where TQuantity : IQuantityOfType<TQuantity>
138+
{
139+
return (TQuantity)converter.ConvertTo(quantity, toUnit);
140+
}
141+
142+
/// <summary>
143+
/// Gets the value of a quantity in the specified unit.
144+
/// </summary>
145+
/// <typeparam name="TQuantity">The quantity type.</typeparam>
146+
/// <typeparam name="TUnit">The unit enum type.</typeparam>
147+
/// <param name="quantity">The quantity to convert.</param>
148+
/// <param name="unit">The target unit.</param>
149+
/// <returns>The converted value.</returns>
150+
public static double As<TQuantity, TUnit>(this TQuantity quantity, TUnit unit)
151+
where TQuantity : IQuantity<TQuantity, TUnit>
152+
where TUnit : struct, Enum
153+
{
154+
return UnitConverter.Default.ConvertValue(quantity, UnitKey.ForUnit(unit));
87155
}
88156

89157
/// <summary>
@@ -110,6 +178,32 @@ public static double As<TQuantity>(this TQuantity quantity, UnitSystem unitSyste
110178
#endif
111179
}
112180

181+
/// <summary>
182+
/// Converts a quantity to the specified unit while preserving its concrete type.
183+
/// </summary>
184+
/// <typeparam name="TQuantity">The quantity type.</typeparam>
185+
/// <typeparam name="TUnit">The unit enum type.</typeparam>
186+
/// <param name="quantity">The quantity to convert.</param>
187+
/// <param name="unit">The target unit.</param>
188+
/// <returns>The converted quantity.</returns>
189+
public static TQuantity ToUnit<TQuantity, TUnit>(this TQuantity quantity, TUnit unit)
190+
where TQuantity : IQuantity<TQuantity, TUnit>
191+
where TUnit : struct, Enum
192+
{
193+
return quantity.ToUnit(unit, UnitConverter.Default);
194+
}
195+
196+
/// <inheritdoc cref="ToUnit{TQuantity,TUnit}(TQuantity,TUnit)" />
197+
/// <param name="quantity">The quantity to convert.</param>
198+
/// <param name="unit">The target unit.</param>
199+
/// <param name="unitConverter">The converter to use.</param>
200+
public static TQuantity ToUnit<TQuantity, TUnit>(this TQuantity quantity, TUnit unit, UnitConverter unitConverter)
201+
where TQuantity : IQuantity<TQuantity, TUnit>
202+
where TUnit : struct, Enum
203+
{
204+
return unitConverter.ConvertToUnit(quantity, UnitKey.ForUnit(unit));
205+
}
206+
113207
/// <summary>
114208
/// Converts the specified quantity to a new quantity with a unit determined by the given <see cref="UnitSystem" />.
115209
/// </summary>
@@ -145,6 +239,28 @@ public static TQuantity ToUnit<TQuantity>(this TQuantity quantity, UnitSystem un
145239
#endif
146240
}
147241

242+
/// <summary>
243+
/// Gets the value of a quantity in the specified unit.
244+
/// </summary>
245+
/// <param name="quantity">The quantity to convert.</param>
246+
/// <param name="unit">The target unit.</param>
247+
/// <returns>The converted value.</returns>
248+
public static double As(this IQuantity quantity, UnitKey unit)
249+
{
250+
return UnitConverter.Default.ConvertValue(quantity, unit);
251+
}
252+
253+
/// <summary>
254+
/// Converts a quantity to the specified unit.
255+
/// </summary>
256+
/// <param name="quantity">The quantity to convert.</param>
257+
/// <param name="unit">The target unit.</param>
258+
/// <returns>The converted quantity.</returns>
259+
public static IQuantity ToUnit(this IQuantity quantity, UnitKey unit)
260+
{
261+
return UnitConverter.Default.ConvertTo(quantity, unit);
262+
}
263+
148264
/// <summary>
149265
/// Converts the specified quantity to a new quantity with a unit determined by the given <see cref="UnitSystem" />.
150266
/// </summary>
@@ -170,6 +286,19 @@ public static IQuantity ToUnit(this IQuantity quantity, UnitSystem unitSystem)
170286
return quantityInfo.From(quantity.As(unitKey), unitKey);
171287
}
172288

289+
/// <summary>
290+
/// Converts a quantity to the specified unit.
291+
/// </summary>
292+
/// <typeparam name="TUnit">The unit enum type.</typeparam>
293+
/// <param name="quantity">The quantity to convert.</param>
294+
/// <param name="unit">The target unit.</param>
295+
/// <returns>The converted quantity.</returns>
296+
public static IQuantity<TUnit> ToUnit<TUnit>(this IQuantity<TUnit> quantity, TUnit unit)
297+
where TUnit : struct, Enum
298+
{
299+
return (IQuantity<TUnit>)UnitConverter.Default.ConvertTo(quantity, UnitKey.ForUnit(unit));
300+
}
301+
173302
/// <summary>
174303
/// Converts the specified quantity to a new quantity with a unit determined by the given <see cref="UnitSystem" />.
175304
/// </summary>

UnitsNet/GeneratedCode/Quantities/AbsorbedDoseOfIonizingRadiation.g.cs

Lines changed: 1 addition & 25 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)