-
Notifications
You must be signed in to change notification settings - Fork 383
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 <andreas.larsen84@gmail.com>
- Loading branch information
1 parent
fb828d1
commit 2424307
Showing
122 changed files
with
2,873 additions
and
1,880 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
// 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.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). | ||
/// "double" can be used as a unitless operand. | ||
/// "1" can be used as the left operand to define inverse relations. | ||
/// </summary> | ||
/// <example> | ||
/// [ | ||
/// "Power.Watt = ElectricPotential.Volt * ElectricCurrent.Ampere", | ||
/// "Speed.MeterPerSecond = Length.Meter / Duration.Second", | ||
/// "ReciprocalLength.InverseMeter = 1 / Length.Meter" | ||
/// ] | ||
/// </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 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) | ||
.Select(r => r with | ||
{ | ||
LeftQuantity = r.RightQuantity, | ||
LeftUnit = r.RightUnit, | ||
RightQuantity = r.LeftQuantity, | ||
RightUnit = r.LeftUnit, | ||
}) | ||
.ToList()); | ||
|
||
// We can infer TimeSpan relations from Duration relations. | ||
var timeSpanQuantity = pseudoQuantity with { Name = "TimeSpan" }; | ||
relations.AddRange(relations | ||
.Where(r => r.LeftQuantity.Name is "Duration") | ||
.Select(r => r with { LeftQuantity = timeSpanQuantity }) | ||
.ToList()); | ||
relations.AddRange(relations | ||
.Where(r => r.RightQuantity.Name is "Duration") | ||
.Select(r => r with { RightQuantity = timeSpanQuantity }) | ||
.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}"); | ||
} | ||
|
||
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 "double" or "TimeSpan") | ||
{ | ||
// Because we cannot add generated operators to double or TimeSpan, 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); | ||
var relationStrings = JsonConvert.DeserializeObject<SortedSet<string>>(text) ?? []; | ||
|
||
var parsedRelations = relationStrings.Select(relationString => ParseRelation(relationString, quantities)).ToList(); | ||
|
||
// File parsed successfully, save it back to disk in the sorted state. | ||
File.WriteAllText(relationsFileName, JsonConvert.SerializeObject(relationStrings, 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 [_, "=", _, "*" or "/", _]) | ||
{ | ||
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 (leftQuantity.Name == "1") | ||
{ | ||
@operator = "inverse"; | ||
leftQuantity = resultQuantity; | ||
leftUnit = resultUnit; | ||
} | ||
|
||
return new QuantityRelation | ||
{ | ||
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}"); | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.