Skip to content

Commit 8fa4d15

Browse files
authored
codegen: Make abbreviations for prefixes explicit (#661)
To avoid relying on exact same count and ordering of "Prefixes" in unit and "AbbreviationsWithPrefixes", which is bugprone when adding new prefixes. - Rename AbbreviationsWithPrefixes to AbbreviationsForPrefixes - Make AbbreviationsForPrefixes an object with prefixes as keys and string or string array as value - Update JSON files
1 parent 8ff6e7b commit 8fa4d15

19 files changed

+218
-285
lines changed

CodeGen/Generators/UnitsNetGenerator.cs

Lines changed: 48 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
using CodeGen.Helpers;
1111
using CodeGen.JsonTypes;
1212
using Newtonsoft.Json;
13-
using Newtonsoft.Json.Linq;
1413
using Serilog;
1514

1615
namespace CodeGen.Generators
@@ -57,11 +56,18 @@ public static void Generate(DirectoryInfo repositoryRoot)
5756

5857
private static Quantity ParseQuantityFile(string jsonFile)
5958
{
60-
var quantity = JsonConvert.DeserializeObject<Quantity>(File.ReadAllText(jsonFile, Encoding.UTF8), JsonSerializerSettings);
61-
AddPrefixUnits(quantity);
62-
FixConversionFunctionsForDecimalValueTypes(quantity);
63-
OrderUnitsByName(quantity);
64-
return quantity;
59+
try
60+
{
61+
var quantity = JsonConvert.DeserializeObject<Quantity>(File.ReadAllText(jsonFile, Encoding.UTF8), JsonSerializerSettings);
62+
AddPrefixUnits(quantity);
63+
FixConversionFunctionsForDecimalValueTypes(quantity);
64+
OrderUnitsByName(quantity);
65+
return quantity;
66+
}
67+
catch (Exception e)
68+
{
69+
throw new Exception($"Error parsing quantity JSON file: {jsonFile}", e);
70+
}
6571
}
6672

6773
private static void GenerateUnitTestClassIfNotExists(StringBuilder sb, Quantity quantity, string filePath)
@@ -144,124 +150,61 @@ private static void AddPrefixUnits(Quantity quantity)
144150
foreach (Unit unit in quantity.Units)
145151
{
146152
// "Kilo", "Nano" etc.
147-
for (var prefixIndex = 0; prefixIndex < unit.Prefixes.Length; prefixIndex++)
153+
foreach (Prefix prefix in unit.Prefixes)
148154
{
149-
Prefix prefix = unit.Prefixes[prefixIndex];
150-
PrefixInfo prefixInfo = PrefixInfo.Entries[prefix];
151-
152-
unitsToAdd.Add(new Unit
155+
try
153156
{
154-
SingularName = $"{prefix}{unit.SingularName.ToCamelCase()}", // "Kilo" + "NewtonPerMeter" => "KilonewtonPerMeter"
155-
PluralName = $"{prefix}{unit.PluralName.ToCamelCase()}", // "Kilo" + "NewtonsPerMeter" => "KilonewtonsPerMeter"
156-
BaseUnits = null, // Can we determine this somehow?
157-
FromBaseToUnitFunc = $"({unit.FromBaseToUnitFunc}) / {prefixInfo.Factor}",
158-
FromUnitToBaseFunc = $"({unit.FromUnitToBaseFunc}) * {prefixInfo.Factor}",
159-
Localization = GetLocalizationForPrefixUnit(unit, prefixIndex, prefixInfo, quantity.Name),
160-
});
157+
PrefixInfo prefixInfo = PrefixInfo.Entries[prefix];
158+
159+
unitsToAdd.Add(new Unit
160+
{
161+
SingularName = $"{prefix}{unit.SingularName.ToCamelCase()}", // "Kilo" + "NewtonPerMeter" => "KilonewtonPerMeter"
162+
PluralName = $"{prefix}{unit.PluralName.ToCamelCase()}", // "Kilo" + "NewtonsPerMeter" => "KilonewtonsPerMeter"
163+
BaseUnits = null, // Can we determine this somehow?
164+
FromBaseToUnitFunc = $"({unit.FromBaseToUnitFunc}) / {prefixInfo.Factor}",
165+
FromUnitToBaseFunc = $"({unit.FromUnitToBaseFunc}) * {prefixInfo.Factor}",
166+
Localization = GetLocalizationForPrefixUnit(unit.Localization, prefixInfo),
167+
});
168+
}
169+
catch (Exception e)
170+
{
171+
throw new Exception($"Error parsing prefix {prefix} for unit {quantity.Name}.{unit.SingularName}.", e);
172+
}
161173
}
162174
}
163175

