Skip to content

Commit 84f06cf

Browse files
committed
✨Add arithmetic operators for inverse/relational quantities
Fixes #1566 Quantities with inverse relations, such as Length and ReciprocalLength, were missing arithmetic operators for things like `Length * ReciprocalLength = double` and `ReciprocalLength / double = Length`. ### Changes - Add missing arithmetic for quantities with inverse relations - Affected: `Density`, `ElectricConductivity`, `ElectricResistivity`, `Length`, `ReciprocalLength`, `Area`, `ReciprocalArea` - Implement `IMultiplyOperators<ReciprocalLength, Length, double>` or similar - Add `public static Length operator /(double value, ReciprocalLength reciprocalLength)` or similar - Add `public static double operator *(ReciprocalLength reciprocalLength, Length length)` or similar - Change `UnitRelations.json` to use `double` instead of `1`, and a new `-- Inverse` configuration flag - Add `IsInverse` and `IsDerived` properties to `QuantityRelation` - Add test `ArithmeticOperators_Relational` for each quantity - Add test `InverseMethod` for each quantity
1 parent 0da4a5a commit 84f06cf

File tree

77 files changed

+1012
-54
lines changed

Some content is hidden

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

77 files changed

+1012
-54
lines changed

CodeGen/Generators/QuantityRelationsParser.cs

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ internal static class QuantityRelationsParser
3232
/// </summary>
3333
/// <example>
3434
/// [
35-
/// "1 = Length.Meter * ReciprocalLength.InverseMeter"
35+
/// "double = Length.Meter * ReciprocalLength.InverseMeter -- Inverse"
3636
/// "Power.Watt = ElectricPotential.Volt * ElectricCurrent.Ampere",
3737
/// "Mass.Kilogram = MassConcentration.KilogramPerCubicMeter * Volume.CubicMeter -- NoInferredDivision",
3838
/// ]
@@ -46,22 +46,23 @@ public static void ParseAndApplyRelations(string rootDir, Quantity[] quantities)
4646
// Add double and 1 as pseudo-quantities to validate relations that use them.
4747
var pseudoQuantity = new Quantity { Name = null!, Units = [new Unit { SingularName = null! }] };
4848
quantityDictionary["double"] = pseudoQuantity with { Name = "double" };
49-
quantityDictionary["1"] = pseudoQuantity with { Name = "1" };
5049

5150
var relations = ParseRelations(rootDir, quantityDictionary);
5251

5352
// Because multiplication is commutative, we can infer the other operand order.
5453
relations.AddRange(relations
55-
.Where(r => r.Operator is "*" or "inverse" && r.LeftQuantity != r.RightQuantity)
54+
.Where(r => r.Operator is "*" && r.LeftQuantity != r.RightQuantity)
5655
.Select(r => r with
5756
{
5857
LeftQuantity = r.RightQuantity,
5958
LeftUnit = r.RightUnit,
6059
RightQuantity = r.LeftQuantity,
6160
RightUnit = r.LeftUnit,
61+
IsDerived = true,
62+
// IsInverse is propagated, to also generate Inverse() method for the right hand quantity.
6263
})
6364
.ToList());
64-
65+
6566
// We can infer division relations from multiplication relations.
6667
relations.AddRange(relations
6768
.Where(r => r is { Operator: "*", NoInferredDivision: false })
@@ -72,6 +73,8 @@ public static void ParseAndApplyRelations(string rootDir, Quantity[] quantities)
7273
LeftUnit = r.ResultUnit,
7374
ResultQuantity = r.LeftQuantity,
7475
ResultUnit = r.LeftUnit,
76+
IsDerived = true,
77+
IsInverse = false, // Don't propagate for inferred division relations, Inverse() methods should only be generated the left and right hand quantities in the original definition.
7578
})
7679
// Skip division between equal quantities because the ratio is already generated as part of the Arithmetic Operators.
7780
.Where(r => r.LeftQuantity != r.RightQuantity)
@@ -91,7 +94,7 @@ public static void ParseAndApplyRelations(string rootDir, Quantity[] quantities)
9194
var list = string.Join("\n ", duplicates);
9295
throw new UnitsNetCodeGenException($"Duplicate inferred relations:\n {list}");
9396
}
94-
97+
9598
var ambiguous = relations
9699
.GroupBy(r => $"{r.LeftQuantity.Name} {r.Operator} {r.RightQuantity.Name}")
97100
.Where(g => g.Count() > 1)
@@ -170,14 +173,14 @@ private static QuantityRelation ParseRelation(string relationString, IReadOnlyDi
170173
var rightUnit = GetUnit(rightQuantity, right.ElementAtOrDefault(1));
171174
var resultUnit = GetUnit(resultQuantity, result.ElementAtOrDefault(1));
172175

173-
if (resultQuantity.Name == "1")
174-
{
175-
@operator = "inverse";
176-
}
176+
// Configuration segments are the parts after the "--" in the relation string.
177+
// Example: "double = Length.Meter * ReciprocalLength.InverseMeter -- Inverse" => ["Inverse"]
178+
var configurationSegments = relationString.Split("--").Last().Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries);
177179

