Skip to content

Commit 2424307

Browse files
Muximizeangularsen
andauthored
Generate operators from unit relations defined in JSON (#1329)
Related #1200 In the PR adding generic math (#1164) @AndreasLeeb states: > Regarding the operators in the *.extra.cs files, that could be tackled easily by describing the dependencies (operations) between different quantities in the quantity JSON files, and then the operator overloads and the generic math interfaces for the quantity structs could also be automatically generated. But that's a topic for another time 😄 I decided to give this a shot. `UnitRelations.json` contains relations extracted from the existing *.extra.cs files. I decided on a new file because multiplication is commutative and I didn't want to duplicate these in the individual quantity JSON files, or risk missing one or the other, so it's best to define them once in one place. The generator handles this by generating two operators for a single multiplication relation. The relations format uses the quantities method names. This is a bit unfortunate, but it's the best I could come up with without making the CodeGen project depend on UnitsNet, which would create a bit of a chicken/egg problem. This is not unheard of (self-hosted compilers) but I wanted to keep it simple for now. The generated code enables the removal of 44 *.extra.cs files, and the 17 remaining contain much less code. --------- Co-authored-by: Andreas Gullberg Larsen <[email protected]>
1 parent fb828d1 commit 2424307

File tree

122 files changed

+2873
-1880
lines changed

Some content is hidden

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

122 files changed

+2873
-1880
lines changed

Diff for: CodeGen/Generators/QuantityRelationsParser.cs

+195
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
// Licensed under MIT No Attribution, see LICENSE file at the root.
2+
// Copyright 2013 Andreas Gullberg Larsen ([email protected]). Maintained at https://github.com/angularsen/UnitsNet.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Linq;
8+
using CodeGen.Exceptions;
9+
using CodeGen.JsonTypes;
10+
using Newtonsoft.Json;
11+
12+
namespace CodeGen.Generators
13+
{
14+
/// <summary>
15+
/// Parses the JSON file that defines the relationships (operators) between quantities
16+
/// and applies them to the parsed quantity objects.
17+
/// </summary>
18+
internal static class QuantityRelationsParser
19+
{
20+
/// <summary>
21+
/// Parse and apply relations to quantities.
22+
///
23+
/// The relations are defined in UnitRelations.json
24+
/// Each defined relation can be applied multiple times to one or two quantities depending on the operator and the operands.
25+
///
26+
/// The format of a relation definition is "Quantity.Unit operator Quantity.Unit = Quantity.Unit" (See examples below).
27+
/// "double" can be used as a unitless operand.
28+
/// "1" can be used as the left operand to define inverse relations.
29+
/// </summary>
30+
/// <example>
31+
/// [
32+
/// "Power.Watt = ElectricPotential.Volt * ElectricCurrent.Ampere",
33+
/// "Speed.MeterPerSecond = Length.Meter / Duration.Second",
34+
/// "ReciprocalLength.InverseMeter = 1 / Length.Meter"
35+
/// ]
36+
/// </example>
37+
/// <param name="rootDir">Repository root directory.</param>
38+
/// <param name="quantities">List of previously parsed Quantity objects.</param>
39+
public static void ParseAndApplyRelations(string rootDir, Quantity[] quantities)
40+
{
41+
var quantityDictionary = quantities.ToDictionary(q => q.Name, q => q);
42+
43+
// Add double and 1 as pseudo-quantities to validate relations that use them.
44+
var pseudoQuantity = new Quantity { Name = null!, Units = [new Unit { SingularName = null! }] };
45+
quantityDictionary["double"] = pseudoQuantity with { Name = "double" };
46+
quantityDictionary["1"] = pseudoQuantity with { Name = "1" };
47+
48+
var relations = ParseRelations(rootDir, quantityDictionary);
49+
50+
// Because multiplication is commutative, we can infer the other operand order.
51+
relations.AddRange(relations
52+
.Where(r => r.Operator is "*" or "inverse" && r.LeftQuantity != r.RightQuantity)
53+
.Select(r => r with
54+
{
55+
LeftQuantity = r.RightQuantity,
56+
LeftUnit = r.RightUnit,
57+
RightQuantity = r.LeftQuantity,
58+
RightUnit = r.LeftUnit,
59+
})
60+
.ToList());
61+
62+
// We can infer TimeSpan relations from Duration relations.
63+
var timeSpanQuantity = pseudoQuantity with { Name = "TimeSpan" };
64+
relations.AddRange(relations
65+
.Where(r => r.LeftQuantity.Name is "Duration")
66+
.Select(r => r with { LeftQuantity = timeSpanQuantity })
67+
.ToList());
68+
relations.AddRange(relations
69+
.Where(r => r.RightQuantity.Name is "Duration")
70+
.Select(r => r with { RightQuantity = timeSpanQuantity })
71+
.ToList());
72+
73+
// Sort all relations to keep generated operators in a consistent order.
74+
relations.Sort();
75+
76+
var duplicates = relations
77+
.GroupBy(r => r.SortString)
78+
.Where(g => g.Count() > 1)
79+
.Select(g => g.Key)
80+
.ToList();
81+
82+
if (duplicates.Any())
83+
{
84+
var list = string.Join("\n ", duplicates);
85+
throw new UnitsNetCodeGenException($"Duplicate inferred relations:\n {list}");
86+
}
87+
88+
foreach (var quantity in quantities)
89+
{
90+
var quantityRelations = new List<QuantityRelation>();
91+
92+
foreach (var relation in relations)
93+
{
94+
if (relation.LeftQuantity == quantity)
95+
{
96+
// The left operand of a relation is responsible for generating the operator.
97+
quantityRelations.Add(relation);
98+
}
99+
else if (relation.RightQuantity == quantity && relation.LeftQuantity.Name is "double" or "TimeSpan")
100+
{
101+
// Because we cannot add generated operators to double or TimeSpan, we make the right operand responsible in this case.
102+
quantityRelations.Add(relation);
103+
}
104+
}
105+
106+
quantity.Relations = quantityRelations.ToArray();
107+
}
108+
}
109+
110+
private static List<QuantityRelation> ParseRelations(string rootDir, IReadOnlyDictionary<string, Quantity> quantities)
111+
{
112+
var relationsFileName = Path.Combine(rootDir, "Common/UnitRelations.json");
113+
114+
try
115+
{
116+
var text = File.ReadAllText(relationsFileName);
117+
var relationStrings = JsonConvert.DeserializeObject<SortedSet<string>>(text) ?? [];
118+
119+
var parsedRelations = relationStrings.Select(relationString => ParseRelation(relationString, quantities)).ToList();
120+
121+
// File parsed successfully, save it back to disk in the sorted state.
122+
File.WriteAllText(relationsFileName, JsonConvert.SerializeObject(relationStrings, Formatting.Indented));
123+
124+
return parsedRelations;
125+
}
126+
catch (Exception e)
127+
{
128+
throw new UnitsNetCodeGenException($"Error parsing relations file: {relationsFileName}", e);
129+
}
130+
}
131+
132+
private static QuantityRelation ParseRelation(string relationString, IReadOnlyDictionary<string, Quantity> quantities)
133+
{
134+
var segments = relationString.Split(' ');
135+
136+
if (segments is not [_, "=", _, "*" or "/", _])
137+
{
138+
throw new Exception($"Invalid relation string: {relationString}");
139+
}
140+
141+
var @operator = segments[3];
142+
var left = segments[2].Split('.');
143+
var right = segments[4].Split('.');
144+
var result = segments[0].Split('.');
145+
146+
var leftQuantity = GetQuantity(left[0]);
147+
var rightQuantity = GetQuantity(right[0]);
148+
var resultQuantity = GetQuantity(result[0]);
149+
150+
var leftUnit = GetUnit(leftQuantity, left.ElementAtOrDefault(1));
151+
var rightUnit = GetUnit(rightQuantity, right.ElementAtOrDefault(1));
152+
var resultUnit = GetUnit(resultQuantity, result.ElementAtOrDefault(1));
153+
154+
if (leftQuantity.Name == "1")
155+
{
156+
@operator = "inverse";
157+
leftQuantity = resultQuantity;
158+
leftUnit = resultUnit;
159+
}
160+
161+
return new QuantityRelation
162+
{
163+
Operator = @operator,
164+
LeftQuantity = leftQuantity,
165+
LeftUnit = leftUnit,
166+
RightQuantity = rightQuantity,
167+
RightUnit = rightUnit,
168+
ResultQuantity = resultQuantity,
169+
ResultUnit = resultUnit
170+
};
171+
172+
Quantity GetQuantity(string quantityName)
173+
{
174+
if (!quantities.TryGetValue(quantityName, out var quantity))
175+
{
176+
throw new Exception($"Undefined quantity {quantityName} in relation string: {relationString}");
177+
}
178+
179+
return quantity;
180+
}
181+
182+
Unit GetUnit(Quantity quantity, string? unitName)
183+
{
184+
try
185+
{
186+
return quantity.Units.First(u => u.SingularName == unitName);
187+
}
188+
catch (InvalidOperationException)
189+
{
190+
throw new Exception($"Undefined unit {unitName} in relation string: {relationString}");
191+
}
192+
}
193+
}
194+
}
195+
}

Diff for: CodeGen/Generators/UnitsNetGen/QuantityGenerator.cs

+122-2
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,12 @@ public string Generate()
3939
using System;
4040
using System.Diagnostics.CodeAnalysis;
4141
using System.Globalization;
42-
using System.Linq;
43-
using System.Runtime.Serialization;
42+
using System.Linq;");
43+
if (_quantity.Relations.Any(r => r.Operator is "*" or "/"))
44+
Writer.WL(@"#if NET7_0_OR_GREATER
45+
using System.Numerics;
46+
#endif");
47+
Writer.WL(@"using System.Runtime.Serialization;
4448
using UnitsNet.InternalHelpers;
4549
using UnitsNet.Units;
4650
@@ -67,6 +71,35 @@ namespace UnitsNet
6771
public readonly partial struct {_quantity.Name} :
6872
{(_quantity.GenerateArithmetic ? "IArithmeticQuantity" : "IQuantity")}<{_quantity.Name}, {_unitEnumName}, {_quantity.ValueType}>,");
6973

74+
if (_quantity.Relations.Any(r => r.Operator is "*" or "/"))
75+
{
76+
Writer.WL(@$"
77+
#if NET7_0_OR_GREATER");
78+
foreach (var relation in _quantity.Relations)
79+
{
80+
if (relation.LeftQuantity == _quantity)
81+
{
82+
switch (relation.Operator)
83+
{
84+
case "*":
85+
Writer.W(@"
86+
IMultiplyOperators");
87+
break;
88+
case "/":
89+
Writer.W(@"
90+
IDivisionOperators");
91+
break;
92+
default:
93+
continue;
94+
}
95+
Writer.WL($"<{relation.LeftQuantity.Name}, {relation.RightQuantity.Name}, {relation.ResultQuantity.Name}>,");
96+
}
97+
}
98+
99+
Writer.WL(@$"
100+
#endif");
101+
}
102+
70103
if (_quantity.ValueType == "decimal") Writer.WL(@$"
71104
IDecimalQuantity,");
72105

@@ -100,6 +133,7 @@ namespace UnitsNet
100133
GenerateStaticFactoryMethods();
101134
GenerateStaticParseMethods();
102135
GenerateArithmeticOperators();
136+
GenerateRelationalOperators();
103137
GenerateEqualityAndComparison();
104138
GenerateConversionMethods();
105139
GenerateToString();
@@ -690,6 +724,92 @@ private void GenerateLogarithmicArithmeticOperators()
690724
" );
691725
}
692726

727+
/// <summary>
728+
/// Generates operators that express relations between quantities as applied by <see cref="QuantityRelationsParser" />.
729+
/// </summary>
730+
private void GenerateRelationalOperators()
731+
{
732+
if (!_quantity.Relations.Any()) return;
733+
734+
Writer.WL($@"
735+
#region Relational Operators
736+
");
737+
738+
foreach (QuantityRelation relation in _quantity.Relations)
739+
{
740+
if (relation.Operator == "inverse")
741+
{
742+
Writer.WL($@"
743+
/// <summary>Calculates the inverse of this quantity.</summary>
744+
/// <returns>The corresponding inverse quantity, <see cref=""{relation.RightQuantity.Name}""/>.</returns>
745+
public {relation.RightQuantity.Name} Inverse()
746+
{{
747+
return {relation.LeftUnit.PluralName} == 0.0 ? {relation.RightQuantity.Name}.Zero : {relation.RightQuantity.Name}.From{relation.RightUnit.PluralName}(1 / {relation.LeftUnit.PluralName});
748+
}}
749+
");
750+
}
751+
else
752+
{
753+
var leftParameter = relation.LeftQuantity.Name.ToCamelCase();
754+
var leftConversionProperty = relation.LeftUnit.PluralName;
755+
var rightParameter = relation.RightQuantity.Name.ToCamelCase();
756+
var rightConversionProperty = relation.RightUnit.PluralName;
757+
758+
if (relation.LeftQuantity.Name is nameof(TimeSpan))
759+
{
760+
leftConversionProperty = "Total" + leftConversionProperty;
761+
}
762+
763+
if (relation.RightQuantity.Name is nameof(TimeSpan))
764+
{
765+
rightConversionProperty = "Total" + rightConversionProperty;
766+
}
767+
768+
if (leftParameter == rightParameter)
769+
{
770+
leftParameter = "left";
771+
rightParameter = "right";
772+
}
773+
774+
var leftPart = $"{leftParameter}.{leftConversionProperty}";
775+
var rightPart = $"{rightParameter}.{rightConversionProperty}";
776+
777+
if (leftParameter is "double")
778+
{
779+
leftParameter = leftPart = "value";
780+
}
781+
782+
if (rightParameter is "double")
783+
{
784+
rightParameter = rightPart = "value";
785+
}
786+
787+
var leftCast = relation.LeftQuantity.ValueType is "decimal" ? "(double)" : string.Empty;
788+
var rightCast = relation.RightQuantity.ValueType is "decimal" ? "(double)" : string.Empty;
789+
790+
var expression = $"{leftCast}{leftPart} {relation.Operator} {rightCast}{rightPart}";
791+
792+
if (relation.ResultQuantity.Name is not ("double" or "decimal"))
793+
{
794+
expression = $"{relation.ResultQuantity.Name}.From{relation.ResultUnit.PluralName}({expression})";
795+
}
796+
797+
Writer.WL($@"
798+
/// <summary>Get <see cref=""{relation.ResultQuantity.Name}""/> from <see cref=""{relation.LeftQuantity.Name}""/> {relation.Operator} <see cref=""{relation.RightQuantity.Name}""/>.</summary>
799+
public static {relation.ResultQuantity.Name} operator {relation.Operator}({relation.LeftQuantity.Name} {leftParameter}, {relation.RightQuantity.Name} {rightParameter})
800+
{{
801+
return {expression};
802+
}}
803+
");
804+
}
805+
}
806+
807+
Writer.WL($@"
808+
809+
#endregion
810+
");
811+
}
812+
693813
private void GenerateEqualityAndComparison()
694814
{
695815
Writer.WL($@"

Diff for: CodeGen/JsonTypes/Quantity.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
namespace CodeGen.JsonTypes
77
{
8-
internal class Quantity
8+
internal record Quantity
99
{
1010
// 0649 Field is never assigned to
1111
#pragma warning disable 0649
@@ -18,6 +18,7 @@ internal class Quantity
1818
public int LogarithmicScalingFactor = 1;
1919
public string Name = null!;
2020
public Unit[] Units = Array.Empty<Unit>();
21+
public QuantityRelation[] Relations = Array.Empty<QuantityRelation>();
2122
public string? XmlDocRemarks;
2223
public string XmlDocSummary = null!;
2324
public string? ObsoleteText;

0 commit comments

Comments
 (0)