Skip to content

Commit 1f90e8f

Browse files
lipchevangularsen
andauthored
BaseUnit generation for the prefixed units (#1485)
- added a new `UnitPrefixBuilder` (called from the `QuantityJonsFilesParser`) - removed the skipped tests (with the "The BaseUnits are not yet supported by the prefix-generator" reason) - added a `DebugDisplay` for the `Quantity` and the `Unit` (JsonTypes) and - added a `ToString` implementation to the `BaseDimensions` (JsonTypes) https://en.wikipedia.org/wiki/Metric_prefix --------- Co-authored-by: Andreas Gullberg Larsen <[email protected]>
1 parent 090742c commit 1f90e8f

File tree

88 files changed

+1185
-720
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

88 files changed

+1185
-720
lines changed

CodeGen/Generators/QuantityJsonFilesParser.cs

Lines changed: 66 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -6,123 +6,89 @@
66
using System.IO;
77
using System.Linq;
88
using CodeGen.Exceptions;
9-
using CodeGen.Helpers;
9+
using CodeGen.Helpers.PrefixBuilder;
1010
using CodeGen.JsonTypes;
1111
using Newtonsoft.Json;
12+
using static CodeGen.Helpers.PrefixBuilder.BaseUnitPrefixes;
1213

13-
namespace CodeGen.Generators
14+
namespace CodeGen.Generators;
15+
16+
/// <summary>
17+
/// Parses JSON files that define quantities and their units.
18+
/// This will later be used to generate source code and can be reused for different targets such as .NET framework,
19+
/// .NET Core, .NET nanoFramework and even other programming languages.
20+
/// </summary>
21+
internal static class QuantityJsonFilesParser
1422
{
23+
private static readonly JsonSerializerSettings JsonSerializerSettings = new()
24+
{
25+
// Don't override the C# default assigned values if no value is set in JSON
26+
NullValueHandling = NullValueHandling.Ignore
27+
};
28+
29+
private static readonly string[] BaseQuantityFileNames =
30+
["Length", "Mass", "Duration", "ElectricCurrent", "Temperature", "AmountOfSubstance", "LuminousIntensity"];
31+
1532
/// <summary>
1633
/// Parses JSON files that define quantities and their units.
17-
/// This will later be used to generate source code and can be reused for different targets such as .NET framework,
18-
/// .NET Core, .NET nanoFramework and even other programming languages.
1934
/// </summary>
20-
internal static class QuantityJsonFilesParser
35+
/// <param name="rootDir">Repository root directory, where you cloned the repo to such as "c:\dev\UnitsNet".</param>
36+
/// <returns>The parsed quantities and their units.</returns>
37+
public static Quantity[] ParseQuantities(string rootDir)
2138
{
22-
private static readonly JsonSerializerSettings JsonSerializerSettings = new JsonSerializerSettings
23-
{
24-
// Don't override the C# default assigned values if no value is set in JSON
25-
NullValueHandling = NullValueHandling.Ignore
26-
};
39+
var jsonDir = Path.Combine(rootDir, "Common/UnitDefinitions");
40+
var baseQuantityFiles = BaseQuantityFileNames.Select(baseQuantityName => Path.Combine(jsonDir, baseQuantityName + ".json")).ToArray();
2741

28-
/// <summary>
29-
/// Parses JSON files that define quantities and their units.
30-
/// </summary>
31-
/// <param name="rootDir">Repository root directory, where you cloned the repo to such as "c:\dev\UnitsNet".</param>
32-
/// <returns>The parsed quantities and their units.</returns>
33-
public static Quantity[] ParseQuantities(string rootDir)
34-
{
35-
var jsonDir = Path.Combine(rootDir, "Common/UnitDefinitions");
36-
var jsonFileNames = Directory.GetFiles(jsonDir, "*.json");
37-
return jsonFileNames
38-
.OrderBy(fn => fn, StringComparer.InvariantCultureIgnoreCase)
39-
.Select(ParseQuantityFile)
40-
.ToArray();
41-
}
42+
Quantity[] baseQuantities = ParseQuantities(baseQuantityFiles);
43+
Quantity[] derivedQuantities = ParseQuantities(Directory.GetFiles(jsonDir, "*.json").Except(baseQuantityFiles));
4244

43-
private static Quantity ParseQuantityFile(string jsonFileName)
44-
{
45-
try
46-
{
47-
var quantity = JsonConvert.DeserializeObject<Quantity>(File.ReadAllText(jsonFileName), JsonSerializerSettings)
48-
?? throw new UnitsNetCodeGenException($"Unable to parse quantity from JSON file: {jsonFileName}");
45+
return BuildQuantities(baseQuantities, derivedQuantities);
46+
}
4947

50-
AddPrefixUnits(quantity);
51-
OrderUnitsByName(quantity);
52-
return quantity;
53-
}
54-
catch (Exception e)
55-
{
56-
throw new Exception($"Error parsing quantity JSON file: {jsonFileName}", e);
57-
}
58-
}
48+
private static Quantity[] ParseQuantities(IEnumerable<string> jsonFiles)
49+
{
50+
return jsonFiles.Select(ParseQuantity).ToArray();
51+
}
5952

60-
private static void OrderUnitsByName(Quantity quantity)
53+
private static Quantity ParseQuantity(string jsonFileName)
54+
{
55+
try
6156
{
62-
quantity.Units = quantity.Units.OrderBy(u => u.SingularName, StringComparer.OrdinalIgnoreCase).ToArray();
57+
return JsonConvert.DeserializeObject<Quantity>(File.ReadAllText(jsonFileName), JsonSerializerSettings)
58+
?? throw new UnitsNetCodeGenException($"Unable to parse quantity from JSON file: {jsonFileName}");
6359
}
64-
65-
private static void AddPrefixUnits(Quantity quantity)
60+
catch (Exception e)
6661
{
67-
var unitsToAdd = new List<Unit>();
68-
foreach (Unit unit in quantity.Units)
69-
foreach (Prefix prefix in unit.Prefixes)
70-
{
71-
try
72-
{
73-
var prefixInfo = PrefixInfo.Entries[prefix];
74-
75-
unitsToAdd.Add(new Unit
76-
{
77-
SingularName = $"{prefix}{unit.SingularName.ToCamelCase()}", // "Kilo" + "NewtonPerMeter" => "KilonewtonPerMeter"
78-
PluralName = $"{prefix}{unit.PluralName.ToCamelCase()}", // "Kilo" + "NewtonsPerMeter" => "KilonewtonsPerMeter"
79-
BaseUnits = null, // Can we determine this somehow?
80-
FromBaseToUnitFunc = $"({unit.FromBaseToUnitFunc}) / {prefixInfo.Factor}",
81-
FromUnitToBaseFunc = $"({unit.FromUnitToBaseFunc}) * {prefixInfo.Factor}",
82-
Localization = GetLocalizationForPrefixUnit(unit.Localization, prefixInfo),
83-
ObsoleteText = unit.ObsoleteText,
84-
SkipConversionGeneration = unit.SkipConversionGeneration,
85-
AllowAbbreviationLookup = unit.AllowAbbreviationLookup
86-
} );
87-
}
88-
catch (Exception e)
89-
{
90-
throw new Exception($"Error parsing prefix {prefix} for unit {quantity.Name}.{unit.SingularName}.", e);
91-
}
92-
}
93-
94-
quantity.Units = quantity.Units.Concat(unitsToAdd).ToArray();
62+
throw new Exception($"Error parsing quantity JSON file: {jsonFileName}", e);
9563
}
64+
}
9665

97-
/// <summary>
98-
/// Create unit abbreviations for a prefix unit, given a unit and the prefix.
99-
/// The unit abbreviations are either prefixed with the SI prefix or an explicitly configured abbreviation via
100-
/// <see cref="Localization.AbbreviationsForPrefixes" />.
101-
/// </summary>
102-
private static Localization[] GetLocalizationForPrefixUnit(IEnumerable<Localization> localizations, PrefixInfo prefixInfo)
66+
/// <summary>
67+
/// Combines base quantities and derived quantities into a single collection,
68+
/// while generating prefixed units for each quantity.
69+
/// </summary>
70+
/// <param name="baseQuantities">
71+
/// The array of base quantities, each containing its respective units.
72+
/// </param>
73+
/// <param name="derivedQuantities">
74+
/// The array of derived quantities, each containing its respective units.
75+
/// </param>
76+
/// <returns>
77+
/// An ordered array of all quantities, including both base and derived quantities,
78+
/// with prefixed units generated and added to their respective unit collections.
79+
/// </returns>
80+
/// <remarks>
81+
/// This method utilizes the <see cref="UnitPrefixBuilder" /> to generate prefixed units
82+
/// for each quantity. The resulting quantities are sorted alphabetically by their names.
83+
/// </remarks>
84+
private static Quantity[] BuildQuantities(Quantity[] baseQuantities, Quantity[] derivedQuantities)
85+
{
86+
var prefixBuilder = new UnitPrefixBuilder(FromBaseUnits(baseQuantities.SelectMany(x => x.Units)));
87+
return baseQuantities.Concat(derivedQuantities).Select(quantity =>
10388
{
104-
return localizations.Select(loc =>
105-
{
106-
if (loc.TryGetAbbreviationsForPrefix(prefixInfo.Prefix, out string[]? unitAbbreviationsForPrefix))
107-
{
108-
return new Localization
109-
{
110-
Culture = loc.Culture,
111-
Abbreviations = unitAbbreviationsForPrefix
112-
};
113-
}
114-
115-
// No prefix unit abbreviations are specified, so fall back to prepending the default SI prefix to each unit abbreviation:
116-
// kilo ("k") + meter ("m") => kilometer ("km")
117-
var prefix = prefixInfo.GetPrefixForCultureOrSiPrefix(loc.Culture);
118-
unitAbbreviationsForPrefix = loc.Abbreviations.Select(unitAbbreviation => $"{prefix}{unitAbbreviation}").ToArray();
119-
120-
return new Localization
121-
{
122-
Culture = loc.Culture,
123-
Abbreviations = unitAbbreviationsForPrefix
124-
};
125-
}).ToArray();
126-
}
89+
List<Unit> prefixedUnits = prefixBuilder.GeneratePrefixUnits(quantity);
90+
quantity.Units = quantity.Units.Concat(prefixedUnits).OrderBy(unit => unit.SingularName, StringComparer.OrdinalIgnoreCase).ToArray();
91+
return quantity;
92+
}).OrderBy(quantity => quantity.Name, StringComparer.InvariantCultureIgnoreCase).ToArray();
12793
}
12894
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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 CodeGen.JsonTypes;
5+
6+
namespace CodeGen.Helpers.PrefixBuilder;
7+
8+
/// <summary>
9+
/// Represents a unique key that combines a base unit and a prefix.
10+
/// </summary>
11+
/// <param name="BaseUnit">
12+
/// The base unit associated with the prefix. For example, "Gram".
13+
/// </param>
14+
/// <param name="Prefix">
15+
/// The prefix applied to the base unit. For example, <see cref="JsonTypes.Prefix.Kilo" />.
16+
/// </param>
17+
internal readonly record struct BaseUnitPrefix(string BaseUnit, Prefix Prefix);
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
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.Collections.Generic;
5+
using System.Diagnostics.CodeAnalysis;
6+
using System.Linq;
7+
using CodeGen.JsonTypes;
8+
9+
namespace CodeGen.Helpers.PrefixBuilder;
10+
11+
/// <summary>
12+
/// Represents a collection of base unit prefixes and their associated mappings.
13+
/// </summary>
14+
/// <remarks>
15+
/// This class provides functionality to manage and retrieve mappings between base units and their prefixed
16+
/// counterparts,
17+
/// including scale factors and prefixed unit names. It supports operations such as creating mappings from a collection
18+
/// of base units and finding matching prefixes for specific units.
19+
/// </remarks>
20+
internal class BaseUnitPrefixes
21+
{
22+
/// <summary>
23+
/// A dictionary that maps metric prefixes to their corresponding exponent values.
24+
/// </summary>
25+
/// <remarks>
26+
/// This dictionary excludes binary prefixes such as Kibi, Mebi, Gibi, Tebi, Pebi, and Exbi.
27+
/// </remarks>
28+
private static readonly Dictionary<Prefix, int> MetricPrefixFactors = PrefixInfo.Entries.Where(x => x.Key.IsMetricPrefix())
29+
.ToDictionary(pair => pair.Key, pair => pair.Value.GetDecimalExponent());
30+
31+
/// <summary>
32+
/// A dictionary that maps the exponent values to their corresponding <see cref="Prefix" />.
33+
/// This is used to find the appropriate prefix for a given factor.
34+
/// </summary>
35+
private static readonly Dictionary<int, Prefix> PrefixFactorsByValue = MetricPrefixFactors.ToDictionary(pair => pair.Value, pair => pair.Key);
36+
37+
/// <summary>
38+
/// Lookup of prefixed unit name from base unit + prefix pairs, such as ("Gram", Prefix.Kilo) => "Kilogram".
39+
/// </summary>
40+
private readonly Dictionary<BaseUnitPrefix, string> _baseUnitPrefixConversions;
41+
42+
/// <summary>
43+
/// A dictionary that maps prefixed unit strings to their corresponding base unit and fractional factor.
44+
/// </summary>
45+
/// <remarks>
46+
/// This dictionary is used to handle units with SI prefixes, allowing for the conversion of prefixed units
47+
/// to their base units and the associated fractional factors. The keys are the prefixed unit strings, and the values
48+
/// are tuples containing the base unit string and the fractional factor.
49+
/// </remarks>
50+
private readonly Dictionary<string, PrefixScaleFactor> _prefixedStringFactors;
51+
52+
private BaseUnitPrefixes(Dictionary<string, PrefixScaleFactor> prefixedStringFactors, Dictionary<BaseUnitPrefix, string> baseUnitPrefixConversions)
53+
{
54+
_prefixedStringFactors = prefixedStringFactors;
55+
_baseUnitPrefixConversions = baseUnitPrefixConversions;
56+
}
57+
58+
/// <summary>
59+
/// Creates an instance of <see cref="BaseUnitPrefixes" /> from a collection of base units.
60+
/// </summary>
61+
/// <param name="baseUnits">
62+
/// A collection of base units, each containing a singular name and associated prefixes.
63+
/// </param>
64+
/// <returns>
65+
/// A new instance of <see cref="BaseUnitPrefixes" /> containing mappings of base units
66+
/// and their prefixed counterparts.
67+
/// </returns>
68+
/// <remarks>
69+
/// This method processes the provided base units to generate mappings between base unit prefixes
70+
/// and their corresponding prefixed unit names, as well as scale factors for each prefixed unit.
71+
/// </remarks>
72+
public static BaseUnitPrefixes FromBaseUnits(IEnumerable<Unit> baseUnits)
73+
{
74+
var baseUnitPrefixConversions = new Dictionary<BaseUnitPrefix, string>();
75+
var prefixedStringFactors = new Dictionary<string, PrefixScaleFactor>();
76+
foreach (Unit baseUnit in baseUnits)
77+
{
78+
var unitName = baseUnit.SingularName;
79+
prefixedStringFactors[unitName] = new PrefixScaleFactor(unitName, 0);
80+
foreach (Prefix prefix in baseUnit.Prefixes)
81+
{
82+
var prefixedUnitName = prefix + unitName.ToCamelCase();
83+
baseUnitPrefixConversions[new BaseUnitPrefix(unitName, prefix)] = prefixedUnitName;
84+
prefixedStringFactors[prefixedUnitName] = new PrefixScaleFactor(unitName, MetricPrefixFactors[prefix]);
85+
}
86+
}
87+
88+
return new BaseUnitPrefixes(prefixedStringFactors, baseUnitPrefixConversions);
89+
}
90+
91+
/// <summary>
92+
/// Attempts to find a matching prefix for a given unit name, exponent, and prefix.
93+
/// </summary>
94+
/// <param name="unitName">
95+
/// The name of the unit to match. For example, "Meter".
96+
/// </param>
97+
/// <param name="exponent">
98+
/// The exponent associated with the unit. For example, 3 for cubic meters.
99+
/// </param>
100+
/// <param name="prefix">
101+
/// The prefix to match. For example, <see cref="Prefix.Kilo" />.
102+
/// </param>
103+
/// <param name="matchingPrefix">
104+
/// When this method returns, contains the matching <see cref="BaseUnitPrefix" /> if a match is found;
105+
/// otherwise, the default value of <see cref="BaseUnitPrefix" />.
106+
/// </param>
107+
/// <returns>
108+
/// <see langword="true" /> if a matching prefix is found; otherwise, <see langword="false" />.
109+
/// </returns>
110+
/// <remarks>
111+
/// This method determines if a given unit can be associated with a specific prefix, given the exponent of the
112+
/// associated dimension.
113+
/// </remarks>
114+
internal bool TryGetMatchingPrefix(string unitName, int exponent, Prefix prefix, out BaseUnitPrefix matchingPrefix)
115+
{
116+
if (exponent == 0 || !_prefixedStringFactors.TryGetValue(unitName, out PrefixScaleFactor? targetPrefixFactor))
117+
{
118+
matchingPrefix = default;
119+
return false;
120+
}
121+
122+
if (MetricPrefixFactors.TryGetValue(prefix, out var prefixFactor))
123+
{
124+
var (quotient, remainder) = int.DivRem(prefixFactor, exponent);
125+
// Ensure the prefix factor is divisible by the exponent without a remainder and that there is a valid prefix matching the target scale
126+
if (remainder == 0 && TryGetPrefixWithScale(targetPrefixFactor.ScaleFactor + quotient, out Prefix calculatedPrefix))
127+
{
128+
matchingPrefix = new BaseUnitPrefix(targetPrefixFactor.BaseUnit, calculatedPrefix);
129+
return true;
130+
}
131+
}
132+
133+
matchingPrefix = default;
134+
return false;
135+
}
136+
137+
private static bool TryGetPrefixWithScale(int logScale, out Prefix calculatedPrefix)
138+
{
139+
return PrefixFactorsByValue.TryGetValue(logScale, out calculatedPrefix);
140+
}
141+
142+
/// <summary>
143+
/// Attempts to retrieve the prefixed unit name for a given base unit and prefix combination.
144+
/// </summary>
145+
/// <param name="prefix">
146+
/// A <see cref="BaseUnitPrefix" /> representing the combination of a base unit and a prefix.
147+
/// </param>
148+
/// <param name="prefixedUnitName">
149+
/// When this method returns, contains the prefixed unit name if the lookup was successful; otherwise, <c>null</c>.
150+
/// </param>
151+
/// <returns>
152+
/// <c>true</c> if the prefixed unit name was successfully retrieved; otherwise, <c>false</c>.
153+
/// </returns>
154+
internal bool TryGetPrefixForUnit(BaseUnitPrefix prefix, [NotNullWhen(true)] out string? prefixedUnitName)
155+
{
156+
return _baseUnitPrefixConversions.TryGetValue(prefix, out prefixedUnitName);
157+
}
158+
159+
/// <summary>
160+
/// Represents the scaling factor that is required for converting from the <see cref="BaseUnit" />.
161+
/// </summary>
162+
/// <param name="BaseUnit">Name of base unit, e.g. "Meter".</param>
163+
/// <param name="ScaleFactor">The log-scale factor, e.g. 3 for kilometer.</param>
164+
private record PrefixScaleFactor(string BaseUnit, int ScaleFactor);
165+
}

0 commit comments

Comments
 (0)