Skip to content

Commit 424090f

Browse files
authored
✨ Allocate unique unit enum values (#1097)
Fixes #1068 Modify CodeGen to allocate unique unit enum values and store them in a JSON file. This fixes the problem of breaking changes to the unit enum values when adding a new unit, because the enum names are sorted lexically and their values are implicitly generated. ### Summary of solution Every time CodeGen is run, it loads a JSON file with the allocated unit enum values. If a new unit has been added since last time, it will allocate a new enum value for that unit. The allocation considers the first 10 available enum values, starting from `1` and includes any gaps in the sequence. So if `LengthUnit` enum already has allocated values 2,3,4,6 then it considers the values 1,5,7,8,9,10,11,12,13,14. It then picks a random value from that available number sequence as the new unit's enum value. This helps avoid or simplify merge conflicts adding units to the same quantity, by not competing for the same "next" value. Since we randomly pick from the next 10 values, we will produce gaps, which is why we also try to fill those gaps in future allocations. It then ensures there are now duplicate values, and throws if found. This will effectively break the build and describe what units are in conflict. Next it saves the JSON file with the latest values, to be included in the PR for adding a new unit. Finally it passes the values to `UnitTypeGenerator` to explicitly assign values when generating each unit enum type. **Common/UnitEnumValues.g.json** ```json { "Acceleration": { "CentimeterPerSecondSquared": 1, "DecimeterPerSecondSquared": 2, "FootPerSecondSquared": 3 }, "Length": { // ... } } ``` ```cs public enum AccelerationUnit { // Before Undefined = 0, CentimeterPerSecondSquared, DecimeterPerSecondSquared, FootPerSecondSquared, // After Undefined = 0, CentimeterPerSecondSquared = 1, DecimeterPerSecondSquared = 2, FootPerSecondSquared = 3, ``` ### Changes - Allocate new unit enum values before generating code - Update unit type generator to assign allocated enum values - Persist allocated unit enum values to `/Common/UnitEnumValues.g.json` - Throw exception if duplicate values are detected. - Pick from 10 random available values to avoid or simplify merge conflicts when merging multiple pull requests that add units to the same quantity, instead of competing for the same value.
1 parent 5a9f3dd commit 424090f

File tree

241 files changed

+4731
-2817
lines changed

Some content is hidden

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

241 files changed

+4731
-2817
lines changed

.editorconfig

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ root = true
88
[*]
99
indent_style = space
1010
trim_trailing_whitespace = true
11-
trim_trailing_white_space_on_save = true
1211
# (Please don't specify an indent_size here; that has too many unintended consequences.)
1312

1413
# Code files
@@ -66,7 +65,7 @@ dotnet_style_explicit_tuple_names = true:suggestion
6665
# Prefer "var" everywhere
6766
csharp_style_var_for_built_in_types = true:suggestion
6867
csharp_style_var_when_type_is_apparent = true:suggestion
69-
csharp_style_var_elsewhere = true:suggestion
68+
csharp_style_var_elsewhere = false:suggestion
7069

7170
# Prefer method-like constructs to have a block body
7271
csharp_style_expression_bodied_methods = false:none

CodeGen/Generators/NanoFrameworkGen/UnitTypeGenerator.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using CodeGen.Helpers;
2+
using CodeGen.Helpers.UnitEnumValueAllocation;
23
using CodeGen.JsonTypes;
34

45
namespace CodeGen.Generators.NanoFrameworkGen
@@ -7,11 +8,13 @@ namespace CodeGen.Generators.NanoFrameworkGen
78
internal class UnitTypeGenerator : GeneratorBase
89
{
910
private readonly Quantity _quantity;
11+
private readonly UnitEnumNameToValue _unitEnumNameToValue;
1012
private readonly string _unitEnumName;
1113

12-
public UnitTypeGenerator(Quantity quantity)
14+
public UnitTypeGenerator(Quantity quantity, UnitEnumNameToValue unitEnumNameToValue)
1315
{
1416
_quantity = quantity;
17+
_unitEnumNameToValue = unitEnumNameToValue;
1518
_unitEnumName = $"{quantity.Name}Unit";
1619
}
1720

@@ -48,7 +51,7 @@ public enum {_unitEnumName}
4851

4952
Writer.WLIfText(2, GetObsoleteAttributeOrNull(unit.ObsoleteText));
5053
Writer.WL($@"
51-
{unit.SingularName},");
54+
{unit.SingularName} = {_unitEnumNameToValue[unit.SingularName]},");
5255
}
5356

5457
Writer.WL($@"

CodeGen/Generators/NanoFrameworkGenerator.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Text.RegularExpressions;
77
using CodeGen.Generators.NanoFrameworkGen;
88
using CodeGen.Helpers;
9+
using CodeGen.Helpers.UnitEnumValueAllocation;
910
using CodeGen.JsonTypes;
1011
using NuGet.Common;
1112
using Serilog;
@@ -40,7 +41,8 @@ internal static class NanoFrameworkGenerator
4041
/// </summary>
4142
/// <param name="rootDir">The root directory</param>
4243
/// <param name="quantities">The quantities to create</param>
43-
public static void Generate(string rootDir, Quantity[] quantities)
44+
/// <param name="quantityNameToUnitEnumValues"></param>
45+
public static void Generate(string rootDir, Quantity[] quantities, QuantityNameToUnitEnumValues quantityNameToUnitEnumValues)
4446
{
4547
// get latest version of .NET nanoFramework mscorlib
4648
ILogger logger = NullLogger.Instance;
@@ -82,7 +84,9 @@ public static void Generate(string rootDir, Quantity[] quantities)
8284
versions.MscorlibNugetVersion,
8385
versions.MathNugetVersion);
8486

85-
GenerateUnitType(quantity, Path.Combine(outputUnits, $"{quantity.Name}Unit.g.cs"));
87+
UnitEnumNameToValue unitEnumValues = quantityNameToUnitEnumValues[quantity.Name];
88+
89+
GenerateUnitType(quantity, Path.Combine(outputUnits, $"{quantity.Name}Unit.g.cs"), unitEnumValues);
8690
GenerateQuantity(quantity, Path.Combine(outputQuantities, $"{quantity.Name}.g.cs"));
8791
GenerateProject(quantity, Path.Combine(projectPath, $"{quantity.Name}.nfproj"), versions);
8892

@@ -347,9 +351,9 @@ private static void GenerateProperties(string filePath, string version)
347351
Log.Information("✅ AssemblyInfo.cs (nanoFramework)");
348352
}
349353

350-
private static void GenerateUnitType(Quantity quantity, string filePath)
354+
private static void GenerateUnitType(Quantity quantity, string filePath, UnitEnumNameToValue unitEnumValues)
351355
{
352-
var content = new UnitTypeGenerator(quantity).Generate();
356+
var content = new UnitTypeGenerator(quantity, unitEnumValues).Generate();
353357
File.WriteAllText(filePath, content);
354358
}
355359

CodeGen/Generators/UnitsNetGen/UnitTypeGenerator.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
using CodeGen.Helpers;
2+
using CodeGen.Helpers.UnitEnumValueAllocation;
23
using CodeGen.JsonTypes;
34

45
namespace CodeGen.Generators.UnitsNetGen
56
{
67
internal class UnitTypeGenerator : GeneratorBase
78
{
89
private readonly Quantity _quantity;
10+
private readonly UnitEnumNameToValue _unitEnumNameToValue;
911
private readonly string _unitEnumName;
1012

11-
public UnitTypeGenerator(Quantity quantity)
13+
public UnitTypeGenerator(Quantity quantity, UnitEnumNameToValue unitEnumNameToValue)
1214
{
1315
_quantity = quantity;
16+
_unitEnumNameToValue = unitEnumNameToValue;
1417
_unitEnumName = $"{quantity.Name}Unit";
1518
}
1619

@@ -47,7 +50,7 @@ public enum {_unitEnumName}
4750

4851
Writer.WLIfText(2, GetObsoleteAttributeOrNull(unit.ObsoleteText));
4952
Writer.WL($@"
50-
{unit.SingularName},");
53+
{unit.SingularName} = {_unitEnumNameToValue[unit.SingularName]},");
5154
}
5255

5356
Writer.WL($@"

CodeGen/Generators/UnitsNetGenerator.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.IO;
55
using System.Linq;
66
using CodeGen.Generators.UnitsNetGen;
7+
using CodeGen.Helpers.UnitEnumValueAllocation;
78
using CodeGen.JsonTypes;
89
using Serilog;
910

@@ -31,7 +32,8 @@ internal static class UnitsNetGenerator
3132
/// </summary>
3233
/// <param name="rootDir">Path to repository root directory.</param>
3334
/// <param name="quantities">The parsed quantities.</param>
34-
public static void Generate(string rootDir, Quantity[] quantities)
35+
/// <param name="quantityNameToUnitEnumValues">Allocated unit enum values for generating unit enum types.</param>
36+
public static void Generate(string rootDir, Quantity[] quantities, QuantityNameToUnitEnumValues quantityNameToUnitEnumValues)
3537
{
3638
var outputDir = $"{rootDir}/UnitsNet/GeneratedCode";
3739
var extensionsOutputDir = $"{rootDir}/UnitsNet.NumberExtensions/GeneratedCode";
@@ -49,8 +51,10 @@ public static void Generate(string rootDir, Quantity[] quantities)
4951

5052
foreach (var quantity in quantities)
5153
{
54+
UnitEnumNameToValue unitEnumValues = quantityNameToUnitEnumValues[quantity.Name];
55+
5256
GenerateQuantity(quantity, $"{outputDir}/Quantities/{quantity.Name}.g.cs");
53-
GenerateUnitType(quantity, $"{outputDir}/Units/{quantity.Name}Unit.g.cs");
57+
GenerateUnitType(quantity, $"{outputDir}/Units/{quantity.Name}Unit.g.cs", unitEnumValues);
5458
GenerateNumberToExtensions(quantity, $"{extensionsOutputDir}/NumberTo{quantity.Name}Extensions.g.cs");
5559
GenerateNumberToExtensionsTestClass(quantity, $"{extensionsTestOutputDir}/NumberTo{quantity.Name}ExtensionsTest.g.cs");
5660

@@ -102,9 +106,9 @@ private static void GenerateNumberToExtensionsTestClass(Quantity quantity, strin
102106
File.WriteAllText(filePath, content);
103107
}
104108

105-
private static void GenerateUnitType(Quantity quantity, string filePath)
109+
private static void GenerateUnitType(Quantity quantity, string filePath, UnitEnumNameToValue unitEnumValues)
106110
{
107-
var content = new UnitTypeGenerator(quantity).Generate();
111+
var content = new UnitTypeGenerator(quantity, unitEnumValues).Generate();
108112
File.WriteAllText(filePath, content);
109113
}
110114

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 System.Collections.Generic;
5+
6+
namespace CodeGen.Helpers.UnitEnumValueAllocation
7+
{
8+
/// <summary>
9+
/// Data structure to allocate unique unit enum values that are preserved when adding new units.
10+
/// <br/><br/>
11+
/// Updating transitive UnitsNet dependency cause wrong unit · Issue #1068 · angularsen/UnitsNet
12+
/// https://github.com/angularsen/UnitsNet/issues/1068
13+
/// </summary>
14+
internal class QuantityNameToUnitEnumValues : Dictionary<string, UnitEnumNameToValue>
15+
{
16+
}
17+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
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 System.Text;
9+
using System.Text.Json;
10+
using CodeGen.Exceptions;
11+
using CodeGen.JsonTypes;
12+
using Serilog;
13+
14+
namespace CodeGen.Helpers.UnitEnumValueAllocation
15+
{
16+
/// <summary>
17+
/// Allocates unique enum values per quantity and persists the mapping to a JSON file to ensure the values do not
18+
/// change when adding new units.
19+
/// <br/><br/>
20+
/// Updating transitive UnitsNet dependency cause wrong unit · Issue #1068 · angularsen/UnitsNet
21+
/// https://github.com/angularsen/UnitsNet/issues/1068
22+
/// </summary>
23+
internal class UnitEnumValueAllocator
24+
{
25+
private static readonly JsonSerializerOptions JsonOptions = new()
26+
{
27+
AllowTrailingCommas = true, ReadCommentHandling = JsonCommentHandling.Skip, WriteIndented = true
28+
};
29+
30+
private readonly string _jsonFile;
31+
private readonly QuantityNameToUnitEnumValues _quantityNameToUnitEnumValues;
32+
33+
/// <summary>
34+
/// Creates an instance for the given JSON file path.
35+
/// </summary>
36+
/// <param name="jsonFile">Path to the JSON file that describes the currently allocated enum values.</param>
37+
private UnitEnumValueAllocator(string jsonFile)
38+
{
39+
_jsonFile = jsonFile;
40+
_quantityNameToUnitEnumValues = ReadFromFile(jsonFile);
41+
}
42+
43+
/// <summary>
44+
/// Ensure that all units have a unique unit enum value per quantity.
45+
/// The values are persisted to a JSON file to ensure the values do not change as new units are added later.
46+
/// </summary>
47+
/// <remarks>
48+
/// If the same value is found for two or more units, then an exception is thrown that effectively breaks the build
49+
/// with instructions on how
50+
/// to manually resolve the conflict by assigning unique values. This typically happens by merging two pull requests
51+
/// that both add units to the same
52+
/// quantity, which competes for the next available unit enum value.
53+
/// </remarks>
54+
/// <param name="jsonFile">The JSON file for storing the enum value allocations.</param>
55+
/// <param name="quantities">The list of quantities to ensure have unique unit enum values per quantity.</param>
56+
/// <returns></returns>
57+
internal static QuantityNameToUnitEnumValues AllocateNewUnitEnumValues(string jsonFile, Quantity[] quantities)
58+
{
59+
var unitEnumValueAllocator = new UnitEnumValueAllocator(jsonFile);
60+
61+
foreach (Quantity quantity in quantities)
62+
{
63+
unitEnumValueAllocator.AllocateNewUnitEnumValues(quantity);
64+
}
65+
66+
unitEnumValueAllocator.EnsureUniqueUnitEnumValuesPerQuantity();
67+
unitEnumValueAllocator.SaveToFile();
68+
69+
return unitEnumValueAllocator._quantityNameToUnitEnumValues;
70+
}
71+
72+
private void EnsureUniqueUnitEnumValuesPerQuantity()
73+
{
74+
List<string> duplicateErrorMessages = new();
75+
foreach ((var quantityName, UnitEnumNameToValue? unitEnumValues) in _quantityNameToUnitEnumValues)
76+
{
77+
// Minor optimization for the common case where there are no duplicates, to skip the more heavy LINQ of grouping and filtering.
78+
if (unitEnumValues.Values.Count != unitEnumValues.Values.ToHashSet().Count)
79+
{
80+
duplicateErrorMessages.AddRange(unitEnumValues
81+
.GroupBy(x => x.Value, x => x.Key) // Group unit names on enum value.
82+
.Where(g => g.Count() > 1) // More than one unit name is mapped to the same enum value.
83+
.Select(unitNames => $"{quantityName} has duplicate unit enum value {unitNames.Key} for units {string.Join(", ", unitNames)}."));
84+
}
85+
}
86+
87+
if (duplicateErrorMessages.Any())
88+
{
89+
throw new UnitsNetCodeGenException(
90+
@$"One or more units have the same unit enum value. This typically happens when merging multiple pull requests adding units to the same quantity.
91+
Resolve this by manually editing the JSON file to assign unique unit enum values per quantity.
92+
93+
JSON file:
94+
{_jsonFile}
95+
96+
Conflicts:
97+
{string.Join("\n", duplicateErrorMessages)}");
98+
}
99+
}
100+
101+
/// <summary>
102+
/// Allocates a unique enum value for all units of the given quantity that don't already have one stored in the JSON
103+
/// file.
104+
/// </summary>
105+
/// <param name="quantity">The quantity info.</param>
106+
private void AllocateNewUnitEnumValues(Quantity quantity)
107+
{
108+
foreach (Unit unit in quantity.Units)
109+
{
110+
EnsureUnitEnumValueIsAllocated(quantity, unit);
111+
}
112+
}
113+
114+
/// <summary>
115+
/// Allocates a unique enum value for the given unit, if not already allocated.
116+
/// </summary>
117+
/// <param name="quantity">The quantity info.</param>
118+
/// <param name="unit">The unit info.</param>
119+
private void EnsureUnitEnumValueIsAllocated(Quantity quantity, Unit unit)
120+
{
121+
// Get or create new list of enum values for quantity.
122+
if (!_quantityNameToUnitEnumValues.TryGetValue(quantity.Name, out UnitEnumNameToValue? enumValues))
123+
{
124+
enumValues = _quantityNameToUnitEnumValues[quantity.Name] = new UnitEnumNameToValue();
125+
}
126+
127+
// Already allocated enum value for this unit?
128+
if (enumValues.ContainsKey(unit.SingularName))
129+
{
130+
return;
131+
}
132+
133+
int value = enumValues.AssignUniqueValue(unit.SingularName);
134+
135+
Log.Information("Allocated new value {Value} for {Quantity}.{Unit}", value, quantity.Name, unit.SingularName);
136+
}
137+
138+
private void SaveToFile()
139+
{
140+
var fileContentStringBuilder = new StringBuilder();
141+
fileContentStringBuilder.Append(@"//------------------------------------------------------------------------------
142+
// <auto-generated>
143+
// This file is updated by \generate-code.bat and represents the unique unit enum values allocated when adding new units.
144+
// Do not modify this file manually unless you know what you are doing, as it may cause breaking changes for consumers relying on consistent enum values.
145+
// </auto-generated>
146+
//------------------------------------------------------------------------------
147+
//
148+
// Licensed under MIT No Attribution, see LICENSE file at the root.
149+
// Copyright 2013 Andreas Gullberg Larsen ([email protected]). Maintained at https://github.com/angularsen/UnitsNet.
150+
");
151+
152+
fileContentStringBuilder.AppendLine(JsonSerializer.Serialize(_quantityNameToUnitEnumValues, JsonOptions));
153+
File.WriteAllText(_jsonFile, fileContentStringBuilder.ToString());
154+
}
155+
156+
/// <summary>
157+
/// Loads the stored allocations from the JSON file.
158+
/// </summary>
159+
/// <param name="jsonFile"></param>
160+
/// <exception cref="InvalidOperationException">Failed to deserialize.</exception>
161+
private static QuantityNameToUnitEnumValues ReadFromFile(string jsonFile)
162+
{
163+
if (File.Exists(jsonFile))
164+
{
165+
return JsonSerializer.Deserialize<QuantityNameToUnitEnumValues>(File.ReadAllText(jsonFile), JsonOptions)
166+
?? throw new InvalidOperationException($"Failed to deserialize file: {jsonFile}");
167+
}
168+
169+
return new QuantityNameToUnitEnumValues();
170+
}
171+
}
172+
}

0 commit comments

Comments
 (0)