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
diff --git a/CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs b/CodeGen/Helpers/PrefixBuilder/UnitPrefixBuilder.cs
index 7ef1a63139..54e9c85a6a 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,21 @@ 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 m², 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);
+
private readonly BaseUnitPrefixes _prefixes;
///
@@ -45,6 +62,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,
@@ -54,34 +74,86 @@ 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())
{
- 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
- });
+ continue;
}
- catch (Exception e)
+
+ ThrowIfPrefixesAreUnsafeForPoweredUnit(quantity, unit);
+
+ 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);
+ }
}
}
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))
+ {
+ 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)
+ {
+ // 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))
+ {
+ 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..d35e4f67e6 100644
--- a/Docs/quantity-and-unit-definition-schema.md
+++ b/Docs/quantity-and-unit-definition-schema.md
@@ -206,6 +206,16 @@ 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. 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
Each `Localization` entry configures abbreviations for one culture.