Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ namespace ktsu.Semantics.Quantities;
/// <param name="value">The value in Milliliter.</param>
/// <returns>A new Capacity instance.</returns>
/// <exception cref="System.ArgumentException">Thrown when the resulting magnitude would be negative.</exception>
public static Capacity<T> FromMilliliter(T value) => Create(Vector0Guards.EnsureNonNegative((value * MetricMagnitudes.Values<T>.Milli), nameof(value)));
public static Capacity<T> FromMilliliter(T value) => Create(Vector0Guards.EnsureNonNegative((value * (MetricMagnitudes.Values<T>.Milli * Units.ConversionConstants.Values<T>.LiterToCubicMeters)), nameof(value)));

/// <summary>
/// Creates a new Capacity from a value in CubicCentimeter.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ namespace ktsu.Semantics.Quantities;
/// <param name="value">The value in Milliliter.</param>
/// <returns>A new <see cref="Volume{T}"/> instance.</returns>
/// <exception cref="System.ArgumentException">Thrown when the resulting magnitude would be negative.</exception>
public static Volume<T> FromMilliliter(T value) => Create(Vector0Guards.EnsureNonNegative((value * MetricMagnitudes.Values<T>.Milli), nameof(value)));
public static Volume<T> FromMilliliter(T value) => Create(Vector0Guards.EnsureNonNegative((value * (MetricMagnitudes.Values<T>.Milli * Units.ConversionConstants.Values<T>.LiterToCubicMeters)), nameof(value)));

/// <summary>
/// Creates a new <see cref="Volume{T}"/> from a value in CubicCentimeter.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -948,7 +948,7 @@ public sealed record Milliliter : IUnit, IVolumeUnit
public DimensionInfo Dimension => PhysicalDimensions.Volume;

/// <summary>Gets the multiplication factor used in the to-base affine conversion.</summary>
public double ToBaseFactor => MetricMagnitudes.Milli;
public double ToBaseFactor => MetricMagnitudes.Milli * LiterToCubicMeters;

/// <summary>Gets the additive offset used in the to-base affine conversion.</summary>
public double ToBaseOffset => 0d;
Expand All @@ -957,7 +957,7 @@ public sealed record Milliliter : IUnit, IVolumeUnit
public Milliliter() { }

/// <inheritdoc/>
T IUnit.ToBaseFactorAs<T>() => MetricMagnitudes.Values<T>.Milli;
T IUnit.ToBaseFactorAs<T>() => MetricMagnitudes.Values<T>.Milli * ConversionConstants.Values<T>.LiterToCubicMeters;

/// <inheritdoc/>
T IUnit.ToBaseOffsetAs<T>() => T.Zero;
Expand Down
3 changes: 2 additions & 1 deletion Semantics.SourceGenerators/Metadata/units.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,8 @@
"symbol": "mL",
"description": "Milliliter - 0.001 liters.",
"system": "SIDerived",
"magnitude": "Milli"
"magnitude": "Milli",
"conversionFactor": "LiterToCubicMeters"
},
{
"name": "Gallon",
Expand Down
73 changes: 73 additions & 0 deletions Semantics.Test/Quantities/UnitBackfillTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -45,6 +49,75 @@
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<double> v = Volume<double>.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<double> v = Volume<double>.FromMilliliter(1000.0);
Assert.AreEqual(1.0, v.In(Units.Liter), Tolerance);
Assert.AreEqual(1.0, Capacity<double>.FromMilliliter(1000.0).In(Units.Liter), Tolerance);
}

/// <summary>
/// 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³).
/// </summary>
[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<string, IUnit> units = typeof(Units)
.GetFields(BindingFlags.Public | BindingFlags.Static)
.Select(static field => field.GetValue(null))
.OfType<IUnit>()
.GroupBy(static unit => unit.Name, StringComparer.Ordinal)
.ToDictionary(static group => group.Key, static group => group.First(), StringComparer.Ordinal);

List<string> checkedUnits = [];
List<string> 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.");

Check warning on line 117 in Semantics.Test/Quantities/UnitBackfillTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'CollectionAssert.Contains'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Semantics&issues=AaERSoG6H3hKMZTgqqM7&open=AaERSoG6H3hKMZTgqqM7&pullRequest=382
Assert.IsEmpty(wrong, $"These prefixed units are not the prefix times their unprefixed unit: {string.Join(", ", wrong)}");
}

// ---- Length / Area / Volume ----

[TestMethod]
Expand Down
Loading