-
Notifications
You must be signed in to change notification settings - Fork 408
Generate operators from unit relations defined in JSON #1329
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
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
d93e058
Define unit relations
Muximize ce1a6ff
Add relations to CodeGen
Muximize cfefd26
Generate code, clean up extras
Muximize c7918a3
Separate relation inference and application, add more comments
Muximize 4de9789
Sort inferred relations to keep generated operators in a consistent o…
Muximize 7046318
Generated code with re-sorted operators
Muximize 59e8f7f
Add examples
Muximize 049eaf9
Automatically remove duplicate relation definitions
Muximize 7a52a91
Throw exception on duplicate inferred relations
Muximize 5c9abed
Generate code: changed SortString format changes the order of some re…
Muximize 2b9d456
Make casting decimal types neater
Muximize f4231c1
Clean up GenerateRelationalOperators a bit
Muximize 5802fb2
Merge remote-tracking branch 'origin/release/v6' into relations
angularsen File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 ([email protected]). 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(); | ||
Muximize marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // 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}"); | ||
Muximize marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| var @operator = segments[3]; | ||
| var left = segments[2].Split('.'); | ||
| var right = segments[4].Split('.'); | ||
| var result = segments[0].Split('.'); | ||
Muximize marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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 hidden or 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 hidden or 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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.