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
+ }
0 commit comments