178180
return new QuantityRelation
179181
{
180-
NoInferredDivision = segments.Contains("NoInferredDivision"),
182+
NoInferredDivision = configurationSegments.Contains("NoInferredDivision"),
183+
IsInverse = configurationSegments.Contains("Inverse"),
181184
Operator = @operator,
182185
LeftQuantity = leftQuantity,
183186
LeftUnit = leftUnit,
@@ -210,4 +213,4 @@ Unit GetUnit(Quantity quantity, string? unitName)
210213
}
211214
}
212215
}
213-
}
216+
}

CodeGen/Generators/UnitsNetGen/QuantityGenerator.cs

Lines changed: 41 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -788,61 +788,65 @@ private void GenerateRelationalOperators()
788788
Writer.WL($@"
789789
#region Relational Operators
790790
");
791-
792-
foreach (QuantityRelation relation in _quantity.Relations)
791+
// Generate an Inverse() method for the original relation definition, if the quantity can be inverted to another quantity.
792+
// E.g. double = Length.Meter * ReciprocalLength.InverseMeter
793+
if (_quantity.Relations.FirstOrDefault(x => x is { IsInverse: true, }) is { } inverseRelation)
793794
{
794-
if (relation.Operator == "inverse")
795-
{
796-
Writer.WL($@"
795+
Unit unit = inverseRelation.LeftUnit;
796+
Quantity inverseQuantity = inverseRelation.RightQuantity;
797+
Unit inverseUnit = inverseRelation.RightUnit;
798+
799+
Writer.WL($@"
797800
/// <summary>Calculates the inverse of this quantity.</summary>
798-
/// <returns>The corresponding inverse quantity, <see cref=""{relation.RightQuantity.Name}""/>.</returns>
799-
public {relation.RightQuantity.Name} Inverse()
801+
/// <returns>The corresponding inverse quantity, <see cref=""{inverseQuantity.Name}""/>.</returns>
802+
public {inverseQuantity.Name} Inverse()
800803
{{
801-
return {relation.RightQuantity.Name}.From{relation.RightUnit.PluralName}(1 / {relation.LeftUnit.PluralName});
804+
return {inverseQuantity.Name}.From{inverseUnit.PluralName}(1 / {unit.PluralName});
802805
}}
803806
");
804-
}
805-
else
806-
{
807-
var leftParameter = relation.LeftQuantity.Name.ToCamelCase();
808-
var leftConversionProperty = relation.LeftUnit.PluralName;
809-
var rightParameter = relation.RightQuantity.Name.ToCamelCase();
810-
var rightConversionProperty = relation.RightUnit.PluralName;
807+
}
811808

812-
if (leftParameter == rightParameter)
813-
{
814-
leftParameter = "left";
815-
rightParameter = "right";
816-
}
809+
// Generate arithmetic operator overloads for each relation, including inverse relations.
810+
foreach (QuantityRelation relation in _quantity.Relations)
811+
{
812+
var leftParameter = relation.LeftQuantity.Name.ToCamelCase();
813+
var leftConversionProperty = relation.LeftUnit.PluralName;
814+
var rightParameter = relation.RightQuantity.Name.ToCamelCase();
815+
var rightConversionProperty = relation.RightUnit.PluralName;
816+
817+
if (leftParameter == rightParameter)
818+
{
819+
leftParameter = "left";
820+
rightParameter = "right";
821+
}
817822

818-
var leftPart = $"{leftParameter}.{leftConversionProperty}";
819-
var rightPart = $"{rightParameter}.{rightConversionProperty}";
823+
var leftPart = $"{leftParameter}.{leftConversionProperty}";
824+
var rightPart = $"{rightParameter}.{rightConversionProperty}";
820825

821-
if (leftParameter is "double")
822-
{
823-
leftParameter = leftPart = "value";
824-
}
826+
if (leftParameter is "double")
827+
{
828+
leftParameter = leftPart = "value";
829+
}
825830

826-
if (rightParameter is "double")
827-
{
828-
rightParameter = rightPart = "value";
829-
}
831+
if (rightParameter is "double")
832+
{
833+
rightParameter = rightPart = "value";
834+
}
830835

831-
var expression = $"{leftPart} {relation.Operator} {rightPart}";
836+
var expression = $"{leftPart} {relation.Operator} {rightPart}";
832837

833-
if (relation.ResultQuantity.Name is not "double")
834-
{
835-
expression = $"{relation.ResultQuantity.Name}.From{relation.ResultUnit.PluralName}({expression})";
836-
}
838+
if (relation.ResultQuantity.Name is not "double")
839+
{
840+
expression = $"{relation.ResultQuantity.Name}.From{relation.ResultUnit.PluralName}({expression})";
841+
}
837842

838-
Writer.WL($@"
843+
Writer.WL($@"
839844
/// <summary>Get <see cref=""{relation.ResultQuantity.Name}""/> from <see cref=""{relation.LeftQuantity.Name}""/> {relation.Operator} <see cref=""{relation.RightQuantity.Name}""/>.</summary>
840845
public static {relation.ResultQuantity.Name} operator {relation.Operator}({relation.LeftQuantity.Name} {leftParameter}, {relation.RightQuantity.Name} {rightParameter})
841846
{{
842847
return {expression};
843848
}}
844849
");
845-
}
846850
}
847851

848852
Writer.WL($@"

CodeGen/Generators/UnitsNetGen/UnitTestBaseClassGenerator.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,62 @@ public void ArithmeticOperators()
884884
AssertEx.EqualTolerance(2, {_quantity.Name}.From{_baseUnit.PluralName}(10)/{_quantity.Name}.From{_baseUnit.PluralName}(5), {_baseUnit.PluralName}Tolerance);
885885
}}
886886
");
887+
888+
if (_quantity.Relations.Length > 0)
889+
{
890+
Writer.WL($@"
891+
/// <summary>Tests generated arithmetic operators for quantity relations defined in <c>Common/UnitRelations.json</c></summary>
892+
[Fact]
893+
public void ArithmeticOperators_Relational()
894+
{{");
895+
foreach (QuantityRelation relation in _quantity.Relations)
896+
{
897+
var leftQuantity = relation.LeftQuantity;
898+
var leftUnit = relation.LeftUnit;
899+
var rightQuantity = relation.RightQuantity;
900+
var rightUnit = relation.RightUnit;
901+
var expectedValue = relation.Operator switch
902+
{
903+
"+" => 12,
904+
"-" => 8,
905+
"*" => 20,
906+
"/" => 5,
907+
_ => throw new NotSupportedException($"Unsupported operator: {relation.Operator}")
908+
};
909+
var left = GetQuantityValueText(leftQuantity, leftUnit, 10);
910+
var right = GetQuantityValueText(rightQuantity, rightUnit, 2);
911+
var expected = GetQuantityValueText(relation.ResultQuantity, relation.ResultUnit, expectedValue);
912+
913+
Writer.WL($@"
914+
Assert.Equal({expected}, {left} {relation.Operator} {right});");
915+
}
916+
Writer.WL($@"
917+
}}
918+
");
919+
}
920+
921+
if (_quantity.Relations.FirstOrDefault(x => x.IsInverse) is { } inverseRelation)
922+
{
923+
var quantityName = _quantity.Name;
924+
Unit unit = inverseRelation.LeftQuantity.Name == quantityName ? inverseRelation.LeftUnit : inverseRelation.RightUnit;
925+
Quantity inverseQuantity = inverseRelation.LeftQuantity.Name == quantityName ? inverseRelation.RightQuantity : inverseRelation.LeftQuantity;
926+
Unit inverseUnit = inverseRelation.LeftQuantity.Name == quantityName ? inverseRelation.RightUnit : inverseRelation.LeftUnit;
927+
928+
Writer.WL($@"
929+
930+
[Fact]
931+
public void InverseMethod()
932+
{{
933+
{quantityName} v = {quantityName}.From{unit.PluralName}(10);
934+
935+
{inverseQuantity.Name} inverse = v.Inverse();
936+
937+
AssertEx.EqualTolerance(0.1, inverse.Value, 1e-5);
938+
Assert.Equal({inverseQuantity.Name}Unit.{inverseUnit.SingularName}, inverse.Unit);");
939+
Writer.WL($@"
940+
}}
941+
");
942+
}
887943
}
888944
else
889945
{
@@ -1119,6 +1175,20 @@ public void NegationOperator_ReturnsQuantity_WithNegatedValue(double value)
11191175
return Writer.ToString();
11201176
}
11211177

1178+
/// <summary>
1179+
/// Returns either the number value as string, such as <c>5</c>, or the quantity factory method, such as <c>Length.FromMeters(100)</c>.
1180+
/// </summary>
1181+
/// <param name="quantity"></param>
1182+
/// <param name="unit"></param>
1183+
/// <param name="value"></param>
1184+
/// <returns></returns>
1185+
private static string GetQuantityValueText(Quantity quantity, Unit unit, int value)
1186+
{
1187+
return quantity.Name == "double"
1188+
? value.ToString()
1189+
: $"{quantity.Name}.From{unit.PluralName}({value})";
1190+
}
1191+
11221192
private bool IsAmbiguousAbbreviation(Localization localization, string abbreviation)
11231193
{
11241194
return _quantity.Units.Count(u =>

CodeGen/JsonTypes/Quantity.cs

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

44
using System;
55
using System.Diagnostics;
6+
using System.Linq;
67

78
namespace CodeGen.JsonTypes
89
{
@@ -26,5 +27,7 @@ internal record Quantity
2627

2728
// 0649 Field is never assigned to
2829
#pragma warning restore 0649
30+
31+
public Unit GetBaseUnit() => Units.First(x => x.SingularName == BaseUnit);
2932
}
3033
}

CodeGen/JsonTypes/QuantityRelation.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ namespace CodeGen.JsonTypes
88
internal record QuantityRelation : IComparable<QuantityRelation>
99
{
1010
public bool NoInferredDivision = false;
11+
12+
/// <summary>Whether this is a relation between two quantities that are the inverse of each other, such as <c>double = Length.Meter * ReciprocalLength.InverseMeter -- Inverse</c>.</summary>
13+
public bool IsInverse = false;
14+
15+
/// <summary>Whether this relation is derived from another relation, such as obtaining division from a multiplication definition. Can be used to treat the original definitions differently.</summary>
16+
public bool IsDerived = false;
17+
1118
public string Operator = null!;
1219

1320
public Quantity LeftQuantity = null!;
@@ -32,4 +39,4 @@ public int CompareTo(QuantityRelation? other)
3239

3340
private static string PrependDot(string? s) => s == null ? string.Empty : "." + s;
3441
}
35-
}
42+
}

Common/UnitRelations.json

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
11
[
2-
"1 = Area.SquareMeter * ReciprocalArea.InverseSquareMeter",
3-
"1 = Density.KilogramPerCubicMeter * SpecificVolume.CubicMeterPerKilogram",
4-
"1 = ElectricResistivity.OhmMeter * ElectricConductivity.SiemensPerMeter",
5-
"1 = Length.Meter * ReciprocalLength.InverseMeter",
62
"Acceleration.MeterPerSecondSquared = Jerk.MeterPerSecondCubed * Duration.Second",
73
"AmountOfSubstance.Mole = MolarFlow.MolePerSecond * Duration.Second",
84
"AmountOfSubstance.Mole = Molarity.MolePerCubicMeter * Volume.CubicMeter",
@@ -11,6 +7,9 @@
117
"Area.SquareMeter = Length.Meter * Length.Meter",
128
"Area.SquareMeter = Volume.CubicMeter * ReciprocalLength.InverseMeter",
139
"AreaMomentOfInertia.MeterToTheFourth = Volume.CubicMeter * Length.Meter",
10+
"double = Density.KilogramPerCubicMeter * SpecificVolume.CubicMeterPerKilogram -- Inverse",
11+
"double = ElectricResistivity.OhmMeter * ElectricConductivity.SiemensPerMeter -- Inverse",
12+
"double = Length.Meter * ReciprocalLength.InverseMeter -- Inverse",
1413
"double = SpecificEnergy.JoulePerKilogram * BrakeSpecificFuelConsumption.KilogramPerJoule",
1514
"DynamicViscosity.NewtonSecondPerMeterSquared = Density.KilogramPerCubicMeter * KinematicViscosity.SquareMeterPerSecond",
1615
"ElectricCharge.AmpereHour = ElectricCurrent.Ampere * Duration.Hour",
@@ -64,7 +63,7 @@
6463
"Pressure.Pascal = PressureChangeRate.PascalPerSecond * Duration.Second",
6564
"Pressure.Pascal = SpecificWeight.NewtonPerCubicMeter * Length.Meter",
6665
"RadiationEquivalentDose.Sievert = RadiationEquivalentDoseRate.SievertPerHour * Duration.Hour",
67-
"Ratio.DecimalFraction = Area.SquareMeter * ReciprocalArea.InverseSquareMeter -- NoInferredDivision",
66+
"Ratio.DecimalFraction = Area.SquareMeter * ReciprocalArea.InverseSquareMeter -- NoInferredDivision Inverse",
6867
"Ratio.DecimalFraction = TemperatureDelta.Kelvin * CoefficientOfThermalExpansion.PerKelvin -- NoInferredDivision",
6968
"ReciprocalArea.InverseSquareMeter = ReciprocalLength.InverseMeter * ReciprocalLength.InverseMeter",
7069
"ReciprocalLength.InverseMeter = Length.Meter * ReciprocalArea.InverseSquareMeter",

UnitsNet.Tests/GeneratedCode/TestsBase/AccelerationTestsBase.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.Tests/GeneratedCode/TestsBase/AmountOfSubstanceTestsBase.g.cs

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

UnitsNet.Tests/GeneratedCode/TestsBase/AngleTestsBase.g.cs

Lines changed: 9 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)