diff --git a/UnitsNet.Tests/CustomCode/QuantityInfos/Builder/QuantitiesSelectorTests.cs b/UnitsNet.Tests/CustomCode/QuantityInfos/Builder/QuantitiesSelectorTests.cs new file mode 100644 index 0000000000..78fc005fcd --- /dev/null +++ b/UnitsNet.Tests/CustomCode/QuantityInfos/Builder/QuantitiesSelectorTests.cs @@ -0,0 +1,33 @@ +// 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. + +namespace UnitsNet.Tests.QuantityInfos.Builder; + +public class QuantitiesSelectorTests +{ + [Fact] + public void GetQuantityInfos_WithoutAdditions_ReturnsBaseSelection() + { + var selector = new QuantitiesSelector(() => [Length.Info]); + + Assert.Equal([Length.Info], selector.GetQuantityInfos()); + } + + [Fact] + public void WithAdditionalQuantities_AppendsAllSelectionsInOrder() + { + QuantitiesSelector selector = new QuantitiesSelector(() => [Length.Info]) + .WithAdditionalQuantities([Mass.Info]) + .WithAdditionalQuantities([Duration.Info]); + + Assert.Equal([Length.Info, Mass.Info, Duration.Info], selector.GetQuantityInfos()); + } + + [Fact] + public void WithAdditionalQuantities_WithNull_ThrowsArgumentNullException() + { + var selector = new QuantitiesSelector(() => [Length.Info]); + + Assert.Throws(() => selector.WithAdditionalQuantities(null!)); + } +} diff --git a/UnitsNet.Tests/UnitAbbreviationsCacheTests.cs b/UnitsNet.Tests/UnitAbbreviationsCacheTests.cs index 9b45d8b990..1e68f6c185 100644 --- a/UnitsNet.Tests/UnitAbbreviationsCacheTests.cs +++ b/UnitsNet.Tests/UnitAbbreviationsCacheTests.cs @@ -49,6 +49,40 @@ public void CreateDefault_ReturnsAnAbbreviationCacheWithDefaultQuantityInfoLooku Assert.Throws(() => unitAbbreviationCache.GetUnitAbbreviations(HowMuchUnit.Some)); } + [Fact] + public void CreateDefault_WithAdditionalQuantity_ReturnsCacheWithExtendedQuantityInfoLookup() + { + var unitAbbreviationCache = UnitAbbreviationsCache.CreateDefault(selector => selector.WithAdditionalQuantities([HowMuch.Info])); + + Assert.NotEqual(UnitsNetSetup.Default.Quantities, unitAbbreviationCache.Quantities); + Assert.Equal("g", unitAbbreviationCache.GetUnitAbbreviations(MassUnit.Gram, AmericanCulture)[0]); + Assert.Empty(unitAbbreviationCache.GetUnitAbbreviations(HowMuchUnit.Some, AmericanCulture)); + } + + [Fact] + public void Create_WithBaseAndAdditionalQuantities_ReturnsConfiguredCache() + { + var unitAbbreviationCache = UnitAbbreviationsCache.Create([Mass.Info], + selector => selector.WithAdditionalQuantities([HowMuch.Info])); + + Assert.Equal([Mass.Info, HowMuch.Info], unitAbbreviationCache.Quantities.Infos); + Assert.Throws(() => unitAbbreviationCache.GetUnitAbbreviations(LengthUnit.Meter)); + } + + [Fact] + public void FactoryMethods_WithNullArguments_ThrowArgumentNullException() + { + Assert.Multiple(checks: + [ + () => Assert.Equal("configureQuantities", + Assert.Throws(() => UnitAbbreviationsCache.CreateDefault(null!)).ParamName), + () => Assert.Equal("defaultQuantities", + Assert.Throws(() => UnitAbbreviationsCache.Create(null!, _ => { })).ParamName), + () => Assert.Equal("configureQuantities", + Assert.Throws(() => UnitAbbreviationsCache.Create([Mass.Info], null!)).ParamName) + ]); + } + [Fact] public void UnitAbbreviationsCache_Default_ReturnsInstanceFromUnitsNetSetup() { diff --git a/UnitsNet.Tests/UnitParserTests.cs b/UnitsNet.Tests/UnitParserTests.cs index e168f08aeb..0c9c06a158 100644 --- a/UnitsNet.Tests/UnitParserTests.cs +++ b/UnitsNet.Tests/UnitParserTests.cs @@ -35,6 +35,60 @@ public void CreateDefault_CreatesNewAbbreviationsCacheWithDefaultQuantities() Assert.NotEqual(UnitAbbreviationsCache.Default, unitParser.Abbreviations); Assert.Equal(UnitsNetSetup.Default.Quantities, unitParser.Quantities); } + + [Fact] + public void CreateDefault_WithAdditionalQuantity_CreatesParserWithExtendedCatalog() + { + var unitParser = UnitParser.CreateDefault(selector => selector.WithAdditionalQuantities([HowMuch.Info])); + + Assert.Contains(HowMuch.Info, unitParser.Quantities.Infos); + } + + [Fact] + public void CreateDefault_WithConfiguredQuantityAndAbbreviation_ParsesExternalQuantityUnit() + { + var unitParser = UnitParser.CreateDefault( + selector => selector.WithAdditionalQuantities([HowMuch.Info]), + abbreviations => abbreviations.MapUnitToAbbreviation(HowMuchUnit.Some, "some")); + + Assert.Equal(HowMuchUnit.Some, unitParser.Parse("some")); + } + + [Fact] + public void Create_WithBaseAndAdditionalQuantities_CreatesParserWithSelectedCatalog() + { + var unitParser = UnitParser.Create([Mass.Info], + selector => selector.WithAdditionalQuantities([HowMuch.Info])); + + Assert.Equal([Mass.Info, HowMuch.Info], unitParser.Quantities.Infos); + } + + [Fact] + public void Create_WithConfiguredAbbreviation_ParsesExternalQuantityUnit() + { + var unitParser = UnitParser.Create([HowMuch.Info], _ => { }, + abbreviations => abbreviations.MapUnitToAbbreviation(HowMuchUnit.Some, "some")); + + Assert.Equal(HowMuchUnit.Some, unitParser.Parse("some")); + } + + [Fact] + public void FactoryMethods_WithNullArguments_ThrowArgumentNullException() + { + Assert.Multiple(checks: + [ + () => Assert.Equal("configureQuantities", + Assert.Throws(() => UnitParser.CreateDefault((Action)null!)).ParamName), + () => Assert.Equal("configureAbbreviations", + Assert.Throws(() => UnitParser.CreateDefault(_ => { }, null!)).ParamName), + () => Assert.Equal("defaultQuantities", + Assert.Throws(() => UnitParser.Create(null!, _ => { })).ParamName), + () => Assert.Equal("configureQuantities", + Assert.Throws(() => UnitParser.Create([Mass.Info], null!)).ParamName), + () => Assert.Equal("configureAbbreviations", + Assert.Throws(() => UnitParser.Create([Mass.Info], _ => { }, null!)).ParamName) + ]); + } [Theory] [InlineData("m^^2", AreaUnit.SquareMeter)] diff --git a/UnitsNet.Tests/UnitsNetSetupTests.cs b/UnitsNet.Tests/UnitsNetSetupTests.cs new file mode 100644 index 0000000000..cc7364b019 --- /dev/null +++ b/UnitsNet.Tests/UnitsNetSetupTests.cs @@ -0,0 +1,69 @@ +// 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 UnitsNet.Tests.CustomQuantities; +using UnitsNet.Units; + +namespace UnitsNet.Tests; + +public class UnitsNetSetupTests +{ + [Fact] + public void Create_WithSelectedQuantities_WiresIsolatedComponentsToSameCatalog() + { + UnitsNetSetup setup = UnitsNetSetup.Create(builder => builder.WithQuantities([Mass.Info])); + + Assert.Equal([Mass.Info], setup.Quantities.Infos); + Assert.Same(setup.Quantities, setup.UnitAbbreviations.Quantities); + Assert.Same(setup.UnitAbbreviations, setup.UnitParser.Abbreviations); + Assert.NotSame(UnitsNetSetup.Default, setup); + Assert.Throws(() => setup.Quantities.GetQuantityByUnitType(typeof(LengthUnit))); + } + + [Fact] + public void Create_WithAdditionalQuantities_AppendsToSelectedCatalog() + { + UnitsNetSetup setup = UnitsNetSetup.Create(builder => builder + .WithQuantities([Mass.Info]) + .WithAdditionalQuantities([HowMuch.Info])); + + Assert.Equal([Mass.Info, HowMuch.Info], setup.Quantities.Infos); + } + + [Fact] + public void Create_WithSelectedAndAdditionalQuantities_AppendsToSelectedCatalog() + { + UnitsNetSetup setup = UnitsNetSetup.Create(builder => builder.WithQuantities([Mass.Info], + selector => selector.WithAdditionalQuantities([HowMuch.Info]))); + + Assert.Equal([Mass.Info, HowMuch.Info], setup.Quantities.Infos); + } + + [Fact] + public void FactoryMethods_WithNullArguments_ThrowArgumentNullException() + { + var builder = new UnitsNetSetup.DefaultConfigurationBuilder(); + + Assert.Multiple(checks: + [ + () => Assert.Equal("configuration", + Assert.Throws(() => UnitsNetSetup.Create(null!)).ParamName), + () => Assert.Equal("quantities", + Assert.Throws(() => builder.WithQuantities((IEnumerable)null!)).ParamName), + () => Assert.Equal("quantities", + Assert.Throws(() => builder.WithQuantities((Func>)null!)).ParamName), + () => Assert.Equal("quantities", + Assert.Throws(() => builder.WithQuantities((IEnumerable)null!, _ => { })).ParamName), + () => Assert.Equal("configureQuantities", + Assert.Throws(() => builder.WithQuantities(() => [Mass.Info], null!)).ParamName) + ]); + } + + [Fact] + public void Builder_WithQuantitiesTwice_ThrowsInvalidOperationException() + { + Assert.Throws(() => UnitsNetSetup.Create(builder => builder + .WithQuantities([Mass.Info]) + .WithQuantities([Length.Info]))); + } +} diff --git a/UnitsNet/CustomCode/QuantityInfo/Builder/QuantitiesSelector.cs b/UnitsNet/CustomCode/QuantityInfo/Builder/QuantitiesSelector.cs new file mode 100644 index 0000000000..d8332802c1 --- /dev/null +++ b/UnitsNet/CustomCode/QuantityInfo/Builder/QuantitiesSelector.cs @@ -0,0 +1,39 @@ +// 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.Linq; + +namespace UnitsNet; + +/// +/// Selects the quantities used to construct an isolated UnitsNet component or setup. +/// +public sealed class QuantitiesSelector +{ + private readonly Func> _defaultQuantities; + private IEnumerable? _additionalQuantities; + + internal QuantitiesSelector(Func> defaultQuantities) + { + _defaultQuantities = defaultQuantities ?? throw new ArgumentNullException(nameof(defaultQuantities)); + } + + /// + /// Appends external quantity definitions to the current selection. + /// + /// The quantity definitions to append. + /// This selector, for method chaining. + public QuantitiesSelector WithAdditionalQuantities(IEnumerable quantities) + { + if (quantities is null) throw new ArgumentNullException(nameof(quantities)); + + _additionalQuantities = _additionalQuantities?.Concat(quantities) ?? quantities; + return this; + } + + internal IEnumerable GetQuantityInfos() + { + IEnumerable quantities = _defaultQuantities(); + return _additionalQuantities is null ? quantities : quantities.Concat(_additionalQuantities); + } +} diff --git a/UnitsNet/CustomCode/UnitAbbreviationsCache.cs b/UnitsNet/CustomCode/UnitAbbreviationsCache.cs index af397e9d35..0a023f5d06 100644 --- a/UnitsNet/CustomCode/UnitAbbreviationsCache.cs +++ b/UnitsNet/CustomCode/UnitAbbreviationsCache.cs @@ -85,6 +85,27 @@ public static UnitAbbreviationsCache CreateDefault() return new UnitAbbreviationsCache(); } + /// + /// Creates a cache for the built-in quantities and any additions made by . + /// + /// Configures the selected quantities. + /// A cache mapped to the configured quantity selection. + public static UnitAbbreviationsCache CreateDefault(Action configureQuantities) + { + return Create(Quantity.DefaultProvider.Quantities, configureQuantities); + } + + /// + /// Creates a cache for a configured selection based on . + /// + /// The initial quantity definitions. + /// Configures the selected quantities. + /// A cache mapped to the configured quantity selection. + public static UnitAbbreviationsCache Create(IEnumerable defaultQuantities, Action configureQuantities) + { + return new UnitAbbreviationsCache(QuantityInfoLookup.Create(defaultQuantities, configureQuantities)); + } + #region MapUnitToAbbreviation overloads /// diff --git a/UnitsNet/CustomCode/UnitParser.cs b/UnitsNet/CustomCode/UnitParser.cs index b403d86782..21985ce6ff 100644 --- a/UnitsNet/CustomCode/UnitParser.cs +++ b/UnitsNet/CustomCode/UnitParser.cs @@ -82,6 +82,55 @@ public static UnitParser CreateDefault() return new UnitParser(UnitAbbreviationsCache.CreateDefault()); } + /// + /// Creates a parser for the built-in quantities and any additions made by . + /// + /// Configures the selected quantities. + /// A parser for the configured quantity selection. + public static UnitParser CreateDefault(Action configureQuantities) + { + return new UnitParser(UnitAbbreviationsCache.CreateDefault(configureQuantities)); + } + + /// + /// Creates a parser for the built-in quantities, with configurable quantities and abbreviations. + /// + /// Configures the selected quantities. + /// Configures abbreviations before constructing the parser. + /// A parser for the configured quantities and abbreviations. + public static UnitParser CreateDefault(Action configureQuantities, Action configureAbbreviations) + { + return Create(Quantity.DefaultProvider.Quantities, configureQuantities, configureAbbreviations); + } + + /// + /// Creates a parser for a configured selection based on . + /// + /// The initial quantity definitions. + /// Configures the selected quantities. + /// A parser for the configured quantity selection. + public static UnitParser Create(IEnumerable defaultQuantities, Action configureQuantities) + { + return new UnitParser(UnitAbbreviationsCache.Create(defaultQuantities, configureQuantities)); + } + + /// + /// Creates a parser for a base catalog, with configurable quantities and abbreviations. + /// + /// The initial quantity definitions. + /// Configures the selected quantities. + /// Configures abbreviations before constructing the parser. + /// A parser for the configured quantities and abbreviations. + public static UnitParser Create(IEnumerable defaultQuantities, Action configureQuantities, + Action configureAbbreviations) + { + if (configureAbbreviations is null) throw new ArgumentNullException(nameof(configureAbbreviations)); + + var unitAbbreviations = UnitAbbreviationsCache.Create(defaultQuantities, configureQuantities); + configureAbbreviations(unitAbbreviations); + return new UnitParser(unitAbbreviations); + } + /// /// Parses a unit abbreviation for a given unit enumeration type. /// Example: Parse<LengthUnit>("km") => LengthUnit.Kilometer diff --git a/UnitsNet/CustomCode/UnitsNetSetup.cs b/UnitsNet/CustomCode/UnitsNetSetup.cs index e524434b5d..4e20412cef 100644 --- a/UnitsNet/CustomCode/UnitsNetSetup.cs +++ b/UnitsNet/CustomCode/UnitsNetSetup.cs @@ -14,6 +14,84 @@ namespace UnitsNet; /// public sealed class UnitsNetSetup { + /// + /// Builds a UnitsNet setup by selecting built-in or external quantity definitions. + /// + public sealed class DefaultConfigurationBuilder + { + private QuantitiesSelector? _quantitiesSelector; + + /// + /// Uses the specified quantities as the setup's base catalog. + /// + /// The quantity definitions to use. + /// This builder, for method chaining. + public DefaultConfigurationBuilder WithQuantities(IEnumerable quantities) + { + if (quantities is null) throw new ArgumentNullException(nameof(quantities)); + return WithQuantities(() => quantities); + } + + /// + /// Uses quantities returned by as the setup's base catalog. + /// + /// Provides the quantity definitions to use. + /// This builder, for method chaining. + public DefaultConfigurationBuilder WithQuantities(Func> quantities) + { + if (quantities is null) throw new ArgumentNullException(nameof(quantities)); + if (_quantitiesSelector is not null) throw new InvalidOperationException("The base quantity selection is already configured."); + + _quantitiesSelector = new QuantitiesSelector(quantities); + return this; + } + + /// + /// Uses the specified quantities as the setup's base catalog and configures that selection. + /// + /// The quantity definitions to use. + /// Configures the selected quantities. + /// This builder, for method chaining. + public DefaultConfigurationBuilder WithQuantities(IEnumerable quantities, Action configureQuantities) + { + if (quantities is null) throw new ArgumentNullException(nameof(quantities)); + return WithQuantities(() => quantities, configureQuantities); + } + + /// + /// Uses quantities returned by as the setup's base catalog and configures that selection. + /// + /// Provides the quantity definitions to use. + /// Configures the selected quantities. + /// This builder, for method chaining. + public DefaultConfigurationBuilder WithQuantities(Func> quantities, Action configureQuantities) + { + if (configureQuantities is null) throw new ArgumentNullException(nameof(configureQuantities)); + + WithQuantities(quantities); + configureQuantities(_quantitiesSelector!); + return this; + } + + /// + /// Appends external quantity definitions to the current selection. + /// + /// The quantity definitions to append. + /// This builder, for method chaining. + public DefaultConfigurationBuilder WithAdditionalQuantities(IEnumerable quantities) + { + _quantitiesSelector ??= new QuantitiesSelector(() => Quantity.DefaultProvider.Quantities); + _quantitiesSelector.WithAdditionalQuantities(quantities); + return this; + } + + internal UnitsNetSetup Build() + { + IEnumerable quantities = _quantitiesSelector?.GetQuantityInfos() ?? Quantity.DefaultProvider.Quantities; + return new UnitsNetSetup(quantities, UnitConverter.CreateDefault()); + } + } + static UnitsNetSetup() { IReadOnlyCollection quantityInfos = Quantity.DefaultProvider.Quantities; @@ -24,6 +102,20 @@ static UnitsNetSetup() Default = new UnitsNetSetup(quantityInfos, unitConverter); } + /// + /// Creates an isolated setup without changing . + /// + /// Configures the quantities included in the setup. + /// A configured UnitsNet setup. + public static UnitsNetSetup Create(Action configuration) + { + if (configuration is null) throw new ArgumentNullException(nameof(configuration)); + + var builder = new DefaultConfigurationBuilder(); + configuration(builder); + return builder.Build(); + } + /// /// Create a new UnitsNet setup with the given quantities, their units and unit conversion functions between units. /// diff --git a/UnitsNet/QuantityInfoLookup.cs b/UnitsNet/QuantityInfoLookup.cs index 5aa7843f93..ce788a39e9 100644 --- a/UnitsNet/QuantityInfoLookup.cs +++ b/UnitsNet/QuantityInfoLookup.cs @@ -99,6 +99,16 @@ public QuantityInfoLookup(IEnumerable quantityInfos) /// public IReadOnlyList Infos => _quantities; + internal static QuantityInfoLookup Create(IEnumerable defaultQuantities, Action configureQuantities) + { + if (defaultQuantities is null) throw new ArgumentNullException(nameof(defaultQuantities)); + if (configureQuantities is null) throw new ArgumentNullException(nameof(configureQuantities)); + + var selector = new QuantitiesSelector(() => defaultQuantities); + configureQuantities(selector); + return new QuantityInfoLookup(selector.GetQuantityInfos()); + } + /// /// Retrieves the associated with the specified quantity type. ///