From be726e9270e0014132fac10206b051ddce9b6c8f Mon Sep 17 00:00:00 2001 From: Tristan Milnthorp <5693174+tmilnthorp@users.noreply.github.com> Date: Tue, 6 Oct 2026 13:51:07 -0400 Subject: [PATCH 1/4] Convert quantities to and from a UnitOf UnitOf describes a unit of a quantity by its names, base units and conversions, 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. Since the unit is tied to its quantity, using it with another quantity doesn't compile. This makes it possible to use units that UnitsNet doesn't define, without a unit enum value or any setup. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012XKhDsyHDc5BHrmibxScqG --- Docs/extending-with-custom-units.md | 14 +++ UnitsNet.Tests/UnitOfTests.cs | 94 +++++++++++++++++++ .../CustomCode/QuantityInfo/Units/UnitOf.cs | 94 +++++++++++++++++++ UnitsNet/Extensions/QuantityExtensions.cs | 14 +++ UnitsNet/QuantityInfo.cs | 16 ++++ 5 files changed, 232 insertions(+) create mode 100644 UnitsNet.Tests/UnitOfTests.cs create mode 100644 UnitsNet/CustomCode/QuantityInfo/Units/UnitOf.cs diff --git a/Docs/extending-with-custom-units.md b/Docs/extending-with-custom-units.md index cfbba73989..73478a4d31 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 conversion from 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, so there are 1000/201168 furlongs in a meter +var furlong = new UnitOf("Furlong", "Furlongs", BaseUnits.Undefined, QuantityValue.FromTerms(1000, 201168)); + +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 both conversion expressions to the constructor. `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..5993d9d7f3 --- /dev/null +++ b/UnitsNet.Tests/UnitOfTests.cs @@ -0,0 +1,94 @@ +// 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, so there are 1000/201168 furlongs in a meter. + private static readonly UnitOf Furlong = new("Furlong", "Furlongs", BaseUnits.Undefined, QuantityValue.FromTerms(1000, 201168)); + + // Same conversions as TemperatureUnit.DegreeFahrenheit, to compare against. + private static readonly UnitOf Fahrenheit = new("Fahrenheit", "Fahrenheits", BaseUnits.Undefined, + new ConversionExpression(coefficient: QuantityValue.FromTerms(9, 5), constantTerm: QuantityValue.FromTerms(-45967, 100)), + new ConversionExpression(coefficient: QuantityValue.FromTerms(5, 9), constantTerm: QuantityValue.FromTerms(45967, 180))); + + [Fact] + public void Constructor_WithCoefficient_DerivesTheInverseConversion() + { + Assert.Equal("Furlong", Furlong.Name); + Assert.Equal("Furlongs", Furlong.PluralName); + Assert.Equal(BaseUnits.Undefined, Furlong.BaseUnits); + Assert.Equal(QuantityValue.FromTerms(1000, 201168), Furlong.ConversionFromBase.Evaluate(QuantityValue.One)); + Assert.Equal(QuantityValue.FromTerms(201168, 1000), Furlong.ConversionToBase.Evaluate(QuantityValue.One)); + } + + [Fact] + public void Constructor_WithNullArguments_ThrowsArgumentNullException() + { + Assert.Throws(() => new UnitOf(null!, "Furlongs", BaseUnits.Undefined, 1)); + Assert.Throws(() => new UnitOf("Furlong", null!, BaseUnits.Undefined, 1)); + Assert.Throws(() => new UnitOf("Furlong", "Furlongs", null!, 1)); + } + + [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(QuantityValue.FromTerms(402336, 1000), length.Value); + } + + [Fact] + public void From_AffineUnit_ReturnsQuantityInBaseUnit() + { + Temperature temperature = Temperature.Info.From(QuantityValue.FromTerms(986, 10), Fahrenheit); + + Assert.Equal(Temperature.BaseUnit, temperature.Unit); + Assert.Equal(Temperature.FromDegreesFahrenheit(QuantityValue.FromTerms(986, 10)).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..7b93da2094 --- /dev/null +++ b/UnitsNet/CustomCode/QuantityInfo/Units/UnitOf.cs @@ -0,0 +1,94 @@ +// 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, base units 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 associated with this unit. + /// + /// The conversion coefficient from the base unit of the quantity to this unit, such as 100 for centimeters of a + /// length. + /// + /// + /// Thrown when , , or is + /// null. + /// + public UnitOf(string singularName, string pluralName, BaseUnits baseUnits, QuantityValue conversionFromBase) + : this(singularName, pluralName, baseUnits, conversionFromBase, QuantityValue.Inverse(conversionFromBase)) + { + } + + /// + /// 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 associated with this unit. + /// 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; } + + /// + /// Gets the for this unit. + /// + public BaseUnits BaseUnits { get; } + + /// + public ConversionExpression ConversionFromBase { get; } + + /// + public ConversionExpression ConversionToBase { 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) { From ab571daafde85eedcc6024834c76daac556404fa Mon Sep 17 00:00:00 2001 From: Tristan Milnthorp <5693174+tmilnthorp@users.noreply.github.com> Date: Tue, 6 Oct 2026 14:10:39 -0400 Subject: [PATCH 2/4] Remove the base units parameter from UnitOf Base units are made of unit enum values, which a unit that UnitsNet doesn't define has none of, and nothing reads them yet. UnitOf still implements IUnitDefinition, with BaseUnits.Undefined as an explicit implementation. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012XKhDsyHDc5BHrmibxScqG --- Docs/extending-with-custom-units.md | 2 +- UnitsNet.Tests/UnitOfTests.cs | 18 +++++++---- .../CustomCode/QuantityInfo/Units/UnitOf.cs | 31 +++++++------------ 3 files changed, 25 insertions(+), 26 deletions(-) diff --git a/Docs/extending-with-custom-units.md b/Docs/extending-with-custom-units.md index 73478a4d31..bc5c9ac4f2 100644 --- a/Docs/extending-with-custom-units.md +++ b/Docs/extending-with-custom-units.md @@ -31,7 +31,7 @@ To convert a quantity to or from a unit that UnitsNet doesn't define, describe t ```c# // 1 furlong = 201.168 m, so there are 1000/201168 furlongs in a meter -var furlong = new UnitOf("Furlong", "Furlongs", BaseUnits.Undefined, QuantityValue.FromTerms(1000, 201168)); +var furlong = new UnitOf("Furlong", "Furlongs", QuantityValue.FromTerms(1000, 201168)); QuantityValue furlongs = Length.FromMiles(1).As(furlong); // 8 Length length = Length.Info.From(2, furlong); // 402.336 m, in the base unit diff --git a/UnitsNet.Tests/UnitOfTests.cs b/UnitsNet.Tests/UnitOfTests.cs index 5993d9d7f3..ca63f9e0f9 100644 --- a/UnitsNet.Tests/UnitOfTests.cs +++ b/UnitsNet.Tests/UnitOfTests.cs @@ -10,10 +10,10 @@ namespace UnitsNet.Tests; public class UnitOfTests { // 1 furlong = 201.168 m, so there are 1000/201168 furlongs in a meter. - private static readonly UnitOf Furlong = new("Furlong", "Furlongs", BaseUnits.Undefined, QuantityValue.FromTerms(1000, 201168)); + private static readonly UnitOf Furlong = new("Furlong", "Furlongs", QuantityValue.FromTerms(1000, 201168)); // Same conversions as TemperatureUnit.DegreeFahrenheit, to compare against. - private static readonly UnitOf Fahrenheit = new("Fahrenheit", "Fahrenheits", BaseUnits.Undefined, + private static readonly UnitOf Fahrenheit = new("Fahrenheit", "Fahrenheits", new ConversionExpression(coefficient: QuantityValue.FromTerms(9, 5), constantTerm: QuantityValue.FromTerms(-45967, 100)), new ConversionExpression(coefficient: QuantityValue.FromTerms(5, 9), constantTerm: QuantityValue.FromTerms(45967, 180))); @@ -22,7 +22,6 @@ public void Constructor_WithCoefficient_DerivesTheInverseConversion() { Assert.Equal("Furlong", Furlong.Name); Assert.Equal("Furlongs", Furlong.PluralName); - Assert.Equal(BaseUnits.Undefined, Furlong.BaseUnits); Assert.Equal(QuantityValue.FromTerms(1000, 201168), Furlong.ConversionFromBase.Evaluate(QuantityValue.One)); Assert.Equal(QuantityValue.FromTerms(201168, 1000), Furlong.ConversionToBase.Evaluate(QuantityValue.One)); } @@ -30,9 +29,16 @@ public void Constructor_WithCoefficient_DerivesTheInverseConversion() [Fact] public void Constructor_WithNullArguments_ThrowsArgumentNullException() { - Assert.Throws(() => new UnitOf(null!, "Furlongs", BaseUnits.Undefined, 1)); - Assert.Throws(() => new UnitOf("Furlong", null!, BaseUnits.Undefined, 1)); - Assert.Throws(() => new UnitOf("Furlong", "Furlongs", null!, 1)); + Assert.Throws(() => new UnitOf(null!, "Furlongs", 1)); + Assert.Throws(() => new UnitOf("Furlong", null!, 1)); + } + + [Fact] + public void BaseUnits_AsUnitDefinition_IsUndefined() + { + IUnitDefinition unitDefinition = Furlong; + + Assert.Equal(BaseUnits.Undefined, unitDefinition.BaseUnits); } [Fact] diff --git a/UnitsNet/CustomCode/QuantityInfo/Units/UnitOf.cs b/UnitsNet/CustomCode/QuantityInfo/Units/UnitOf.cs index 7b93da2094..0c2be97cef 100644 --- a/UnitsNet/CustomCode/QuantityInfo/Units/UnitOf.cs +++ b/UnitsNet/CustomCode/QuantityInfo/Units/UnitOf.cs @@ -7,8 +7,8 @@ namespace UnitsNet; /// -/// A unit of , described by its names, base units and conversions, such as a furlong -/// of . +/// A unit of , described by its names and conversions, such as a furlong of +/// . /// /// /// Quantities can be converted to and from the unit with @@ -26,17 +26,15 @@ public sealed class UnitOf : IUnitDefinition /// /// The singular name of the unit, such as "Furlong". /// The plural name of the unit, such as "Furlongs". - /// The associated with this unit. /// /// The conversion coefficient from the base unit of the quantity to this unit, such as 100 for centimeters of a /// length. /// /// - /// Thrown when , , or is - /// null. + /// Thrown when or is null. /// - public UnitOf(string singularName, string pluralName, BaseUnits baseUnits, QuantityValue conversionFromBase) - : this(singularName, pluralName, baseUnits, conversionFromBase, QuantityValue.Inverse(conversionFromBase)) + public UnitOf(string singularName, string pluralName, QuantityValue conversionFromBase) + : this(singularName, pluralName, conversionFromBase, QuantityValue.Inverse(conversionFromBase)) { } @@ -45,20 +43,15 @@ public UnitOf(string singularName, string pluralName, BaseUnits baseUnits, Quant /// /// The singular name of the unit, such as "Furlong". /// The plural name of the unit, such as "Furlongs". - /// The associated with this unit. /// 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. + /// Thrown when or is null. /// - public UnitOf(string singularName, string pluralName, BaseUnits baseUnits, - ConversionExpression conversionFromBase, - ConversionExpression conversionToBase) + public UnitOf(string singularName, string pluralName, 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; } @@ -73,17 +66,17 @@ public UnitOf(string singularName, string pluralName, BaseUnits baseUnits, /// public string PluralName { get; } - /// - /// Gets the for this unit. - /// - public BaseUnits BaseUnits { get; } - /// public ConversionExpression ConversionFromBase { get; } /// public ConversionExpression ConversionToBase { get; } + /// + /// Always , since base units are made of the units that UnitsNet defines. + /// + BaseUnits IUnitDefinition.BaseUnits => BaseUnits.Undefined; + /// /// Returns the name of the unit. /// From 18d67e90eb4fdffdf45e7e2edf316fc38de3edc4 Mon Sep 17 00:00:00 2001 From: Tristan Milnthorp <5693174+tmilnthorp@users.noreply.github.com> Date: Tue, 6 Oct 2026 14:14:06 -0400 Subject: [PATCH 3/4] Create a UnitOf from its value in the base unit The constructor took the conversion from the base unit, so a furlong needed the fraction 1000/201168. Taking the value of one unit in the base unit instead lets it be written as 201.168, as units are usually described. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012XKhDsyHDc5BHrmibxScqG --- Docs/extending-with-custom-units.md | 8 ++++---- UnitsNet.Tests/UnitOfTests.cs | 16 ++++++++-------- UnitsNet/CustomCode/QuantityInfo/Units/UnitOf.cs | 10 +++++----- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Docs/extending-with-custom-units.md b/Docs/extending-with-custom-units.md index bc5c9ac4f2..8e77cd625d 100644 --- a/Docs/extending-with-custom-units.md +++ b/Docs/extending-with-custom-units.md @@ -27,17 +27,17 @@ Conversion methods like `myLength.As()` and `myLength.ToUnit()` currently only s ### 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 conversion from the base unit of the quantity, and UnitsNet uses it directly, without a unit enum value or any setup. +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, so there are 1000/201168 furlongs in a meter -var furlong = new UnitOf("Furlong", "Furlongs", QuantityValue.FromTerms(1000, 201168)); +// 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 both conversion expressions to the constructor. `From` returns the quantity in its base unit, since a quantity can only be in one of its own units. +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? diff --git a/UnitsNet.Tests/UnitOfTests.cs b/UnitsNet.Tests/UnitOfTests.cs index ca63f9e0f9..5ed4c3ac04 100644 --- a/UnitsNet.Tests/UnitOfTests.cs +++ b/UnitsNet.Tests/UnitOfTests.cs @@ -9,21 +9,21 @@ namespace UnitsNet.Tests; public class UnitOfTests { - // 1 furlong = 201.168 m, so there are 1000/201168 furlongs in a meter. - private static readonly UnitOf Furlong = new("Furlong", "Furlongs", QuantityValue.FromTerms(1000, 201168)); + // 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: QuantityValue.FromTerms(9, 5), constantTerm: QuantityValue.FromTerms(-45967, 100)), + new ConversionExpression(coefficient: 1.8, constantTerm: -459.67), new ConversionExpression(coefficient: QuantityValue.FromTerms(5, 9), constantTerm: QuantityValue.FromTerms(45967, 180))); [Fact] - public void Constructor_WithCoefficient_DerivesTheInverseConversion() + 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)); - Assert.Equal(QuantityValue.FromTerms(201168, 1000), Furlong.ConversionToBase.Evaluate(QuantityValue.One)); } [Fact] @@ -74,16 +74,16 @@ public void From_ReturnsQuantityInBaseUnit() Length length = Length.Info.From(2, Furlong); Assert.Equal(LengthUnit.Meter, length.Unit); - Assert.Equal(QuantityValue.FromTerms(402336, 1000), length.Value); + Assert.Equal(402.336m, length.Value); } [Fact] public void From_AffineUnit_ReturnsQuantityInBaseUnit() { - Temperature temperature = Temperature.Info.From(QuantityValue.FromTerms(986, 10), Fahrenheit); + Temperature temperature = Temperature.Info.From(98.6, Fahrenheit); Assert.Equal(Temperature.BaseUnit, temperature.Unit); - Assert.Equal(Temperature.FromDegreesFahrenheit(QuantityValue.FromTerms(986, 10)).Kelvins, temperature.Value); + Assert.Equal(Temperature.FromDegreesFahrenheit(98.6).Kelvins, temperature.Value); } [Fact] diff --git a/UnitsNet/CustomCode/QuantityInfo/Units/UnitOf.cs b/UnitsNet/CustomCode/QuantityInfo/Units/UnitOf.cs index 0c2be97cef..c21ea277dc 100644 --- a/UnitsNet/CustomCode/QuantityInfo/Units/UnitOf.cs +++ b/UnitsNet/CustomCode/QuantityInfo/Units/UnitOf.cs @@ -26,15 +26,15 @@ public sealed class UnitOf : IUnitDefinition /// /// The singular name of the unit, such as "Furlong". /// The plural name of the unit, such as "Furlongs". - /// - /// The conversion coefficient from the base unit of the quantity to this unit, such as 100 for centimeters of a - /// length. + /// + /// 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 conversionFromBase) - : this(singularName, pluralName, conversionFromBase, QuantityValue.Inverse(conversionFromBase)) + public UnitOf(string singularName, string pluralName, QuantityValue valueInBaseUnit) + : this(singularName, pluralName, QuantityValue.Inverse(valueInBaseUnit), valueInBaseUnit) { } From f3eaa8fdd3d0724e54ed2e113b158aad8c89e397 Mon Sep 17 00:00:00 2001 From: Tristan Milnthorp <5693174+tmilnthorp@users.noreply.github.com> Date: Tue, 6 Oct 2026 14:17:45 -0400 Subject: [PATCH 4/4] Create a UnitOf from the base units it's made of A unit that UnitsNet doesn't define can still be made of the base units it does, such as feet per day of speed. New constructor overloads take its base units, which UnitOf now exposes; the others leave them undefined. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012XKhDsyHDc5BHrmibxScqG --- UnitsNet.Tests/UnitOfTests.cs | 16 +++++-- .../CustomCode/QuantityInfo/Units/UnitOf.cs | 44 +++++++++++++++++-- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/UnitsNet.Tests/UnitOfTests.cs b/UnitsNet.Tests/UnitOfTests.cs index 5ed4c3ac04..b2ff778136 100644 --- a/UnitsNet.Tests/UnitOfTests.cs +++ b/UnitsNet.Tests/UnitOfTests.cs @@ -31,14 +31,24 @@ 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_AsUnitDefinition_IsUndefined() + public void BaseUnits_WhenNotGiven_IsUndefined() { - IUnitDefinition unitDefinition = Furlong; + 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.Undefined, unitDefinition.BaseUnits); + Assert.Equal(baseUnits, footPerDay.BaseUnits); + Assert.Equal(baseUnits, ((IUnitDefinition)footPerDay).BaseUnits); + Assert.Equal(new QuantityValue(86400), Speed.FromFeetPerSecond(1).As(footPerDay)); } [Fact] diff --git a/UnitsNet/CustomCode/QuantityInfo/Units/UnitOf.cs b/UnitsNet/CustomCode/QuantityInfo/Units/UnitOf.cs index c21ea277dc..c2fc8536cb 100644 --- a/UnitsNet/CustomCode/QuantityInfo/Units/UnitOf.cs +++ b/UnitsNet/CustomCode/QuantityInfo/Units/UnitOf.cs @@ -34,7 +34,24 @@ public sealed class UnitOf : IUnitDefinition /// Thrown when or is null. /// public UnitOf(string singularName, string pluralName, QuantityValue valueInBaseUnit) - : this(singularName, pluralName, QuantityValue.Inverse(valueInBaseUnit), 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) { } @@ -49,9 +66,29 @@ public UnitOf(string singularName, string pluralName, QuantityValue valueInBaseU /// 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; } @@ -73,9 +110,10 @@ public UnitOf(string singularName, string pluralName, ConversionExpression conve public ConversionExpression ConversionToBase { get; } /// - /// Always , since base units are made of the units that UnitsNet defines. + /// The base units the unit is made of, or if it isn't made of the base + /// units of UnitsNet, such as a furlong. /// - BaseUnits IUnitDefinition.BaseUnits => BaseUnits.Undefined; + public BaseUnits BaseUnits { get; } /// /// Returns the name of the unit.