diff --git a/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.QuantitiesGenerator/Capacity.g.cs b/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.QuantitiesGenerator/Capacity.g.cs
index c06801b..25c1751 100644
--- a/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.QuantitiesGenerator/Capacity.g.cs
+++ b/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.QuantitiesGenerator/Capacity.g.cs
@@ -112,7 +112,7 @@ namespace ktsu.Semantics.Quantities;
/// The value in Milliliter.
/// A new Capacity instance.
/// Thrown when the resulting magnitude would be negative.
- public static Capacity FromMilliliter(T value) => Create(Vector0Guards.EnsureNonNegative((value * MetricMagnitudes.Values.Milli), nameof(value)));
+ public static Capacity FromMilliliter(T value) => Create(Vector0Guards.EnsureNonNegative((value * (MetricMagnitudes.Values.Milli * Units.ConversionConstants.Values.LiterToCubicMeters)), nameof(value)));
///
/// Creates a new Capacity from a value in CubicCentimeter.
diff --git a/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.QuantitiesGenerator/Volume.g.cs b/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.QuantitiesGenerator/Volume.g.cs
index a90a46c..798bc33 100644
--- a/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.QuantitiesGenerator/Volume.g.cs
+++ b/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.QuantitiesGenerator/Volume.g.cs
@@ -111,7 +111,7 @@ namespace ktsu.Semantics.Quantities;
/// The value in Milliliter.
/// A new instance.
/// Thrown when the resulting magnitude would be negative.
- public static Volume FromMilliliter(T value) => Create(Vector0Guards.EnsureNonNegative((value * MetricMagnitudes.Values.Milli), nameof(value)));
+ public static Volume FromMilliliter(T value) => Create(Vector0Guards.EnsureNonNegative((value * (MetricMagnitudes.Values.Milli * Units.ConversionConstants.Values.LiterToCubicMeters)), nameof(value)));
///
/// Creates a new from a value in CubicCentimeter.
diff --git a/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.UnitsGenerator/Units.g.cs b/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.UnitsGenerator/Units.g.cs
index 911ec21..4c3bc3b 100644
--- a/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.UnitsGenerator/Units.g.cs
+++ b/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.UnitsGenerator/Units.g.cs
@@ -948,7 +948,7 @@ public sealed record Milliliter : IUnit, IVolumeUnit
public DimensionInfo Dimension => PhysicalDimensions.Volume;
/// Gets the multiplication factor used in the to-base affine conversion.
- public double ToBaseFactor => MetricMagnitudes.Milli;
+ public double ToBaseFactor => MetricMagnitudes.Milli * LiterToCubicMeters;
/// Gets the additive offset used in the to-base affine conversion.
public double ToBaseOffset => 0d;
@@ -957,7 +957,7 @@ public sealed record Milliliter : IUnit, IVolumeUnit
public Milliliter() { }
///
- T IUnit.ToBaseFactorAs() => MetricMagnitudes.Values.Milli;
+ T IUnit.ToBaseFactorAs() => MetricMagnitudes.Values.Milli * ConversionConstants.Values.LiterToCubicMeters;
///
T IUnit.ToBaseOffsetAs() => T.Zero;
diff --git a/Semantics.SourceGenerators/Metadata/units.json b/Semantics.SourceGenerators/Metadata/units.json
index e3550a6..6f416b4 100644
--- a/Semantics.SourceGenerators/Metadata/units.json
+++ b/Semantics.SourceGenerators/Metadata/units.json
@@ -200,7 +200,8 @@
"symbol": "mL",
"description": "Milliliter - 0.001 liters.",
"system": "SIDerived",
- "magnitude": "Milli"
+ "magnitude": "Milli",
+ "conversionFactor": "LiterToCubicMeters"
},
{
"name": "Gallon",
diff --git a/Semantics.Test/Quantities/UnitBackfillTests.cs b/Semantics.Test/Quantities/UnitBackfillTests.cs
index c36d8d2..e0844e4 100644
--- a/Semantics.Test/Quantities/UnitBackfillTests.cs
+++ b/Semantics.Test/Quantities/UnitBackfillTests.cs
@@ -2,6 +2,10 @@
namespace ktsu.Semantics.Test.Quantities;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
using ktsu.Semantics.Quantities;
using ktsu.Semantics.Quantities.Units;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -45,6 +49,75 @@ public void Temperature_FromRankine_Is_Absolute_With_Fahrenheit_Degrees()
Assert.AreEqual(273.15, t.Value, Tolerance);
}
+ // ---- Volume: the litre is not the base unit, so a prefix alone is not enough ----
+
+ [TestMethod]
+ public void Volume_FromMilliliter_Is_1e6_CubicMeters()
+ {
+ Volume v = Volume.FromMilliliter(1.0);
+ Assert.AreEqual(1e-6, v.Value, Tolerance);
+ Assert.AreEqual(1e-6, Units.Milliliter.ToBaseFactor, Tolerance);
+ }
+
+ [TestMethod]
+ public void Volume_FromMilliliter_1000_Is_1_Liter()
+ {
+ Volume v = Volume.FromMilliliter(1000.0);
+ Assert.AreEqual(1.0, v.In(Units.Liter), Tolerance);
+ Assert.AreEqual(1.0, Capacity.FromMilliliter(1000.0).In(Units.Liter), Tolerance);
+ }
+
+ ///
+ /// Every SI-prefixed unit whose unprefixed form is also in the catalogue is exactly the prefix times
+ /// that unit. A prefixed unit declared with only a magnitude is scaled from the dimension's base unit,
+ /// which is right for Millimeter (from Meter) and wrong for Milliliter (the litre is not m³).
+ ///
+ [TestMethod]
+ public void EveryPrefixedUnit_Is_ThePrefixTimesItsUnprefixedUnit()
+ {
+ (string Name, double Scale)[] prefixes =
+ [
+ ("Pico", 1e-12), ("Nano", 1e-9), ("Micro", 1e-6), ("Milli", 1e-3), ("Centi", 1e-2),
+ ("Kilo", 1e3), ("Mega", 1e6),
+ ];
+
+ Dictionary units = typeof(Units)
+ .GetFields(BindingFlags.Public | BindingFlags.Static)
+ .Select(static field => field.GetValue(null))
+ .OfType()
+ .GroupBy(static unit => unit.Name, StringComparer.Ordinal)
+ .ToDictionary(static group => group.Key, static group => group.First(), StringComparer.Ordinal);
+
+ List checkedUnits = [];
+ List wrong = [];
+ foreach (IUnit unit in units.Values)
+ {
+ foreach ((string prefix, double scale) in prefixes)
+ {
+ if (!unit.Name.StartsWith(prefix, StringComparison.Ordinal) || unit.Name.Length == prefix.Length)
+ {
+ continue;
+ }
+
+ string rest = unit.Name[prefix.Length..];
+ if (!units.TryGetValue(char.ToUpperInvariant(rest[0]) + rest[1..], out IUnit? unprefixed))
+ {
+ continue;
+ }
+
+ checkedUnits.Add(unit.Name);
+ double expected = scale * unprefixed.ToBaseFactor;
+ if (Math.Abs(unit.ToBaseFactor - expected) > Math.Abs(expected) * 1e-12)
+ {
+ wrong.Add($"{unit.Name} ({unit.ToBaseFactor}, expected {expected})");
+ }
+ }
+ }
+
+ CollectionAssert.Contains(checkedUnits, "Milliliter", "The scan should pair Milliliter with Liter.");
+ Assert.IsEmpty(wrong, $"These prefixed units are not the prefix times their unprefixed unit: {string.Join(", ", wrong)}");
+ }
+
// ---- Length / Area / Volume ----
[TestMethod]