From 1943656e5ee02a960a1c081b595fa725eb012b07 Mon Sep 17 00:00:00 2001 From: Tristan Milnthorp <5693174+tmilnthorp@users.noreply.github.com> Date: Tue, 6 Oct 2026 14:25:02 -0400 Subject: [PATCH] Convert quantities to and from a UnitOf UnitOf describes a unit of a quantity by its names and its value in the base unit of the quantity, without a unit enum value. Quantities can be converted to it with As(UnitOf), and created from a value in it with QuantityInfo.From(value, UnitOf), which returns the quantity in its base unit. This is the subset of the v6 API that v5 supports, so code written against it keeps compiling on v6. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012XKhDsyHDc5BHrmibxScqG --- README.md | 11 ++++ UnitsNet.Tests/UnitOfTests.cs | 99 +++++++++++++++++++++++++++++++++++ UnitsNet/UnitOf.cs | 90 +++++++++++++++++++++++++++++++ UnitsNet/UnitOfExtensions.cs | 58 ++++++++++++++++++++ 4 files changed, 258 insertions(+) create mode 100644 UnitsNet.Tests/UnitOfTests.cs create mode 100644 UnitsNet/UnitOf.cs create mode 100644 UnitsNet/UnitOfExtensions.cs diff --git a/README.md b/README.md index 0f643c0f84..d3e656d977 100644 --- a/README.md +++ b/README.md @@ -274,6 +274,17 @@ Units.NET allows you to add your own units and quantities at runtime, to represe Read more at [Extending-with-Custom-Units](https://github.com/angularsen/UnitsNet/wiki/Extending-with-Custom-Units). +#### Convert to and from your own unit of a quantity + +To convert a quantity to or from a unit that Units.NET doesn't define, describe the unit with a `UnitOf` and its value in the base unit of the quantity. No unit enum value or setup is needed. + +```c# +var furlong = new UnitOf("Furlong", "Furlongs", 201.168); // 1 furlong = 201.168 m + +double furlongs = Length.FromMiles(1).As(furlong); // 8 +Length length = Length.Info.From(2, furlong); // 402.336 m, in the base unit +``` + #### Map between unit enum values and unit abbreviations ```c# // Map unit enum values to unit abbreviations diff --git a/UnitsNet.Tests/UnitOfTests.cs b/UnitsNet.Tests/UnitOfTests.cs new file mode 100644 index 0000000000..8be1edebfc --- /dev/null +++ b/UnitsNet.Tests/UnitOfTests.cs @@ -0,0 +1,99 @@ +// Licensed under MIT No Attribution, see LICENSE file at the root. +// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. + +using System; +using UnitsNet.Units; +using Xunit; + +namespace UnitsNet.Tests +{ + public class UnitOfTests + { + // 1 furlong = 201.168 m + private static readonly UnitOf Furlong = new("Furlong", "Furlongs", 201.168); + + [Fact] + public void Constructor_SetsNames() + { + Assert.Equal("Furlong", Furlong.Name); + Assert.Equal("Furlongs", Furlong.PluralName); + } + + [Fact] + public void Constructor_WithNullArguments_ThrowsArgumentNullException() + { + Assert.Throws(() => new UnitOf(null!, "Furlongs", 1)); + Assert.Throws(() => new UnitOf("Furlong", null!, 1)); + Assert.Throws(() => new UnitOf("Furlong", "Furlongs", (BaseUnits)null!, 1)); + } + + [Fact] + public void BaseUnits_WhenNotGiven_IsUndefined() + { + Assert.Equal(BaseUnits.Undefined, Furlong.BaseUnits); + } + + [Fact] + public void Constructor_WithBaseUnits_KeepsThem() + { + var baseUnits = new BaseUnits(length: LengthUnit.Foot, time: DurationUnit.Day); + var footPerDay = new UnitOf("FootPerDay", "FeetPerDay", baseUnits, 0.3048 / 86400); + + Assert.Equal(baseUnits, footPerDay.BaseUnits); + Assert.Equal(86400, Speed.FromFeetPerSecond(1).As(footPerDay), 9); + } + + [Fact] + public void ToString_ReturnsName() + { + Assert.Equal("Furlong", Furlong.ToString()); + } + + [Fact] + public void As_ReturnsValueInThatUnit() + { + Assert.Equal(1, Length.FromFeet(660).As(Furlong), 12); + Assert.Equal(8, Length.FromMiles(1).As(Furlong), 12); + } + + [Fact] + public void As_FromOtherUnitOfTheQuantity_ReturnsValueInThatUnit() + { + Assert.Equal(1, Length.FromKilometers(0.201168).As(Furlong), 12); + } + + [Fact] + public void As_NullUnit_ThrowsArgumentNullException() + { + Assert.Throws(() => Length.FromMeters(1).As((UnitOf)null!)); + } + + [Fact] + public void From_ReturnsQuantityInBaseUnit() + { + Length length = Length.Info.From(2, Furlong); + + Assert.Equal(LengthUnit.Meter, length.Unit); + Assert.Equal(402.336, length.Value, 12); + } + + [Fact] + public void From_RoundTripsWithAs() + { + Assert.Equal(42, Length.Info.From(42, Furlong).As(Furlong), 12); + } + + [Fact] + public void From_InfoOfAnotherQuantity_ThrowsArgumentException() + { + Assert.Throws(() => Mass.Info.From(1, Furlong)); + } + + [Fact] + public void From_NullArguments_ThrowsArgumentNullException() + { + Assert.Throws(() => Length.Info.From(1, (UnitOf)null!)); + Assert.Throws(() => ((QuantityInfo)null!).From(1, Furlong)); + } + } +} diff --git a/UnitsNet/UnitOf.cs b/UnitsNet/UnitOf.cs new file mode 100644 index 0000000000..97977a1451 --- /dev/null +++ b/UnitsNet/UnitOf.cs @@ -0,0 +1,90 @@ +// Licensed under MIT No Attribution, see LICENSE file at the root. +// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. + +using System; +using System.Diagnostics; + +namespace UnitsNet +{ + /// + /// A unit of , described by its names and its value in the base unit of the + /// quantity, such as a furlong of . + /// + /// + /// Quantities can be converted to and from the unit with + /// and + /// . + /// Since the unit is tied to its quantity, using it with another quantity doesn't compile. + /// + /// The quantity this is a unit of, such as . + [DebuggerDisplay("{Name}")] + public sealed class UnitOf + where TQuantity : IQuantity + { + /// + /// Initializes a new instance of the class. + /// + /// The singular name of the unit, such as "Furlong". + /// The plural name of the unit, such as "Furlongs". + /// + /// The value of one of this unit in the base unit of the quantity, such as 201.168 for a furlong, which is + /// 201.168 meters. + /// + /// + /// Thrown when or is null. + /// + public UnitOf(string singularName, string pluralName, QuantityValue valueInBaseUnit) + : this(singularName, pluralName, BaseUnits.Undefined, valueInBaseUnit) + { + } + + /// + /// Initializes a new instance of the class for a unit made of the base units of + /// UnitsNet, such as feet per day of . + /// + /// The singular name of the unit, such as "FootPerDay". + /// The plural name of the unit, such as "FeetPerDay". + /// The base units the unit is made of, such as feet and days. + /// The value of one of this unit in the base unit of the quantity. + /// + /// Thrown when , , or + /// is null. + /// + public UnitOf(string singularName, string pluralName, BaseUnits baseUnits, QuantityValue valueInBaseUnit) + { + Name = singularName ?? throw new ArgumentNullException(nameof(singularName)); + PluralName = pluralName ?? throw new ArgumentNullException(nameof(pluralName)); + BaseUnits = baseUnits ?? throw new ArgumentNullException(nameof(baseUnits)); + ValueInBaseUnit = (double)valueInBaseUnit; + } + + /// + /// The singular name of the unit, such as "Furlong". + /// + public string Name { get; } + + /// + /// The plural name of the unit, such as "Furlongs". + /// + public string PluralName { get; } + + /// + /// The base units the unit is made of, or if it isn't made of the + /// base units of UnitsNet, such as a furlong. + /// + public BaseUnits BaseUnits { get; } + + /// + /// The value of one of this unit in the base unit of the quantity. + /// + internal double ValueInBaseUnit { get; } + + /// + /// Returns the name of the unit. + /// + public override string ToString() + { + return Name; + } + } +} diff --git a/UnitsNet/UnitOfExtensions.cs b/UnitsNet/UnitOfExtensions.cs new file mode 100644 index 0000000000..902f169e72 --- /dev/null +++ b/UnitsNet/UnitOfExtensions.cs @@ -0,0 +1,58 @@ +// Licensed under MIT No Attribution, see LICENSE file at the root. +// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet. + +using System; + +namespace UnitsNet +{ + /// + /// Converts quantities to and from a . + /// + public static class UnitOfExtensions + { + /// + /// Gets the value of the quantity in a , such as a furlong of + /// . + /// + /// The quantity to convert. + /// The unit to get the value in. + /// The value in . + /// is null. + public static double As(this TQuantity quantity, UnitOf unit) + where TQuantity : IQuantity + { + if (unit is null) throw new ArgumentNullException(nameof(unit)); + return quantity.As(quantity.QuantityInfo.BaseUnitInfo.Value) / unit.ValueInBaseUnit; + } + + /// + /// Creates a quantity from a value in a , such as a furlong of + /// . + /// + /// The information of the quantity, such as . + /// The numerical value in . + /// The unit of the value. + /// + /// The quantity in its base unit, since a quantity can only be in one of its own units. + /// + /// + /// or is null. + /// + /// + /// is not the information of . + /// + public static TQuantity From(this QuantityInfo quantityInfo, QuantityValue value, UnitOf unit) + where TQuantity : IQuantity + { + if (quantityInfo is null) throw new ArgumentNullException(nameof(quantityInfo)); + if (unit is null) throw new ArgumentNullException(nameof(unit)); + if (quantityInfo.ValueType != typeof(TQuantity)) + { + throw new ArgumentException( + $"A unit of {typeof(TQuantity).Name} can't be used with {quantityInfo.Name}.", nameof(quantityInfo)); + } + + return (TQuantity)Quantity.From((double)value * unit.ValueInBaseUnit, quantityInfo.BaseUnitInfo.Value); + } + } +}