164176
quantity.Units = quantity.Units.Concat(unitsToAdd).ToArray();
165177
}
166178

167-
private static Localization[] GetLocalizationForPrefixUnit(Unit unit, int prefixIndex, PrefixInfo prefixInfo, string quantityName)
179+
/// <summary>
180+
/// Create unit abbreviations for a prefix unit, given a unit and the prefix.
181+
/// The unit abbreviations are either prefixed with the SI prefix or an explicitly configured abbreviation via <see cref="AbbreviationsForPrefixes"/>.
182+
/// </summary>
183+
private static Localization[] GetLocalizationForPrefixUnit(IEnumerable<Localization> localizations, PrefixInfo prefixInfo)
168184
{
169-
string[] GetUnitAbbreviationsForPrefix(Localization loc)
185+
return localizations.Select(loc =>
170186
{
171-
// If no custom abbreviations are specified, prepend the default prefix to each unit abbreviation: kilo ("k") + meter ("m") => kilometer ("km")
172-
if (loc.AbbreviationsWithPrefixes == null || !loc.AbbreviationsWithPrefixes.Any())
187+
if (loc.TryGetAbbreviationsForPrefix(prefixInfo.Prefix, out string[] unitAbbreviationsForPrefix))
173188
{
174-
string prefix = prefixInfo.Abbreviation;
175-
return loc.Abbreviations.Select(unitAbbreviation => $"{prefix}{unitAbbreviation}").ToArray();
176-
}
177-
178-
/*
179-
Prepend prefix to all abbreviations of a unit.
180-
Some languages, like Russian, you can't simply prepend "k" for kilo prefix, so the prefix abbreviations must be explicitly defined
181-
with AbbreviationsWithPrefixes.
182-
183-
Example 1 - Torque.Newtonmeter has only a single abbreviation in Russian, so AbbreviationsWithPrefixes is an array of strings mapped to each prefix
184-
185-
{
186-
"SingularName": "NewtonMeter",
187-
"PluralName": "NewtonMeters",
188-
"FromUnitToBaseFunc": "x",
189-
"FromBaseToUnitFunc": "x",
190-
"Prefixes": [ "Kilo", "Mega" ],
191-
"Localization": [
192-
{
193-
"Culture": "en-US",
194-
"Abbreviations": [ "N·m" ]
195-
},
196-
{
197-
"Culture": "ru-RU",
198-
"Abbreviations": [ "Н·м" ],
199-
"AbbreviationsWithPrefixes": [ "кН·м", "МН·м" ]
200-
}
201-
]
202-
},
203-
204-
Example 2 - Duration.Second has 3 prefixes and 2 abbreviations in Russian, so AbbreviationsWithPrefixes is an array of 3 items where each
205-
represents the unit abbreviations for that prefix - typically a variant of those in "Abbreviations", but the counts don't have to match.
206-
207-
{
208-
"SingularName": "Second",
209-
"PluralName": "Seconds",
210-
"BaseUnits": {
211-
"T": "Second"
212-
},
213-
"FromUnitToBaseFunc": "x",
214-
"FromBaseToUnitFunc": "x",
215-
"Prefixes": [ "Nano", "Micro", "Milli" ],
216-
"Localization": [
217-
{
218-
"Culture": "en-US",
219-
"Abbreviations": [ "s", "sec", "secs", "second", "seconds" ]
220-
},
221-
{
222-
"Culture": "ru-RU",
223-
"Abbreviations": [ "с", "сек" ],
224-
"AbbreviationsWithPrefixes": [ ["нс", "нсек"], ["мкс", "мксек"], ["мс", "мсек"] ]
225-
}
226-
]
227-
}
228-
*/
229-
230-
EnsureValidAbbreviationsWithPrefixes(loc, unit, quantityName);
231-
JToken abbreviationsForPrefix = loc.AbbreviationsWithPrefixes[prefixIndex];
232-
switch (abbreviationsForPrefix.Type)
233-
{
234-
case JTokenType.Array:
235-
return abbreviationsForPrefix.ToObject<string[]>();
236-
case JTokenType.String:
237-
return new[] {abbreviationsForPrefix.ToObject<string>()};
238-
default:
239-
throw new NotSupportedException("Expect AbbreviationsWithPrefixes to be an array of strings or string arrays.");
189+
// Use explicitly defined prefix unit abbreviations
190+
return new Localization
191+
{
192+
Culture = loc.Culture,
193+
Abbreviations = unitAbbreviationsForPrefix,
194+
};
240195
}
241-
}
242196

243-
Localization WithPrefixes(Localization loc)
244-
{
245-
string[] unitAbbreviationsForPrefix = GetUnitAbbreviationsForPrefix(loc);
197+
// No prefix unit abbreviations are specified, so fall back to prepending the default SI prefix to each unit abbreviation:
198+
// kilo ("k") + meter ("m") => kilometer ("km")
199+
string prefix = prefixInfo.Abbreviation;
200+
unitAbbreviationsForPrefix = loc.Abbreviations.Select(unitAbbreviation => $"{prefix}{unitAbbreviation}").ToArray();
246201

247202
return new Localization
248203
{
249204
Culture = loc.Culture,
250205
Abbreviations = unitAbbreviationsForPrefix,
251206
};
252-
}
253-
254-
return unit.Localization.Select(WithPrefixes).ToArray();
255-
}
256-
257-
private static void EnsureValidAbbreviationsWithPrefixes(Localization localization, Unit unit, string quantityName)
258-
{
259-
if (localization.AbbreviationsWithPrefixes.Length > 0 &&
260-
localization.AbbreviationsWithPrefixes.Length != unit.Prefixes.Length)
261-
{
262-
throw new InvalidDataException(
263-
$"The Prefixes array length {unit.Prefixes.Length} does not match Localization.AbbreviationsWithPrefixes array length {localization.AbbreviationsWithPrefixes.Length} for {quantityName}.{unit.SingularName}");
264-
}
207+
}).ToArray();
265208
}
266209
}
267210
}

