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); + } + } +}