Skip to content

✨Add arithmetic operators for inversely related quantities #1586

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
25 changes: 14 additions & 11 deletions CodeGen/Generators/QuantityRelationsParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ internal static class QuantityRelationsParser
/// </summary>
/// <example>
/// [
/// "1 = Length.Meter * ReciprocalLength.InverseMeter"
/// "double = Length.Meter * ReciprocalLength.InverseMeter -- Inverse"
/// "Power.Watt = ElectricPotential.Volt * ElectricCurrent.Ampere",
/// "Mass.Kilogram = MassConcentration.KilogramPerCubicMeter * Volume.CubicMeter -- NoInferredDivision",
/// ]
Expand All @@ -46,22 +46,23 @@ public static void ParseAndApplyRelations(string rootDir, Quantity[] quantities)
// Add double and 1 as pseudo-quantities to validate relations that use them.
var pseudoQuantity = new Quantity { Name = null!, Units = [new Unit { SingularName = null! }] };
quantityDictionary["double"] = pseudoQuantity with { Name = "double" };
quantityDictionary["1"] = pseudoQuantity with { Name = "1" };

var relations = ParseRelations(rootDir, quantityDictionary);

// Because multiplication is commutative, we can infer the other operand order.
relations.AddRange(relations
.Where(r => r.Operator is "*" or "inverse" && r.LeftQuantity != r.RightQuantity)
.Where(r => r.Operator is "*" && r.LeftQuantity != r.RightQuantity)
.Select(r => r with
{
LeftQuantity = r.RightQuantity,
LeftUnit = r.RightUnit,
RightQuantity = r.LeftQuantity,
RightUnit = r.LeftUnit,
IsDerived = true,
// IsInverse is propagated, to also generate Inverse() method for the right hand quantity.
})
.ToList());

// We can infer division relations from multiplication relations.
relations.AddRange(relations
.Where(r => r is { Operator: "*", NoInferredDivision: false })
Expand All @@ -72,6 +73,8 @@ public static void ParseAndApplyRelations(string rootDir, Quantity[] quantities)
LeftUnit = r.ResultUnit,
ResultQuantity = r.LeftQuantity,
ResultUnit = r.LeftUnit,
IsDerived = true,
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.
})
// Skip division between equal quantities because the ratio is already generated as part of the Arithmetic Operators.
.Where(r => r.LeftQuantity != r.RightQuantity)
Expand All @@ -91,7 +94,7 @@ public static void ParseAndApplyRelations(string rootDir, Quantity[] quantities)
var list = string.Join("\n ", duplicates);
throw new UnitsNetCodeGenException($"Duplicate inferred relations:\n {list}");
}