CodeGen/Generators/UnitsNetWrcGenerator.cs

Lines changed: 48 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
using CodeGen.Helpers;
1010
using CodeGen.JsonTypes;
1111
using Newtonsoft.Json;
12-
using Newtonsoft.Json.Linq;
1312
using Serilog;
1413
using QuantityGenerator = CodeGen.Generators.UnitsNetWrcGen.QuantityGenerator;
1514
using QuantityTypeGenerator = CodeGen.Generators.UnitsNetWrcGen.QuantityTypeGenerator;
@@ -60,11 +59,18 @@ public static void Generate(DirectoryInfo repositoryRoot)
6059

6160
private static Quantity ParseQuantityFile(string jsonFile)
6261
{
63-
var quantity = JsonConvert.DeserializeObject<Quantity>(File.ReadAllText(jsonFile, Encoding.UTF8), JsonSerializerSettings);
64-
AddPrefixUnits(quantity);
65-
FixConversionFunctionsForDecimalValueTypes(quantity);
66-
OrderUnitsByName(quantity);
67-
return quantity;
62+
try
63+
{
64+
var quantity = JsonConvert.DeserializeObject<Quantity>(File.ReadAllText(jsonFile, Encoding.UTF8), JsonSerializerSettings);
65+
AddPrefixUnits(quantity);
66+
FixConversionFunctionsForDecimalValueTypes(quantity);
67+
OrderUnitsByName(quantity);
68+
return quantity;
69+
}
70+
catch (Exception e)
71+
{
72+
throw new Exception($"Error parsing quantity JSON file: {jsonFile}", e);
73+
}
6874
}
6975

