From f5ee2c3e153ee17b1d2d660327836b2efbc9db9c Mon Sep 17 00:00:00 2001 From: Andreas Gullberg Larsen Date: Thu, 30 Jul 2026 11:42:14 +0200 Subject: [PATCH 1/8] Fail codegen for powered unit prefixes --- .../PrefixBuilder/UnitPrefixBuilder.cs | 33 +++++++++++++++++++ Docs/quantity-and-unit-definition-schema.md | 3 ++ 2 files changed, 36 insertions(+) diff --git a/CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs b/CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs index 7ef1a63139..c2fdeb226a 100644 --- a/CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs +++ b/CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Text.RegularExpressions; +using CodeGen.Exceptions; using CodeGen.JsonTypes; namespace CodeGen.Helpers.PrefixBuilder; @@ -18,6 +20,10 @@ namespace CodeGen.Helpers.PrefixBuilder; /// internal class UnitPrefixBuilder { + private static readonly Regex LeadingPoweredUnitAbbreviationRegex = new( + @"^[^\s/·*()\-\d]+(?:[²³⁴]|\^[234])", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + private readonly BaseUnitPrefixes _prefixes; /// @@ -58,6 +64,8 @@ public List GeneratePrefixUnits(Quantity quantity) { try { + ThrowIfPrefixesAreUnsafeForPoweredUnit(quantity, unit); + PrefixInfo prefixInfo = PrefixInfo.Entries[prefix]; unitsToAdd.Add(new Unit @@ -82,6 +90,31 @@ public List GeneratePrefixUnits(Quantity quantity) return unitsToAdd; } + private static void ThrowIfPrefixesAreUnsafeForPoweredUnit(Quantity quantity, Unit unit) + { + if (!LooksLikePoweredUnit(unit)) + { + return; + } + + throw new UnitsNetCodeGenException( + $"Prefixes cannot be used on {quantity.Name}.{unit.SingularName} because it looks like a powered unit. " + + "Define explicit units instead, such as CubicKilometer for km³ or ThousandCubicMeter for 1000 m³."); + } + + private static bool LooksLikePoweredUnit(Unit unit) + { + if (unit.SingularName.StartsWith("Square", StringComparison.Ordinal) || + unit.SingularName.StartsWith("Cubic", StringComparison.Ordinal)) + { + return true; + } + + return unit.Localization + .SelectMany(localization => localization.Abbreviations) + .Any(abbreviation => LeadingPoweredUnitAbbreviationRegex.IsMatch(abbreviation)); + } + /// /// Applies a metric prefix to the specified base units based on the given dimensions and prefix information. /// diff --git a/Docs/quantity-and-unit-definition-schema.md b/Docs/quantity-and-unit-definition-schema.md index 4d156cbb5b..a110135fc7 100644 --- a/Docs/quantity-and-unit-definition-schema.md +++ b/Docs/quantity-and-unit-definition-schema.md @@ -206,6 +206,9 @@ instead. For example, define `CubicMillimeterPerKilogram` for `mm³/kg` instead `MillicubicMeterPerKilogram`, and define `ThousandCubicMeter` with an abbreviation such as `10³·m³` when the intended unit is 1000 cubic meters rather than cubic kilometers. +CodeGen fails when `Prefixes` is used on a unit that looks like the powered unit itself, such as a unit name starting +with `Square` or `Cubic`, or an abbreviation starting with `m²`, `m³`, `ft³`, or similar powered unit symbols. + ## Localization object Each `Localization` entry configures abbreviations for one culture. From 87b228596ef5543e61d8ecf939e2196466c5d5a6 Mon Sep 17 00:00:00 2001 From: Andreas Gullberg Larsen Date: Thu, 30 Jul 2026 11:57:51 +0200 Subject: [PATCH 2/8] Git ignore .temp/ --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b319cb4b8b..e5bd1ed8e2 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ *.user *.userosscache *.sln.docstates +.temp/ # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs From e40f03e96df984dd606291e87ecc4ff18063bc4b Mon Sep 17 00:00:00 2001 From: Andreas Gullberg Larsen Date: Thu, 30 Jul 2026 11:42:14 +0200 Subject: [PATCH 3/8] Fail codegen for powered unit prefixes --- .../PrefixBuilder/UnitPrefixBuilder.cs | 33 +++++++++++++++++++ Docs/quantity-and-unit-definition-schema.md | 3 ++ 2 files changed, 36 insertions(+) diff --git a/CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs b/CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs index 7ef1a63139..c2fdeb226a 100644 --- a/CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs +++ b/CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Text.RegularExpressions; +using CodeGen.Exceptions; using CodeGen.JsonTypes; namespace CodeGen.Helpers.PrefixBuilder; @@ -18,6 +20,10 @@ namespace CodeGen.Helpers.PrefixBuilder; /// internal class UnitPrefixBuilder { + private static readonly Regex LeadingPoweredUnitAbbreviationRegex = new( + @"^[^\s/·*()\-\d]+(?:[²³⁴]|\^[234])", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + private readonly BaseUnitPrefixes _prefixes; /// @@ -58,6 +64,8 @@ public List GeneratePrefixUnits(Quantity quantity) { try { + ThrowIfPrefixesAreUnsafeForPoweredUnit(quantity, unit); + PrefixInfo prefixInfo = PrefixInfo.Entries[prefix]; unitsToAdd.Add(new Unit @@ -82,6 +90,31 @@ public List GeneratePrefixUnits(Quantity quantity) return unitsToAdd; } + private static void ThrowIfPrefixesAreUnsafeForPoweredUnit(Quantity quantity, Unit unit) + { + if (!LooksLikePoweredUnit(unit)) + { + return; + } + + throw new UnitsNetCodeGenException( + $"Prefixes cannot be used on {quantity.Name}.{unit.SingularName} because it looks like a powered unit. " + + "Define explicit units instead, such as CubicKilometer for km³ or ThousandCubicMeter for 1000 m³."); + } + + private static bool LooksLikePoweredUnit(Unit unit) + { + if (unit.SingularName.StartsWith("Square", StringComparison.Ordinal) || + unit.SingularName.StartsWith("Cubic", StringComparison.Ordinal)) + { + return true; + } + + return unit.Localization + .SelectMany(localization => localization.Abbreviations) + .Any(abbreviation => LeadingPoweredUnitAbbreviationRegex.IsMatch(abbreviation)); + } + /// /// Applies a metric prefix to the specified base units based on the given dimensions and prefix information. /// diff --git a/Docs/quantity-and-unit-definition-schema.md b/Docs/quantity-and-unit-definition-schema.md index 4d156cbb5b..a110135fc7 100644 --- a/Docs/quantity-and-unit-definition-schema.md +++ b/Docs/quantity-and-unit-definition-schema.md @@ -206,6 +206,9 @@ instead. For example, define `CubicMillimeterPerKilogram` for `mm³/kg` instead `MillicubicMeterPerKilogram`, and define `ThousandCubicMeter` with an abbreviation such as `10³·m³` when the intended unit is 1000 cubic meters rather than cubic kilometers. +CodeGen fails when `Prefixes` is used on a unit that looks like the powered unit itself, such as a unit name starting +with `Square` or `Cubic`, or an abbreviation starting with `m²`, `m³`, `ft³`, or similar powered unit symbols. + ## Localization object Each `Localization` entry configures abbreviations for one culture. From 91fff324c019b33c130a6e064115eb2fb6b4dc34 Mon Sep 17 00:00:00 2001 From: Andreas Gullberg Larsen Date: Thu, 30 Jul 2026 12:02:28 +0200 Subject: [PATCH 4/8] Refine powered prefix validation --- .../PrefixBuilder/UnitPrefixBuilder.cs | 49 ++++++++++++------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs b/CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs index c2fdeb226a..e3828d2196 100644 --- a/CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs +++ b/CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs @@ -51,6 +51,9 @@ public UnitPrefixBuilder(BaseUnitPrefixes prefixes) /// /// Thrown when an error occurs while processing a prefix for a unit, such as an invalid prefix or unit configuration. /// + /// + /// Thrown when prefixes are configured for a unit that looks like a powered unit. + /// /// /// This method iterates through the existing units of the specified and applies each defined /// prefix to generate new prefixed units. It ensures that the singular and plural names, conversion functions, @@ -60,30 +63,37 @@ public List GeneratePrefixUnits(Quantity quantity) { var unitsToAdd = new List(); foreach (Unit unit in quantity.Units) - foreach (Prefix prefix in unit.Prefixes) { - try + if (!unit.Prefixes.Any()) { - ThrowIfPrefixesAreUnsafeForPoweredUnit(quantity, unit); + continue; + } - PrefixInfo prefixInfo = PrefixInfo.Entries[prefix]; + ThrowIfPrefixesAreUnsafeForPoweredUnit(quantity, unit); - unitsToAdd.Add(new Unit - { - SingularName = $"{prefix}{unit.SingularName.ToCamelCase()}", // "Kilo" + "NewtonPerMeter" => "KilonewtonPerMeter" - PluralName = $"{prefix}{unit.PluralName.ToCamelCase()}", // "Kilo" + "NewtonsPerMeter" => "KilonewtonsPerMeter" - BaseUnits = GetPrefixedBaseUnits(quantity.BaseDimensions, unit.BaseUnits, prefixInfo), - FromBaseToUnitFunc = $"({unit.FromBaseToUnitFunc}) / {prefixInfo.Factor}", - FromUnitToBaseFunc = $"({unit.FromUnitToBaseFunc}) * {prefixInfo.Factor}", - Localization = GetLocalizationForPrefixUnit(unit.Localization, prefixInfo), - ObsoleteText = unit.ObsoleteText, - SkipConversionGeneration = unit.SkipConversionGeneration, - AllowAbbreviationLookup = unit.AllowAbbreviationLookup - }); - } - catch (Exception e) + foreach (Prefix prefix in unit.Prefixes) { - throw new Exception($"Error parsing prefix {prefix} for unit {quantity.Name}.{unit.SingularName}.", e); + try + { + PrefixInfo prefixInfo = PrefixInfo.Entries[prefix]; + + unitsToAdd.Add(new Unit + { + SingularName = $"{prefix}{unit.SingularName.ToCamelCase()}", // "Kilo" + "NewtonPerMeter" => "KilonewtonPerMeter" + PluralName = $"{prefix}{unit.PluralName.ToCamelCase()}", // "Kilo" + "NewtonsPerMeter" => "KilonewtonsPerMeter" + BaseUnits = GetPrefixedBaseUnits(quantity.BaseDimensions, unit.BaseUnits, prefixInfo), + FromBaseToUnitFunc = $"({unit.FromBaseToUnitFunc}) / {prefixInfo.Factor}", + FromUnitToBaseFunc = $"({unit.FromUnitToBaseFunc}) * {prefixInfo.Factor}", + Localization = GetLocalizationForPrefixUnit(unit.Localization, prefixInfo), + ObsoleteText = unit.ObsoleteText, + SkipConversionGeneration = unit.SkipConversionGeneration, + AllowAbbreviationLookup = unit.AllowAbbreviationLookup + }); + } + catch (Exception e) + { + throw new Exception($"Error parsing prefix {prefix} for unit {quantity.Name}.{unit.SingularName}.", e); + } } } @@ -104,6 +114,7 @@ private static void ThrowIfPrefixesAreUnsafeForPoweredUnit(Quantity quantity, Un private static bool LooksLikePoweredUnit(Unit unit) { + // This intentionally checks naming conventions rather than dimensions, since valid derived units can have powered base dimensions. if (unit.SingularName.StartsWith("Square", StringComparison.Ordinal) || unit.SingularName.StartsWith("Cubic", StringComparison.Ordinal)) { From 45687c09eddb81cdea70238104e5fd09816ea430 Mon Sep 17 00:00:00 2001 From: Andreas Gullberg Larsen Date: Thu, 30 Jul 2026 12:19:59 +0200 Subject: [PATCH 5/8] Document powered abbreviation heuristic --- CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs b/CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs index e3828d2196..b082829f75 100644 --- a/CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs +++ b/CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs @@ -20,6 +20,17 @@ namespace CodeGen.Helpers.PrefixBuilder; /// internal class UnitPrefixBuilder { + /// + /// Matches abbreviations whose leading unit token is raised to the second, third, or fourth power using a + /// superscript or caret, such as , ft³/s, or m^4. + /// + /// + /// This is intentionally not a general unit-expression parser. It does not match a power in a denominator or later + /// compound term (kg/m³, m/s², or N·m²), a leading numeric scale factor + /// (10³·m³), parenthesized or implicit powers, or powers other than two through four. These cases do not put + /// a metric prefix directly before an explicitly powered leading unit token, or are outside the scope of this + /// heuristic. Unit names starting with Square or Cubic are checked separately. + /// private static readonly Regex LeadingPoweredUnitAbbreviationRegex = new( @"^[^\s/·*()\-\d]+(?:[²³⁴]|\^[234])", RegexOptions.Compiled | RegexOptions.CultureInvariant); From 9401925eb6977703c5e46c564be0c8b38cda72a2 Mon Sep 17 00:00:00 2001 From: Andreas Gullberg Larsen Date: Thu, 30 Jul 2026 12:21:08 +0200 Subject: [PATCH 6/8] Document powered prefix validation --- .../Helpers/PrefixBuilder/UnitPrefixBuilder.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs b/CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs index b082829f75..621f1df783 100644 --- a/CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs +++ b/CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs @@ -111,6 +111,22 @@ public List GeneratePrefixUnits(Quantity quantity) return unitsToAdd; } + /// + /// Prevents automatic prefixes from being applied to units that appear to represent a directly powered unit. + /// + /// The quantity that defines the unit. + /// The unit configured with one or more automatic prefixes. + /// + /// Thrown when the unit name starts with Square or Cubic, or when one of its abbreviations starts with + /// a unit token raised to the second, third, or fourth power. + /// + /// + /// Mechanically prefixing a powered unit can produce a misleading abbreviation. For example, prefixing + /// CubicMeter with Kilo produces km³, which means cubic kilometer rather than one thousand + /// cubic meters. This guard uses unit names and as a focused + /// heuristic. It intentionally does not inspect base dimensions, since valid derived units such as watt and joule + /// have powered dimensions but can safely use automatic prefixes. + /// private static void ThrowIfPrefixesAreUnsafeForPoweredUnit(Quantity quantity, Unit unit) { if (!LooksLikePoweredUnit(unit)) From 9e87e5810da287f3766e80042157a26cbf2dd037 Mon Sep 17 00:00:00 2001 From: Andreas Gullberg Larsen Date: Thu, 30 Jul 2026 12:21:37 +0200 Subject: [PATCH 7/8] Clarify powered dimension examples --- CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs b/CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs index 621f1df783..54e9c85a6a 100644 --- a/CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs +++ b/CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs @@ -141,7 +141,8 @@ private static void ThrowIfPrefixesAreUnsafeForPoweredUnit(Quantity quantity, Un private static bool LooksLikePoweredUnit(Unit unit) { - // This intentionally checks naming conventions rather than dimensions, since valid derived units can have powered base dimensions. + // This intentionally checks naming conventions rather than dimensions, since derived units such as Watt, Joule, + // and Ohm have powered base dimensions and safely support prefixes. if (unit.SingularName.StartsWith("Square", StringComparison.Ordinal) || unit.SingularName.StartsWith("Cubic", StringComparison.Ordinal)) { From b8854cd8d9d247cfc2785a23475e8773580beceb Mon Sep 17 00:00:00 2001 From: Andreas Gullberg Larsen Date: Thu, 30 Jul 2026 12:22:36 +0200 Subject: [PATCH 8/8] Add powered prefix documentation examples --- Docs/quantity-and-unit-definition-schema.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Docs/quantity-and-unit-definition-schema.md b/Docs/quantity-and-unit-definition-schema.md index a110135fc7..d35e4f67e6 100644 --- a/Docs/quantity-and-unit-definition-schema.md +++ b/Docs/quantity-and-unit-definition-schema.md @@ -207,7 +207,14 @@ instead. For example, define `CubicMillimeterPerKilogram` for `mm³/kg` instead unit is 1000 cubic meters rather than cubic kilometers. CodeGen fails when `Prefixes` is used on a unit that looks like the powered unit itself, such as a unit name starting -with `Square` or `Cubic`, or an abbreviation starting with `m²`, `m³`, `ft³`, or similar powered unit symbols. +with `Square` or `Cubic`, or an abbreviation starting with `m²`, `m³`, `ft³`, or similar powered unit symbols. For +example, generating `Kilo` from `SquareMeter` would produce `km²`, which means one square kilometer (`1e6 m²`), not +one thousand square meters. Generating it from `CubicMeter` would produce `km³`, which means one cubic kilometer +(`1e9 m³`), not one thousand cubic meters. + +This is a naming and abbreviation heuristic, not a dimensional-analysis check. Derived units such as `Watt`, `Joule`, +and `Ohm` have powered SI base dimensions, but can safely use `Prefixes` because their own abbreviations are not +powered unit symbols. They produce unambiguous units such as `MW`, `MJ`, and `MΩ`. ## Localization object