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