7076
private static void GenerateQuantity(StringBuilder sb, Quantity quantity, string filePath)
@@ -127,124 +133,61 @@ private static void AddPrefixUnits(Quantity quantity)
127133
foreach (Unit unit in quantity.Units)
128134
{
129135
// "Kilo", "Nano" etc.
130-
for (var prefixIndex = 0; prefixIndex < unit.Prefixes.Length; prefixIndex++)
136+
foreach (Prefix prefix in unit.Prefixes)
131137
{
132-
Prefix prefix = unit.Prefixes[prefixIndex];
133-
PrefixInfo prefixInfo = PrefixInfo.Entries[prefix];
134-
135-
unitsToAdd.Add(new Unit
138+
try
136139
{
137-
SingularName = $"{prefix}{unit.SingularName.ToCamelCase()}", // "Kilo" + "NewtonPerMeter" => "KilonewtonPerMeter"
138-
PluralName = $"{prefix}{unit.PluralName.ToCamelCase()}", // "Kilo" + "NewtonsPerMeter" => "KilonewtonsPerMeter"
139-
BaseUnits = null, // Can we determine this somehow?
140-
FromBaseToUnitFunc = $"({unit.FromBaseToUnitFunc}) / {prefixInfo.Factor}",
141-
FromUnitToBaseFunc = $"({unit.FromUnitToBaseFunc}) * {prefixInfo.Factor}",
142-
Localization = GetLocalizationForPrefixUnit(unit, prefixIndex, prefixInfo, quantity.Name),
143-
});
140+
PrefixInfo prefixInfo = PrefixInfo.Entries[prefix];
141+
142+
unitsToAdd.Add(new Unit
143+
{
144+
SingularName = $"{prefix}{unit.SingularName.ToCamelCase()}", // "Kilo" + "NewtonPerMeter" => "KilonewtonPerMeter"
145+
PluralName = $"{prefix}{unit.PluralName.ToCamelCase()}", // "Kilo" + "NewtonsPerMeter" => "KilonewtonsPerMeter"
146+
BaseUnits = null, // Can we determine this somehow?
147+
FromBaseToUnitFunc = $"({unit.FromBaseToUnitFunc}) / {prefixInfo.Factor}",
148+
FromUnitToBaseFunc = $"({unit.FromUnitToBaseFunc}) * {prefixInfo.Factor}",
149+
Localization = GetLocalizationForPrefixUnit(unit.Localization, prefixInfo),
150+
});
151+
}
152+
catch (Exception e)
153+
{
154+
throw new Exception($"Error parsing prefix {prefix} for unit {quantity.Name}.{unit.SingularName}.", e);
155+
}
144156
}
145157
}
146158

147159
quantity.Units = quantity.Units.Concat(unitsToAdd).ToArray();
148160
}
149161