var ambiguous = relations
.GroupBy(r => $"{r.LeftQuantity.Name} {r.Operator} {r.RightQuantity.Name}")
.Where(g => g.Count() > 1)
Expand Down Expand Up @@ -170,14 +173,14 @@ private static QuantityRelation ParseRelation(string relationString, IReadOnlyDi
var rightUnit = GetUnit(rightQuantity, right.ElementAtOrDefault(1));
var resultUnit = GetUnit(resultQuantity, result.ElementAtOrDefault(1));

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

return new QuantityRelation
{
NoInferredDivision = segments.Contains("NoInferredDivision"),
NoInferredDivision = configurationSegments.Contains("NoInferredDivision"),
IsInverse = configurationSegments.Contains("Inverse"),
Operator = @operator,
LeftQuantity = leftQuantity,
LeftUnit = leftUnit,
Expand Down Expand Up @@ -210,4 +213,4 @@ Unit GetUnit(Quantity quantity, string? unitName)
}
}
}
}
}
78 changes: 41 additions & 37 deletions CodeGen/Generators/UnitsNetGen/QuantityGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -788,61 +788,65 @@ private void GenerateRelationalOperators()
Writer.WL($@"
#region Relational Operators
");

foreach (QuantityRelation relation in _quantity.Relations)
// Generate an Inverse() method for the original relation definition, if the quantity can be inverted to another quantity.
// E.g. double = Length.Meter * ReciprocalLength.InverseMeter
if (_quantity.Relations.FirstOrDefault(x => x is { IsInverse: true, }) is { } inverseRelation)
{
if (relation.Operator == "inverse")
{
Writer.WL($@"
Unit unit = inverseRelation.LeftUnit;
Quantity inverseQuantity = inverseRelation.RightQuantity;
Unit inverseUnit = inverseRelation.RightUnit;

Writer.WL($@"
/// <summary>Calculates the inverse of this quantity.</summary>
/// <returns>The corresponding inverse quantity, <see cref=""{relation.RightQuantity.Name}""/>.</returns>
public {relation.RightQuantity.Name} Inverse()
/// <returns>The corresponding inverse quantity, <see cref=""{inverseQuantity.Name}""/>.</returns>
public {inverseQuantity.Name} Inverse()
{{
return {relation.RightQuantity.Name}.From{relation.RightUnit.PluralName}(1 / {relation.LeftUnit.PluralName});
return {inverseQuantity.Name}.From{inverseUnit.PluralName}(1 / {unit.PluralName});
}}
");
}
else
{
var leftParameter = relation.LeftQuantity.Name.ToCamelCase();
var leftConversionProperty = relation.LeftUnit.PluralName;
var rightParameter = relation.RightQuantity.Name.ToCamelCase();
var rightConversionProperty = relation.RightUnit.PluralName;
}

if (leftParameter == rightParameter)
{
leftParameter = "left";
rightParameter = "right";
}
// Generate arithmetic operator overloads for each relation, including inverse relations.
foreach (QuantityRelation relation in _quantity.Relations)
{
var leftParameter = relation.LeftQuantity.Name.ToCamelCase();
var leftConversionProperty = relation.LeftUnit.PluralName;
var rightParameter = relation.RightQuantity.Name.ToCamelCase();
var rightConversionProperty = relation.RightUnit.PluralName;

if (leftParameter == rightParameter)
{
leftParameter = "left";
rightParameter = "right";
}

var leftPart = $"{leftParameter}.{leftConversionProperty}";
var rightPart = $"{rightParameter}.{rightConversionProperty}";
var leftPart = $"{leftParameter}.{leftConversionProperty}";
var rightPart = $"{rightParameter}.{rightConversionProperty}";

if (leftParameter is "double")
{
leftParameter = leftPart = "value";
}
if (leftParameter is "double")
{
leftParameter = leftPart = "value";
}

if (rightParameter is "double")
{
rightParameter = rightPart = "value";
}
if (rightParameter is "double")
{
rightParameter = rightPart = "value";
}

var expression = $"{leftPart} {relation.Operator} {rightPart}";
var expression = $"{leftPart} {relation.Operator} {rightPart}";

if (relation.ResultQuantity.Name is not "double")
{
expression = $"{relation.ResultQuantity.Name}.From{relation.ResultUnit.PluralName}({expression})";
}
if (relation.ResultQuantity.Name is not "double")
{
expression = $"{relation.ResultQuantity.Name}.From{relation.ResultUnit.PluralName}({expression})";
}

Writer.WL($@"
Writer.WL($@"
/// <summary>Get <see cref=""{relation.ResultQuantity.Name}""/> from <see cref=""{relation.LeftQuantity.Name}""/> {relation.Operator} <see cref=""{relation.RightQuantity.Name}""/>.</summary>
public static {relation.ResultQuantity.Name} operator {relation.Operator}({relation.LeftQuantity.Name} {leftParameter}, {relation.RightQuantity.Name} {rightParameter})
{{
return {expression};
}}
");
}
}

Writer.WL($@"
Expand Down
70 changes: 70 additions & 0 deletions CodeGen/Generators/UnitsNetGen/UnitTestBaseClassGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,62 @@ public void ArithmeticOperators()
AssertEx.EqualTolerance(2, {_quantity.Name}.From{_baseUnit.PluralName}(10)/{_quantity.Name}.From{_baseUnit.PluralName}(5), {_baseUnit.PluralName}Tolerance);
}}
");

if (_quantity.Relations.Length > 0)
{
Writer.WL($@"
/// <summary>Tests generated arithmetic operators for quantity relations defined in <c>Common/UnitRelations.json</c></summary>
[Fact]
public void ArithmeticOperators_Relational()
{{");
foreach (QuantityRelation relation in _quantity.Relations)
{
var leftQuantity = relation.LeftQuantity;
var leftUnit = relation.LeftUnit;
var rightQuantity = relation.RightQuantity;
var rightUnit = relation.RightUnit;
var expectedValue = relation.Operator switch
{
"+" => 12,
"-" => 8,
"*" => 20,
"/" => 5,
_ => throw new NotSupportedException($"Unsupported operator: {relation.Operator}")
};
var left = GetQuantityValueText(leftQuantity, leftUnit, 10);
var right = GetQuantityValueText(rightQuantity, rightUnit, 2);
var expected = GetQuantityValueText(relation.ResultQuantity, relation.ResultUnit, expectedValue);

Writer.WL($@"
Assert.Equal({expected}, {left} {relation.Operator} {right});");
}
Writer.WL($@"
}}
");
}

if (_quantity.Relations.FirstOrDefault(x => x.IsInverse) is { } inverseRelation)
{
var quantityName = _quantity.Name;
Unit unit = inverseRelation.LeftQuantity.Name == quantityName ? inverseRelation.LeftUnit : inverseRelation.RightUnit;
Quantity inverseQuantity = inverseRelation.LeftQuantity.Name == quantityName ? inverseRelation.RightQuantity : inverseRelation.LeftQuantity;
Unit inverseUnit = inverseRelation.LeftQuantity.Name == quantityName ? inverseRelation.RightUnit : inverseRelation.LeftUnit;

Writer.WL($@"

[Fact]
public void InverseMethod()
{{
{quantityName} v = {quantityName}.From{unit.PluralName}(10);

{inverseQuantity.Name} inverse = v.Inverse();

AssertEx.EqualTolerance(0.1, inverse.Value, 1e-5);
Assert.Equal({inverseQuantity.Name}Unit.{inverseUnit.SingularName}, inverse.Unit);");
Writer.WL($@"
}}
");
}
}
else
{
Expand Down Expand Up @@ -1119,6 +1175,20 @@ public void NegationOperator_ReturnsQuantity_WithNegatedValue(double value)
return Writer.ToString();
}

/// <summary>
/// Returns either the number value as string, such as <c>5</c>, or the quantity factory method, such as <c>Length.FromMeters(100)</c>.
/// </summary>
/// <param name="quantity"></param>
/// <param name="unit"></param>
/// <param name="value"></param>
/// <returns></returns>
private static string GetQuantityValueText(Quantity quantity, Unit unit, int value)
{
return quantity.Name == "double"
? value.ToString()
: $"{quantity.Name}.From{unit.PluralName}({value})";
}

private bool IsAmbiguousAbbreviation(Localization localization, string abbreviation)
{
return _quantity.Units.Count(u =>
Expand Down
3 changes: 3 additions & 0 deletions CodeGen/JsonTypes/Quantity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Diagnostics;
using System.Linq;

namespace CodeGen.JsonTypes
{
Expand All @@ -26,5 +27,7 @@ internal record Quantity

// 0649 Field is never assigned to
#pragma warning restore 0649

public Unit GetBaseUnit() => Units.First(x => x.SingularName == BaseUnit);
}
}
9 changes: 8 additions & 1 deletion CodeGen/JsonTypes/QuantityRelation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ namespace CodeGen.JsonTypes
internal record QuantityRelation : IComparable<QuantityRelation>
{
public bool NoInferredDivision = false;

/// <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>
public bool IsInverse = false;

/// <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>
public bool IsDerived = false;

public string Operator = null!;

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

private static string PrependDot(string? s) => s == null ? string.Empty : "." + s;
}
}
}
9 changes: 4 additions & 5 deletions Common/UnitRelations.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
[
"1 = Area.SquareMeter * ReciprocalArea.InverseSquareMeter",
"1 = Density.KilogramPerCubicMeter * SpecificVolume.CubicMeterPerKilogram",
"1 = ElectricResistivity.OhmMeter * ElectricConductivity.SiemensPerMeter",
"1 = Length.Meter * ReciprocalLength.InverseMeter",
"Acceleration.MeterPerSecondSquared = Jerk.MeterPerSecondCubed * Duration.Second",
"AmountOfSubstance.Mole = MolarFlow.MolePerSecond * Duration.Second",
"AmountOfSubstance.Mole = Molarity.MolePerCubicMeter * Volume.CubicMeter",
Expand All @@ -11,6 +7,9 @@
"Area.SquareMeter = Length.Meter * Length.Meter",
"Area.SquareMeter = Volume.CubicMeter * ReciprocalLength.InverseMeter",
"AreaMomentOfInertia.MeterToTheFourth = Volume.CubicMeter * Length.Meter",
"double = Density.KilogramPerCubicMeter * SpecificVolume.CubicMeterPerKilogram -- Inverse",
"double = ElectricResistivity.OhmMeter * ElectricConductivity.SiemensPerMeter -- Inverse",
"double = Length.Meter * ReciprocalLength.InverseMeter -- Inverse",
"double = SpecificEnergy.JoulePerKilogram * BrakeSpecificFuelConsumption.KilogramPerJoule",
"DynamicViscosity.NewtonSecondPerMeterSquared = Density.KilogramPerCubicMeter * KinematicViscosity.SquareMeterPerSecond",
"ElectricCharge.AmpereHour = ElectricCurrent.Ampere * Duration.Hour",
Expand Down Expand Up @@ -64,7 +63,7 @@
"Pressure.Pascal = PressureChangeRate.PascalPerSecond * Duration.Second",
"Pressure.Pascal = SpecificWeight.NewtonPerCubicMeter * Length.Meter",
"RadiationEquivalentDose.Sievert = RadiationEquivalentDoseRate.SievertPerHour * Duration.Hour",
"Ratio.DecimalFraction = Area.SquareMeter * ReciprocalArea.InverseSquareMeter -- NoInferredDivision",
"Ratio.DecimalFraction = Area.SquareMeter * ReciprocalArea.InverseSquareMeter -- NoInferredDivision Inverse",
"Ratio.DecimalFraction = TemperatureDelta.Kelvin * CoefficientOfThermalExpansion.PerKelvin -- NoInferredDivision",
"ReciprocalArea.InverseSquareMeter = ReciprocalLength.InverseMeter * ReciprocalLength.InverseMeter",
"ReciprocalLength.InverseMeter = Length.Meter * ReciprocalArea.InverseSquareMeter",
Expand Down
11 changes: 11 additions & 0 deletions UnitsNet.Tests/GeneratedCode/TestsBase/AccelerationTestsBase.g.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions UnitsNet.Tests/GeneratedCode/TestsBase/AngleTestsBase.g.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading