Skip to content

Commit 25ff826

Browse files
tmilnthorpangularsen
authored andcommitted
Dynamic unit conversions (#588)
* Allowing custom unit conversion functions to be specified * Registering default base->unit and unit->base conversion functions in UnitConverter. Adding test for custom direct conversions * Whitespace consistency * Add some overloads and allowing source/target quantities to be different * Inline initialization and style update for field * PR feedback updates * Regen WRC * Use IQuantity.Value in test case * Rename typeparams, remove empty ctor * Micro optimization of lookup of IQuantity type It was looked up and wrapped N times * UnitConverter: Move static field initialization to static ctor They became a bit unwieldy. Move fields to top of class. * Add typed conversion function for same quantity conversion functions Since this is the most commonly used call path by far, it made sense to handle this case separately. The advantage is that the callback quantity is strongly typed. * Use typed conversions in RegisterDefaultConversions * Header update * Add xmldoc and minor renaming * Add test cases for Type parameters * QuantityParser: Make field readonly * UnitParser: Add test parsing HowMuch custom unit * UnitConverter: Echo quantity when converting to same unit, test custom quantity conversions * Port codegen scripts to C# The only difference is fixing the header of a file. * Regen code
1 parent 3501e1e commit 25ff826

File tree

104 files changed

+3453
-2
lines changed

Some content is hidden

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

104 files changed

+3453
-2
lines changed

CodeGen/Generators/UnitsNetGen/QuantityGenerator.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -904,6 +904,17 @@ IQuantity IQuantity.ToUnit(Enum unit)
904904
}}
905905
}}
906906
907+
/// <summary>
908+
/// Converts the current value + unit to the base unit.
909+
/// This is typically the first step in converting from one unit to another.
910+
/// </summary>
911+
/// <returns>The value in the base unit representation.</returns>
912+
internal {_quantity.Name} ToBaseUnit()
913+
{{
914+
var baseUnitValue = GetValueInBaseUnit();
915+
return new {_quantity.Name}(baseUnitValue, BaseUnit);
916+
}}
917+
907918
private {_valueType} GetValueAs({_unitEnumName} unit)
908919
{{
909920
if(Unit == unit)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Licensed under MIT No Attribution, see LICENSE file at the root.
2+
// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet.
3+
4+
using CodeGen.Helpers;
5+
using CodeGen.JsonTypes;
6+
7+
namespace CodeGen.Generators.UnitsNetGen
8+
{
9+
internal class UnitConverterGenerator : GeneratorBase
10+
{
11+
private readonly Quantity[] _quantities;
12+
13+
public UnitConverterGenerator(Quantity[] quantities)
14+
{
15+
_quantities = quantities;
16+
}
17+
18+
public override string Generate()
19+
{
20+
Writer.WL(GeneratedFileHeader);
21+
Writer.WL($@"
22+
using UnitsNet.Units;
23+
24+
// ReSharper disable RedundantCommaInArrayInitializer
25+
// ReSharper disable once CheckNamespace
26+
27+
namespace UnitsNet
28+
{{
29+
public sealed partial class UnitConverter
30+
{{
31+
/// <summary>
32+
/// Registers the default conversion functions in the given <see cref=""UnitConverter""/> instance.
33+
/// </summary>
34+
/// <param name=""unitConverter"">The <see cref=""UnitConverter""/> to register the default conversion functions in.</param>
35+
public static void RegisterDefaultConversions(UnitConverter unitConverter)
36+
{{");
37+
foreach (Quantity quantity in _quantities)
38+
foreach (Unit unit in quantity.Units)
39+
{
40+
Writer.WL(quantity.BaseUnit == unit.SingularName
41+
? $@"
42+
unitConverter.SetConversionFunction<{quantity.Name}>({quantity.Name}.BaseUnit, {quantity.Name}.BaseUnit, q => q);"
43+
: $@"
44+
unitConverter.SetConversionFunction<{quantity.Name}>({quantity.Name}.BaseUnit, {quantity.Name}Unit.{unit.SingularName}, q => q.ToUnit({quantity.Name}Unit.{unit.SingularName}));
45+
unitConverter.SetConversionFunction<{quantity.Name}>({quantity.Name}Unit.{unit.SingularName}, {quantity.Name}.BaseUnit, q => q.ToBaseUnit());");
46+
}
47+
48+
Writer.WL($@"
49+
}}
50+
}}
51+
}}");
52+
53+
return Writer.ToString();
54+
}
55+
}
56+
}

CodeGen/Generators/UnitsNetGenerator.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public static void Generate(string rootDir, Quantity[] quantities)
5858
GenerateUnitAbbreviationsCache(quantities, $"{outputDir}/UnitAbbreviationsCache.g.cs");
5959
GenerateQuantityType(quantities, $"{outputDir}/QuantityType.g.cs");
6060
GenerateStaticQuantity(quantities, $"{outputDir}/Quantity.g.cs");
61+
GenerateUnitConverter(quantities, $"{outputDir}/UnitConverter.g.cs");
6162

6263
var unitCount = quantities.SelectMany(q => q.Units).Count();
6364
Log.Information("");
@@ -119,5 +120,12 @@ private static void GenerateStaticQuantity(Quantity[] quantities, string filePat
119120
File.WriteAllText(filePath, content, Encoding.UTF8);
120121
Log.Information("Quantity.g.cs: ".PadRight(AlignPad) + "(OK)");
121122
}
123+
124+
private static void GenerateUnitConverter(Quantity[] quantities, string filePath)
125+
{
126+
var content = new UnitConverterGenerator(quantities).Generate();
127+
File.WriteAllText(filePath, content, Encoding.UTF8);
128+
Log.Information("UnitConverter.g.cs: ".PadRight(AlignPad) + "(OK)");
129+
}
122130
}
123131
}

UnitsNet.Tests/UnitConverterTest.cs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,115 @@
11
// Licensed under MIT No Attribution, see LICENSE file at the root.
22
// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet.
33

4+
using System;
5+
using UnitsNet.Tests.CustomQuantities;
6+
using UnitsNet.Units;
47
using Xunit;
58

69
namespace UnitsNet.Tests
710
{
811
public class UnitConverterTest
912
{
13+
[Fact]
14+
public void CustomConversionWithSameQuantityType()
15+
{
16+
Length ConversionFunction(Length from) => Length.FromInches(18);
17+
18+
var unitConverter = new UnitConverter();
19+
unitConverter.SetConversionFunction<Length>(LengthUnit.Meter, LengthUnit.Inch, ConversionFunction);
20+
21+
var foundConversionFunction = unitConverter.GetConversionFunction<Length>(LengthUnit.Meter, LengthUnit.Inch);
22+
var converted = foundConversionFunction(Length.FromMeters(1.0));
23+
24+
Assert.Equal(Length.FromInches(18), converted);
25+
}
26+
27+
[Fact]
28+
public void CustomConversionWithSameQuantityTypeByTypeParam()
29+
{
30+
Length ConversionFunction(Length from) => Length.FromInches(18);
31+
32+
var unitConverter = new UnitConverter();
33+
unitConverter.SetConversionFunction(LengthUnit.Meter, LengthUnit.Inch, (ConversionFunction<Length>) ConversionFunction);
34+
35+
var foundConversionFunction = unitConverter.GetConversionFunction(typeof(Length), LengthUnit.Meter, typeof(Length), LengthUnit.Inch);
36+
var converted = foundConversionFunction(Length.FromMeters(1.0));
37+
38+
Assert.Equal(Length.FromInches(18), converted);
39+
}
40+
41+
[Fact]
42+
public void CustomConversionWithDifferentQuantityTypes()
43+
{
44+
IQuantity ConversionFunction(IQuantity from) => Length.FromInches(18);
45+
46+
var unitConverter = new UnitConverter();
47+
unitConverter.SetConversionFunction<Mass, Length>(MassUnit.Grain, LengthUnit.Inch, ConversionFunction);
48+
49+
var foundConversionFunction = unitConverter.GetConversionFunction<Mass, Length>(MassUnit.Grain, LengthUnit.Inch);
50+
var converted = foundConversionFunction(Mass.FromGrains(100));
51+
52+
Assert.Equal(Length.FromInches(18), converted);
53+
}
54+
55+
[Fact]
56+
public void CustomConversionWithDifferentQuantityTypesByTypeParam()
57+
{
58+
IQuantity ConversionFunction(IQuantity from) => Length.FromInches(18);
59+
60+
var unitConverter = new UnitConverter();
61+
unitConverter.SetConversionFunction<Mass, Length>(MassUnit.Grain, LengthUnit.Inch, ConversionFunction);
62+
63+
var foundConversionFunction = unitConverter.GetConversionFunction(typeof(Mass), MassUnit.Grain, typeof(Length), LengthUnit.Inch);
64+
var converted = foundConversionFunction(Mass.FromGrains(100));
65+
66+
Assert.Equal(Length.FromInches(18), converted);
67+
}
68+
69+
[Fact]
70+
public void TryCustomConversionForOilBarrelsToUsGallons()
71+
{
72+
Volume ConversionFunction(Volume from) => Volume.FromUsGallons(from.Value * 42);
73+
74+
var unitConverter = new UnitConverter();
75+
unitConverter.SetConversionFunction<Volume>(VolumeUnit.OilBarrel, VolumeUnit.UsGallon, ConversionFunction);
76+
77+
var foundConversionFunction = unitConverter.GetConversionFunction<Volume>(VolumeUnit.OilBarrel, VolumeUnit.UsGallon);
78+
var converted = foundConversionFunction(Volume.FromOilBarrels(1));
79+
80+
Assert.Equal(Volume.FromUsGallons(42), converted);
81+
}
82+
83+
[Fact]
84+
public void ConversionToSameUnit_ReturnsSameQuantity()
85+
{
86+
var unitConverter = new UnitConverter();
87+
88+
var foundConversionFunction = unitConverter.GetConversionFunction<HowMuch>(HowMuchUnit.ATon, HowMuchUnit.ATon);
89+
var converted = foundConversionFunction(new HowMuch(39, HowMuchUnit.Some)); // Intentionally pass the wrong unit here, to test that the exact same quantity is returned
90+
91+
Assert.Equal(39, converted.Value);
92+
Assert.Equal(HowMuchUnit.Some, converted.Unit);
93+
}
94+
95+
[Theory]
96+
[InlineData(1, HowMuchUnit.Some, HowMuchUnit.Some, 1)]
97+
[InlineData(1, HowMuchUnit.Some, HowMuchUnit.ATon, 2)]
98+
[InlineData(1, HowMuchUnit.Some, HowMuchUnit.AShitTon, 10)]
99+
public void ConversionForUnitsOfCustomQuantity(double fromValue, Enum fromUnit, Enum toUnit, double expectedValue)
100+
{
101+
// Intentionally don't map conversion Some->Some, it is not necessary
102+
var unitConverter = new UnitConverter();
103+
unitConverter.SetConversionFunction<HowMuch>(HowMuchUnit.Some, HowMuchUnit.ATon, x => new HowMuch(x.Value * 2, HowMuchUnit.ATon));
104+
unitConverter.SetConversionFunction<HowMuch>(HowMuchUnit.Some, HowMuchUnit.AShitTon, x => new HowMuch(x.Value * 10, HowMuchUnit.AShitTon));
105+
106+
var foundConversionFunction = unitConverter.GetConversionFunction<HowMuch>(fromUnit, toUnit);
107+
var converted = foundConversionFunction(new HowMuch(fromValue, fromUnit));
108+
109+
Assert.Equal(expectedValue, converted.Value);
110+
Assert.Equal(toUnit, converted.Unit);
111+
}
112+
10113
[Theory]
11114
[InlineData(0, 0, "length", "meter", "centimeter")]
12115
[InlineData(0, 0, "Length", "Meter", "Centimeter")]

UnitsNet.Tests/UnitParserTests.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Globalization;
6+
using UnitsNet.Tests.CustomQuantities;
67
using UnitsNet.Units;
78
using Xunit;
89

@@ -124,5 +125,17 @@ public void ParseMassUnit_GivenCulture(string str, string cultureName, Enum expe
124125
{
125126
Assert.Equal(expectedUnit, UnitParser.Default.Parse<MassUnit>(str, new CultureInfo(cultureName)));
126127
}
128+
129+
[Fact]
130+
public void Parse_MappedCustomUnit()
131+
{
132+
var unitAbbreviationsCache = new UnitAbbreviationsCache();
133+
unitAbbreviationsCache.MapUnitToAbbreviation(HowMuchUnit.Some, "fooh");
134+
var unitParser = new UnitParser(unitAbbreviationsCache);
135+
136+
var parsedUnit = unitParser.Parse<HowMuchUnit>("fooh");
137+
138+
Assert.Equal(HowMuchUnit.Some, parsedUnit);
139+
}
127140
}
128141
}

UnitsNet/CustomCode/QuantityParser.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ internal class QuantityParser
2525
private const NumberStyles ParseNumberStyles = NumberStyles.Number | NumberStyles.Float | NumberStyles.AllowExponent;
2626

2727
private readonly UnitAbbreviationsCache _unitAbbreviationsCache;
28-
private UnitParser _unitParser;
28+
private readonly UnitParser _unitParser;
2929

3030
public static QuantityParser Default { get; }
3131

UnitsNet/GeneratedCode/Quantities/Acceleration.g.cs

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

UnitsNet/GeneratedCode/Quantities/AmountOfSubstance.g.cs

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

UnitsNet/GeneratedCode/Quantities/AmplitudeRatio.g.cs

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

UnitsNet/GeneratedCode/Quantities/Angle.g.cs

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)