150-
private static Localization[] GetLocalizationForPrefixUnit(Unit unit, int prefixIndex, PrefixInfo prefixInfo, string quantityName)
162+
/// <summary>
163+
/// Create unit abbreviations for a prefix unit, given a unit and the prefix.
164+
/// The unit abbreviations are either prefixed with the SI prefix or an explicitly configured abbreviation via <see cref="AbbreviationsForPrefixes"/>.
165+
/// </summary>
166+
private static Localization[] GetLocalizationForPrefixUnit(IEnumerable<Localization> localizations, PrefixInfo prefixInfo)
151167
{
152-
string[] GetUnitAbbreviationsForPrefix(Localization loc)
168+
return localizations.Select(loc =>
153169
{
154-
// If no custom abbreviations are specified, prepend the default prefix to each unit abbreviation: kilo ("k") + meter ("m") => kilometer ("km")
155-
if (loc.AbbreviationsWithPrefixes == null || !loc.AbbreviationsWithPrefixes.Any())
170+
if (loc.TryGetAbbreviationsForPrefix(prefixInfo.Prefix, out string[] unitAbbreviationsForPrefix))
156171
{
157-
string prefix = prefixInfo.Abbreviation;
158-
return loc.Abbreviations.Select(unitAbbreviation => $"{prefix}{unitAbbreviation}").ToArray();
159-
}
160-
161-
/*
162-
Prepend prefix to all abbreviations of a unit.
163-
Some languages, like Russian, you can't simply prepend "k" for kilo prefix, so the prefix abbreviations must be explicitly defined
164-
with AbbreviationsWithPrefixes.
165-
166-
Example 1 - Torque.Newtonmeter has only a single abbreviation in Russian, so AbbreviationsWithPrefixes is an array of strings mapped to each prefix
167-
168-
{
169-
"SingularName": "NewtonMeter",
170-
"PluralName": "NewtonMeters",
171-
"FromUnitToBaseFunc": "x",
172-
"FromBaseToUnitFunc": "x",
173-
"Prefixes": [ "Kilo", "Mega" ],
174-
"Localization": [
175-
{
176-
"Culture": "en-US",
177-
"Abbreviations": [ "N·m" ]
178-
},
179-
{
180-
"Culture": "ru-RU",
181-
"Abbreviations": [ "Н·м" ],
182-
"AbbreviationsWithPrefixes": [ "кН·м", "МН·м" ]
183-
}
184-
]
185-
},
186-
187-
Example 2 - Duration.Second has 3 prefixes and 2 abbreviations in Russian, so AbbreviationsWithPrefixes is an array of 3 items where each
188-
represents the unit abbreviations for that prefix - typically a variant of those in "Abbreviations", but the counts don't have to match.
189-
190-
{
191-
"SingularName": "Second",
192-
"PluralName": "Seconds",
193-
"BaseUnits": {
194-
"T": "Second"
195-
},
196-
"FromUnitToBaseFunc": "x",
197-
"FromBaseToUnitFunc": "x",
198-
"Prefixes": [ "Nano", "Micro", "Milli" ],
199-
"Localization": [
200-
{
201-
"Culture": "en-US",
202-
"Abbreviations": [ "s", "sec", "secs", "second", "seconds" ]
203-
},
204-
{
205-
"Culture": "ru-RU",
206-
"Abbreviations": [ "с", "сек" ],
207-
"AbbreviationsWithPrefixes": [ ["нс", "нсек"], ["мкс", "мксек"], ["мс", "мсек"] ]
208-
}
209-
]
210-
}
211-
*/
212-
213-
EnsureValidAbbreviationsWithPrefixes(loc, unit, quantityName);
214-
JToken abbreviationsForPrefix = loc.AbbreviationsWithPrefixes[prefixIndex];
215-
switch (abbreviationsForPrefix.Type)
216-
{
217-
case JTokenType.Array:
218-
return abbreviationsForPrefix.ToObject<string[]>();
219-
case JTokenType.String:
220-
return new[] {abbreviationsForPrefix.ToObject<string>()};
221-
default:
222-
throw new NotSupportedException("Expect AbbreviationsWithPrefixes to be an array of strings or string arrays.");
172+
// Use explicitly defined prefix unit abbreviations
173+
return new Localization
174+
{
175+
Culture = loc.Culture,
176+
Abbreviations = unitAbbreviationsForPrefix,
177+
};
223178
}
224-
}
225179

226-
Localization WithPrefixes(Localization loc)
227-
{
228-
string[] unitAbbreviationsForPrefix = GetUnitAbbreviationsForPrefix(loc);
180+
// No prefix unit abbreviations are specified, so fall back to prepending the default SI prefix to each unit abbreviation:
181+
// kilo ("k") + meter ("m") => kilometer ("km")
182+
string prefix = prefixInfo.Abbreviation;
183+
unitAbbreviationsForPrefix = loc.Abbreviations.Select(unitAbbreviation => $"{prefix}{unitAbbreviation}").ToArray();
229184

230185
return new Localization
231186
{
232187
Culture = loc.Culture,
233188
Abbreviations = unitAbbreviationsForPrefix,
234189
};
235-
}
236-
237-
return unit.Localization.Select(WithPrefixes).ToArray();
238-
}
239-
240-
private static void EnsureValidAbbreviationsWithPrefixes(Localization localization, Unit unit, string quantityName)
241-
{
242-
if (localization.AbbreviationsWithPrefixes.Length > 0 &&
243-
localization.AbbreviationsWithPrefixes.Length != unit.Prefixes.Length)
244-
{
245-
throw new InvalidDataException(
246-
$"The Prefixes array length {unit.Prefixes.Length} does not match Localization.AbbreviationsWithPrefixes array length {localization.AbbreviationsWithPrefixes.Length} for {quantityName}.{unit.SingularName}");
247-
}
190+
}).ToArray();
248191
}
249192
}
250193
}

0 commit comments

Comments
 (0)