diff --git a/Docs/extending-with-custom-units.md b/Docs/extending-with-custom-units.md index cfbba73989..8e77cd625d 100644 --- a/Docs/extending-with-custom-units.md +++ b/Docs/extending-with-custom-units.md @@ -25,6 +25,20 @@ To add new quantities or units to the `UnitsNet` nuget, please see [Adding a New You miss out on the statically generated code for members like `Length.FromMeters(1)` and `myLength.Meters`. Conversion methods like `myLength.As()` and `myLength.ToUnit()` currently only support their respective unit enums, in this case `LengthUnit`. +### Convert to and from your own unit + +To convert a quantity to or from a unit that UnitsNet doesn't define, describe the unit with a `UnitOf`. You give the value of one of the unit in the base unit of the quantity, and UnitsNet uses it directly, without a unit enum value or any setup. + +```c# +// 1 furlong = 201.168 m +var furlong = new UnitOf("Furlong", "Furlongs", 201.168); + +QuantityValue furlongs = Length.FromMiles(1).As(furlong); // 8 +Length length = Length.Info.From(2, furlong); // 402.336 m, in the base unit +``` + +The unit is tied to its quantity, so `Mass.FromKilograms(1).As(furlong)` doesn't compile. For units with an offset, such as temperatures, pass the conversion expressions from and to the base unit instead. `From` returns the quantity in its base unit, since a quantity can only be in one of its own units. + ### Can I add a custom unit to an existing quantity in UnitsNet? Currently, no. diff --git a/UnitsNet.Tests/UnitOfTests.cs b/UnitsNet.Tests/UnitOfTests.cs new file mode 100644 index 0000000000..b2ff778136 --- /dev/null +++ b/UnitsNet.Tests/UnitOfTests.cs @@ -0,0 +1,110 @@ +// 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); + + // Same conversions as TemperatureUnit.DegreeFahrenheit, to compare against. + private static readonly UnitOf Fahrenheit = new("Fahrenheit", "Fahrenheits", + new ConversionExpression(coefficient: 1.8, constantTerm: -459.67), + new ConversionExpression(coefficient: QuantityValue.FromTerms(5, 9), constantTerm: QuantityValue.FromTerms(45967, 180))); + + [Fact] + public void Constructor_WithValueInBaseUnit_DerivesTheConversionFromBase() + { + Assert.Equal("Furlong", Furlong.Name); + Assert.Equal("Furlongs", Furlong.PluralName); + Assert.Equal(201.168m, Furlong.ConversionToBase.Evaluate(QuantityValue.One)); + Assert.Equal(QuantityValue.FromTerms(1000, 201168), Furlong.ConversionFromBase.Evaluate(QuantityValue.One)); + } + + [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, (Length.FromFeet(1) / Duration.FromDays(1)).MetersPerSecond); + + Assert.Equal(baseUnits, footPerDay.BaseUnits); + Assert.Equal(baseUnits, ((IUnitDefinition)footPerDay).BaseUnits); + Assert.Equal(new QuantityValue(86400), Speed.FromFeetPerSecond(1).As(footPerDay)); + } + + [Fact] + public void ToString_ReturnsName() + { + Assert.Equal("Furlong", Furlong.ToString()); + } + + [Fact] + public void As_ReturnsValueInThatUnit() + { + Assert.Equal(QuantityValue.One, Length.FromFeet(660).As(Furlong)); + Assert.Equal(new QuantityValue(8), Length.FromMiles(1).As(Furlong)); + } + + [Fact] + public void As_AffineUnit_ReturnsValueInThatUnit() + { + Temperature temperature = Temperature.FromDegreesCelsius(37); + + Assert.Equal(temperature.DegreesFahrenheit, temperature.As(Fahrenheit)); + } + + [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.336m, length.Value); + } + + [Fact] + public void From_AffineUnit_ReturnsQuantityInBaseUnit() + { + Temperature temperature = Temperature.Info.From(98.6, Fahrenheit); + + Assert.Equal(Temperature.BaseUnit, temperature.Unit); + Assert.Equal(Temperature.FromDegreesFahrenheit(98.6).Kelvins, temperature.Value); + } + + [Fact] + public void From_RoundTripsWithAs() + { + Assert.Equal(new QuantityValue(42), Length.Info.From(42, Furlong).As(Furlong)); + } + + [Fact] + public void From_NullUnit_ThrowsArgumentNullException() + { + Assert.Throws(() => Length.Info.From(1, (UnitOf)null!)); + } +} diff --git a/UnitsNet/CustomCode/QuantityInfo/Units/UnitOf.cs b/UnitsNet/CustomCode/QuantityInfo/Units/UnitOf.cs new file mode 100644 index 0000000000..c2fc8536cb --- /dev/null +++ b/UnitsNet/CustomCode/QuantityInfo/Units/UnitOf.cs @@ -0,0 +1,125 @@ +// 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 conversions, 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 : IUnitDefinition + 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) + : this(singularName, pluralName, baseUnits, QuantityValue.Inverse(valueInBaseUnit), valueInBaseUnit) + { + } + + /// + /// 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 conversion expression from the base unit of the quantity to this unit. + /// The conversion expression from this unit to the base unit of the quantity. + /// + /// Thrown when or is null. + /// + public UnitOf(string singularName, string pluralName, ConversionExpression conversionFromBase, ConversionExpression conversionToBase) + : this(singularName, pluralName, BaseUnits.Undefined, conversionFromBase, conversionToBase) + { + } + + /// + /// Initializes a new instance of the class for a unit made of the base units of + /// UnitsNet. + /// + /// The singular name of the unit. + /// The plural name of the unit. + /// The base units the unit is made of. + /// The conversion expression from the base unit of the quantity to this unit. + /// The conversion expression from this unit to the base unit of the quantity. + /// + /// Thrown when , , or is + /// null. + /// + public UnitOf(string singularName, string pluralName, BaseUnits baseUnits, + ConversionExpression conversionFromBase, ConversionExpression conversionToBase) + { + Name = singularName ?? throw new ArgumentNullException(nameof(singularName)); + PluralName = pluralName ?? throw new ArgumentNullException(nameof(pluralName)); + BaseUnits = baseUnits ?? throw new ArgumentNullException(nameof(baseUnits)); + ConversionFromBase = conversionFromBase; + ConversionToBase = conversionToBase; + } + + /// + /// 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; } + + /// + public ConversionExpression ConversionFromBase { get; } + + /// + public ConversionExpression ConversionToBase { 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; } + + /// + /// Returns the name of the unit. + /// + public override string ToString() + { + return Name; + } +} diff --git a/UnitsNet/Extensions/QuantityExtensions.cs b/UnitsNet/Extensions/QuantityExtensions.cs index 0e8f40255f..5c0db75238 100644 --- a/UnitsNet/Extensions/QuantityExtensions.cs +++ b/UnitsNet/Extensions/QuantityExtensions.cs @@ -112,6 +112,20 @@ public static QuantityValue As(this TQuantity quantity, TUnit return UnitConverter.Default.ConvertValue(quantity, unit); } + /// + /// 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 QuantityValue As(this TQuantity quantity, UnitOf unit) + where TQuantity : IQuantity + { + if (unit is null) throw new ArgumentNullException(nameof(unit)); + return unit.ConversionFromBase.Evaluate(quantity.GetUnitInfo().ConvertValueToBaseUnit(quantity.Value)); + } + /// /// The quantity to convert. /// The target unit. diff --git a/UnitsNet/QuantityInfo.cs b/UnitsNet/QuantityInfo.cs index 25a337e04f..967dba9496 100644 --- a/UnitsNet/QuantityInfo.cs +++ b/UnitsNet/QuantityInfo.cs @@ -308,6 +308,22 @@ protected QuantityInfoBase(string name, TQuantity zero, BaseDimensions baseDimen return FromDelegate(value, unit); } + /// + /// Creates an instance of the quantity from a value in a , such as a furlong of + /// . + /// + /// The numerical value in . + /// The unit of the value. + /// + /// The quantity in its base unit, since a quantity can only be in a unit of . + /// + /// is null. + public TQuantity From(QuantityValue value, UnitOf unit) + { + if (unit is null) throw new ArgumentNullException(nameof(unit)); + return From(unit.ConversionToBase.Evaluate(value), BaseUnitInfo.Value); + } + /// TQuantity IQuantityInstanceInfo.Create(QuantityValue value, UnitKey unitKey) {