diff --git a/UnitsNet.Serialization.JsonNet/AbbreviatedUnitsConverter.cs b/UnitsNet.Serialization.JsonNet/AbbreviatedUnitsConverter.cs
index 86a785f0ae..ba06ab91d8 100644
--- a/UnitsNet.Serialization.JsonNet/AbbreviatedUnitsConverter.cs
+++ b/UnitsNet.Serialization.JsonNet/AbbreviatedUnitsConverter.cs
@@ -45,7 +45,7 @@ public AbbreviatedUnitsConverter()
///
/// The comparer used to compare the property/quantity names (e.g. StringComparer.OrdinalIgnoreCase)
public AbbreviatedUnitsConverter(IEqualityComparer comparer)
- : this(new Dictionary(Quantity.ByName, comparer), UnitAbbreviationsCache.Default, comparer)
+ : this(new Dictionary(Quantity.ByName, comparer), UnitsNetSetup.Default.UnitAbbreviations, comparer)
{
}
diff --git a/UnitsNet.Tests/AlphabeticalOrderer.cs b/UnitsNet.Tests/AlphabeticalOrderer.cs
new file mode 100644
index 0000000000..bb7754a431
--- /dev/null
+++ b/UnitsNet.Tests/AlphabeticalOrderer.cs
@@ -0,0 +1,29 @@
+// 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.Collections.Generic;
+using System.Linq;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace UnitsNet.Tests;
+
+///
+/// Useful for debugging tests where a particular order of tests is required.
+///
+///
+/// Add the attribute to your test class:
+///
+///
+///
+///
+public class AlphabeticalOrderer : ITestCaseOrderer
+{
+ public IEnumerable OrderTestCases(
+ IEnumerable testCases) where TTestCase : ITestCase =>
+ testCases.OrderBy(testCase => testCase.TestMethod.Method.Name);
+}
diff --git a/UnitsNet.Tests/BaseUnitsTests.cs b/UnitsNet.Tests/BaseUnitsTests.cs
index 3c5366395b..bf4d75f1cd 100644
--- a/UnitsNet.Tests/BaseUnitsTests.cs
+++ b/UnitsNet.Tests/BaseUnitsTests.cs
@@ -140,13 +140,14 @@ public void ToStringGivesExpectedResult()
AmountOfSubstanceUnit.Mole,
LuminousIntensityUnit.Candela);
- var m = UnitAbbreviationsCache.Default.GetDefaultAbbreviation(LengthUnit.Meter);
- var kg = UnitAbbreviationsCache.Default.GetDefaultAbbreviation(MassUnit.Kilogram);
- var s = UnitAbbreviationsCache.Default.GetDefaultAbbreviation(DurationUnit.Second);
- var A = UnitAbbreviationsCache.Default.GetDefaultAbbreviation(ElectricCurrentUnit.Ampere);
- var K = UnitAbbreviationsCache.Default.GetDefaultAbbreviation(TemperatureUnit.Kelvin);
- var mol = UnitAbbreviationsCache.Default.GetDefaultAbbreviation(AmountOfSubstanceUnit.Mole);
- var cd = UnitAbbreviationsCache.Default.GetDefaultAbbreviation(LuminousIntensityUnit.Candela);
+ UnitAbbreviationsCache cache = UnitsNetSetup.Default.UnitAbbreviations;
+ var m = cache.GetDefaultAbbreviation(LengthUnit.Meter);
+ var kg = cache.GetDefaultAbbreviation(MassUnit.Kilogram);
+ var s = cache.GetDefaultAbbreviation(DurationUnit.Second);
+ var A = cache.GetDefaultAbbreviation(ElectricCurrentUnit.Ampere);
+ var K = cache.GetDefaultAbbreviation(TemperatureUnit.Kelvin);
+ var mol = cache.GetDefaultAbbreviation(AmountOfSubstanceUnit.Mole);
+ var cd = cache.GetDefaultAbbreviation(LuminousIntensityUnit.Candela);
Assert.Equal($"[Length]: {m}, [Mass]: {kg}, [Time]: {s}, [Current]: {A}, [Temperature]: {K}, [Amount]: {mol}, [LuminousIntensity]: {cd}", siBaseUnits.ToString());
}
diff --git a/UnitsNet.Tests/QuantityIFormattableTests.cs b/UnitsNet.Tests/QuantityIFormattableTests.cs
index 52ca37e5b8..81237fabb1 100644
--- a/UnitsNet.Tests/QuantityIFormattableTests.cs
+++ b/UnitsNet.Tests/QuantityIFormattableTests.cs
@@ -27,11 +27,12 @@ public void EmptyOrNullFormatStringEqualsGFormat()
[Fact]
public void AFormatGetsAbbreviations()
{
- Assert.Equal(UnitAbbreviationsCache.Default.GetDefaultAbbreviation(MyLength.Unit, CultureInfo.InvariantCulture), MyLength.ToString("a", CultureInfo.InvariantCulture));
- Assert.Equal(UnitAbbreviationsCache.Default.GetDefaultAbbreviation(MyLength.Unit, CultureInfo.InvariantCulture), MyLength.ToString("a0", CultureInfo.InvariantCulture));
+ UnitAbbreviationsCache cache = UnitsNetSetup.Default.UnitAbbreviations;
+ Assert.Equal(cache.GetDefaultAbbreviation(MyLength.Unit, CultureInfo.InvariantCulture), MyLength.ToString("a", CultureInfo.InvariantCulture));
+ Assert.Equal(cache.GetDefaultAbbreviation(MyLength.Unit, CultureInfo.InvariantCulture), MyLength.ToString("a0", CultureInfo.InvariantCulture));
- Assert.Equal(UnitAbbreviationsCache.Default.GetUnitAbbreviations(MyLength.Unit, CultureInfo.InvariantCulture)[1], MyLength.ToString("a1", CultureInfo.InvariantCulture));
- Assert.Equal(UnitAbbreviationsCache.Default.GetUnitAbbreviations(MyLength.Unit, CultureInfo.InvariantCulture)[2], MyLength.ToString("a2", CultureInfo.InvariantCulture));
+ Assert.Equal(cache.GetUnitAbbreviations(MyLength.Unit, CultureInfo.InvariantCulture)[1], MyLength.ToString("a1", CultureInfo.InvariantCulture));
+ Assert.Equal(cache.GetUnitAbbreviations(MyLength.Unit, CultureInfo.InvariantCulture)[2], MyLength.ToString("a2", CultureInfo.InvariantCulture));
}
[Fact]
diff --git a/UnitsNet.Tests/UnitAbbreviationsCacheTests.cs b/UnitsNet.Tests/UnitAbbreviationsCacheTests.cs
index 7c4ab1b992..0f42960f58 100644
--- a/UnitsNet.Tests/UnitAbbreviationsCacheTests.cs
+++ b/UnitsNet.Tests/UnitAbbreviationsCacheTests.cs
@@ -229,6 +229,24 @@ public void GetDefaultAbbreviationFallsBackToUsEnglishCulture()
Assert.Equal("US english abbreviation for Unit1", abbreviation);
}
+ [Fact]
+ public void MapUnitToAbbreviation_DoesNotAffectOtherCacheInstances()
+ {
+ var culture = AmericanCulture;
+ var unit = AreaUnit.SquareMeter;
+
+ var cache1 = new UnitAbbreviationsCache();
+ cache1.MapUnitToAbbreviation(unit, culture, "m^2");
+
+ var cache2 = new UnitAbbreviationsCache();
+ cache2.MapUnitToAbbreviation(unit, culture, "m2");
+
+ Assert.Equal(new[] { "m²", "m^2" }, cache1.GetUnitAbbreviations(unit, culture));
+ Assert.Equal(new[] { "m²", "m2" }, cache2.GetUnitAbbreviations(unit, culture));
+ Assert.Equal("m²", cache1.GetDefaultAbbreviation(unit, culture));
+ Assert.Equal("m²", cache2.GetDefaultAbbreviation(unit, culture));
+ }
+
[Fact]
public void MapUnitToAbbreviation_AddCustomUnit_DoesNotOverrideDefaultAbbreviationForAlreadyMappedUnits()
{
diff --git a/UnitsNet/CustomCode/Quantity.cs b/UnitsNet/CustomCode/Quantity.cs
index ffbfc29071..15e8fab141 100644
--- a/UnitsNet/CustomCode/Quantity.cs
+++ b/UnitsNet/CustomCode/Quantity.cs
@@ -9,15 +9,7 @@ namespace UnitsNet
{
public partial class Quantity
{
- static Quantity()
- {
- Default = new QuantityInfoLookup();
- }
-
- private static QuantityInfoLookup Default
- {
- get;
- }
+ private static QuantityInfoLookup Default => UnitsNetSetup.Default.QuantityInfoLookup;
///
/// All enum value names of , such as "Length" and "Mass".
diff --git a/UnitsNet/CustomCode/QuantityParser.cs b/UnitsNet/CustomCode/QuantityParser.cs
index 52de88bff7..9e003359e5 100644
--- a/UnitsNet/CustomCode/QuantityParser.cs
+++ b/UnitsNet/CustomCode/QuantityParser.cs
@@ -37,7 +37,8 @@ public class QuantityParser
///
/// The default instance of , which uses unit abbreviations.
///
- public static QuantityParser Default { get; }
+ [Obsolete("Use UnitsNetSetup.Default.QuantityParser instead.")]
+ public static QuantityParser Default => UnitsNetSetup.Default.QuantityParser;
///
/// Creates an instance of , optionally specifying an
@@ -50,11 +51,6 @@ public QuantityParser(UnitAbbreviationsCache? unitAbbreviationsCache = null)
_unitParser = new UnitParser(_unitAbbreviationsCache);
}
- static QuantityParser()
- {
- Default = new QuantityParser(UnitAbbreviationsCache.Default);
- }
-
///
/// Parses a quantity from a string, such as "1.2 kg" to or "100 cm" to .
///
diff --git a/UnitsNet/CustomCode/UnitAbbreviationsCache.cs b/UnitsNet/CustomCode/UnitAbbreviationsCache.cs
index 3ad4d9c594..6d47e7f058 100644
--- a/UnitsNet/CustomCode/UnitAbbreviationsCache.cs
+++ b/UnitsNet/CustomCode/UnitAbbreviationsCache.cs
@@ -3,8 +3,10 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
+using System.Resources;
using UnitsNet.Units;
// ReSharper disable once CheckNamespace
@@ -30,23 +32,55 @@ public sealed class UnitAbbreviationsCache
///
/// The static instance used internally for ToString() and Parse() of quantities and units.
///
- public static UnitAbbreviationsCache Default { get; }
+ [Obsolete("Use UnitsNetSetup.Default.UnitAbbreviations instead.")]
+ public static UnitAbbreviationsCache Default => UnitsNetSetup.Default.UnitAbbreviations;
+
+ private readonly object _syncRoot = new();
private QuantityInfoLookup QuantityInfoLookup { get; }
+ ///
+ /// Culture name to abbreviations. To add a custom default abbreviation, add to the beginning of the list.
+ ///
+ private IDictionary> AbbreviationsMap { get; } = new Dictionary>();
+
///
/// Create an instance of the cache and load all the abbreviations defined in the library.
///
+ // TODO Change this to create an empty cache in v6: https://github.com/angularsen/UnitsNet/issues/1200
+ [Obsolete("Use CreateDefault() instead to create an instance that loads the built-in units. The default ctor will change to create an empty cache in UnitsNet v6.")]
public UnitAbbreviationsCache()
+ : this(new QuantityInfoLookup(Quantity.ByName.Values))
{
- QuantityInfoLookup= new QuantityInfoLookup();
}
- static UnitAbbreviationsCache()
+ ///
+ /// Creates an instance of the cache and load all the abbreviations defined in the library.
+ ///
+ ///
+ /// Access type is internal until this class is matured and ready for external use.
+ ///
+ internal UnitAbbreviationsCache(QuantityInfoLookup quantityInfoLookup)
{
- Default = new UnitAbbreviationsCache();
+ QuantityInfoLookup = quantityInfoLookup;
}
+ ///
+ /// Create an instance with empty cache.
+ ///
+ ///
+ /// Workaround until v6 changes the default ctor to create an empty cache.
+ ///
+ /// Instance with empty cache.
+ // TODO Remove in v6: https://github.com/angularsen/UnitsNet/issues/1200
+ public static UnitAbbreviationsCache CreateEmpty() => new(new QuantityInfoLookup(new List()));
+
+ ///
+ /// Create an instance of the cache and load all the built-in unit abbreviations defined in the library.
+ ///
+ /// Instance with default abbreviations cache.
+ public static UnitAbbreviationsCache CreateDefault() => new(new QuantityInfoLookup(Quantity.ByName.Values));
+
///
/// Adds one or more unit abbreviation for the given unit enum value.
/// This is used to dynamically add abbreviations for existing unit enums such as or to extend with third-party unit enums
@@ -57,7 +91,7 @@ static UnitAbbreviationsCache()
/// The type of unit enum.
public void MapUnitToAbbreviation(TUnitType unit, params string[] abbreviations) where TUnitType : Enum
{
- PerformAbbreviationMapping(unit, CultureInfo.CurrentCulture, false, true, abbreviations);
+ PerformAbbreviationMapping(unit, CultureInfo.CurrentCulture, false, abbreviations);
}
///
@@ -70,7 +104,7 @@ public void MapUnitToAbbreviation(TUnitType unit, params string[] abb
/// The type of unit enum.
public void MapUnitToDefaultAbbreviation(TUnitType unit, string abbreviation) where TUnitType : Enum
{
- PerformAbbreviationMapping(unit, CultureInfo.CurrentCulture, true, true, abbreviation);
+ PerformAbbreviationMapping(unit, CultureInfo.CurrentCulture, true, abbreviation);
}
///
@@ -84,7 +118,7 @@ public void MapUnitToDefaultAbbreviation(TUnitType unit, string abbre
/// The type of unit enum.
public void MapUnitToAbbreviation(TUnitType unit, IFormatProvider? formatProvider, params string[] abbreviations) where TUnitType : Enum
{
- PerformAbbreviationMapping(unit, formatProvider, false, true, abbreviations);
+ PerformAbbreviationMapping(unit, formatProvider, false, abbreviations);
}
///
@@ -98,7 +132,7 @@ public void MapUnitToAbbreviation(TUnitType unit, IFormatProvider? fo
/// The type of unit enum.
public void MapUnitToDefaultAbbreviation(TUnitType unit, IFormatProvider? formatProvider, string abbreviation) where TUnitType : Enum
{
- PerformAbbreviationMapping(unit, formatProvider, true, true, abbreviation);
+ PerformAbbreviationMapping(unit, formatProvider, true, abbreviation);
}
///
@@ -113,7 +147,7 @@ public void MapUnitToDefaultAbbreviation(TUnitType unit, IFormatProvi
public void MapUnitToAbbreviation(Type unitType, int unitValue, IFormatProvider? formatProvider, params string[] abbreviations)
{
var enumValue = (Enum)Enum.ToObject(unitType, unitValue);
- PerformAbbreviationMapping(enumValue, formatProvider, false, true, abbreviations);
+ PerformAbbreviationMapping(enumValue, formatProvider, false, abbreviations);
}
///
@@ -128,18 +162,18 @@ public void MapUnitToAbbreviation(Type unitType, int unitValue, IFormatProvider?
public void MapUnitToDefaultAbbreviation(Type unitType, int unitValue, IFormatProvider? formatProvider, string abbreviation)
{
var enumValue = (Enum)Enum.ToObject(unitType, unitValue);
- PerformAbbreviationMapping(enumValue, formatProvider, true, true, abbreviation);
+ PerformAbbreviationMapping(enumValue, formatProvider, true, abbreviation);
}
- internal void PerformAbbreviationMapping(Enum unitValue, IFormatProvider? formatProvider, bool setAsDefault, bool allowAbbreviationLookup, params string[] abbreviations)
+ private void PerformAbbreviationMapping(Enum unitValue, IFormatProvider? formatProvider, bool setAsDefault, params string[] abbreviations)
{
- if(!QuantityInfoLookup.TryGetUnitInfo(unitValue, out var unitInfo))
+ if(!QuantityInfoLookup.TryGetUnitInfo(unitValue, out UnitInfo? unitInfo))
{
unitInfo = new UnitInfo(unitValue, unitValue.ToString(), BaseUnits.Undefined);
QuantityInfoLookup.AddUnitInfo(unitValue, unitInfo);
}
- unitInfo.AddAbbreviation(formatProvider, setAsDefault, allowAbbreviationLookup, abbreviations);
+ AddAbbreviation(unitInfo, formatProvider, setAsDefault, abbreviations);
}
///
@@ -152,7 +186,7 @@ internal void PerformAbbreviationMapping(Enum unitValue, IFormatProvider? format
/// The default unit abbreviation string.
public string GetDefaultAbbreviation(TUnitType unit, IFormatProvider? formatProvider = null) where TUnitType : Enum
{
- var unitType = typeof(TUnitType);
+ Type unitType = typeof(TUnitType);
return GetDefaultAbbreviation(unitType, Convert.ToInt32(unit), formatProvider);
}
@@ -194,10 +228,9 @@ public string[] GetUnitAbbreviations(Type unitType, int unitValue, IFormatProvid
{
formatProvider ??= CultureInfo.CurrentCulture;
- if(TryGetUnitAbbreviations(unitType, unitValue, formatProvider, out var abbreviations))
- return abbreviations;
- else
- throw new NotImplementedException($"No abbreviation is specified for {unitType.Name} with numeric value {unitValue}.");
+ return TryGetUnitAbbreviations(unitType, unitValue, formatProvider, out var abbreviations)
+ ? abbreviations
+ : throw new NotImplementedException($"No abbreviation is specified for {unitType.Name} with numeric value {unitValue}.");
}
///
@@ -215,7 +248,7 @@ private bool TryGetUnitAbbreviations(Type unitType, int unitValue, IFormatProvid
if(QuantityInfoLookup.TryGetUnitInfo(enumInstance, out var unitInfo))
{
- abbreviations = unitInfo.GetAbbreviations(formatProvider!).ToArray();
+ abbreviations = GetAbbreviations(unitInfo, formatProvider!).ToArray();
return true;
}
else
@@ -256,5 +289,177 @@ public IReadOnlyList GetAllUnitAbbreviationsForQuantity(Type unitEnumTyp
return ret;
}
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public IReadOnlyList GetAbbreviations(UnitInfo unitInfo, IFormatProvider? formatProvider = null)
+ {
+ if (formatProvider is not CultureInfo)
+ formatProvider = CultureInfo.CurrentCulture;
+
+ var culture = (CultureInfo)formatProvider;
+ var cultureName = GetCultureNameOrEnglish(culture);
+
+ AbbreviationMapKey key = GetAbbreviationMapKey(unitInfo, cultureName);
+ if (!AbbreviationsMap.TryGetValue(key, out IReadOnlyList? abbreviations))
+ AbbreviationsMap[key] = abbreviations = ReadAbbreviationsFromResourceFile(unitInfo.QuantityName, unitInfo.PluralName, culture);
+
+ return abbreviations.Count == 0 && !culture.Equals(FallbackCulture)
+ ? GetAbbreviations(unitInfo, FallbackCulture)
+ : abbreviations;
+ }
+
+ ///
+ /// Add unit abbreviation for the given , such as "kg" for .
+ ///
+ /// The unit to add for.
+ /// The culture this abbreviation is for, defaults to .
+ /// Whether to set as the primary/default unit abbreviation used by ToString().
+ /// One or more abbreviations to add.
+ private void AddAbbreviation(UnitInfo unitInfo, IFormatProvider? formatProvider, bool setAsDefault,
+ params string[] abbreviations)
+ {
+ if (formatProvider is not CultureInfo)
+ formatProvider = CultureInfo.CurrentCulture;
+
+ var culture = (CultureInfo)formatProvider;
+ var cultureName = GetCultureNameOrEnglish(culture);
+
+ // Restrict concurrency on writes.
+ // By using ConcurrencyDictionary and immutable IReadOnlyList instances, we don't need to lock on reads.
+ lock(_syncRoot)
+ {
+ var currentAbbreviationsList = new List(GetAbbreviations(unitInfo, culture));
+
+ foreach (var abbreviation in abbreviations)
+ {
+ if (!currentAbbreviationsList.Contains(abbreviation))
+ {
+ if (setAsDefault)
+ currentAbbreviationsList.Insert(0, abbreviation);
+ else
+ currentAbbreviationsList.Add(abbreviation);
+ }
+ }
+
+ AbbreviationMapKey key = GetAbbreviationMapKey(unitInfo, cultureName);
+ AbbreviationsMap[key] = currentAbbreviationsList.AsReadOnly();
+ }
+ }
+
+ private static AbbreviationMapKey GetAbbreviationMapKey(UnitInfo unitInfo, string cultureName)
+ {
+ // TODO Enforce quantity name for custom units, optional value was required for backwards compatibility in v5.
+ // TODO Support non-enum units, using quantity name and unit name instead.
+ var unitTypeName = unitInfo.Value.GetType().FullName ?? throw new InvalidOperationException("Could not resolve unit enum type name."); // .QuantityName ?? "MissingQuantityName";
+
+ return new AbbreviationMapKey(
+ UnitTypeName: unitTypeName,
+ UnitName: unitInfo.Name,
+ CultureName: cultureName);
+ }
+
+ private static string GetCultureNameOrEnglish(CultureInfo culture)
+ {
+ // Fallback culture is invariant to support DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1,
+ // but we need to map that to the primary localization, English.
+ return culture.Equals(CultureInfo.InvariantCulture)
+ ? "en-US"
+ : culture.Name;
+ }
+
+ private IReadOnlyList ReadAbbreviationsFromResourceFile(string? quantityName, string unitPluralName, CultureInfo culture)
+ {
+ var abbreviationsList = new List();
+
+ if (quantityName is null) return abbreviationsList.AsReadOnly();
+
+ string resourceName = $"UnitsNet.GeneratedCode.Resources.{quantityName}";
+ var resourceManager = new ResourceManager(resourceName, GetType().Assembly);
+
+ var abbreviationsString = resourceManager.GetString(unitPluralName, culture);
+ if(abbreviationsString is not null)
+ abbreviationsList.AddRange(abbreviationsString.Split(','));
+
+ return abbreviationsList.AsReadOnly();
+ }
+
+#if NETCOREAPP
+ ///
+ /// Key for looking up unit abbreviations for a given unit and culture.
+ ///
+ ///
+ /// TODO Use quantity name instead of unit enum name, as part of moving from enums to string-based lookups.
+ ///
+ /// The unit enum type name, such as "UnitsNet.Units.LengthUnit" or "MyApp.HowMuchUnit".
+ /// The unit name, such as "Centimeter".
+ /// The culture name, such as "en-US".
+ [SuppressMessage("ReSharper", "NotAccessedPositionalProperty.Local", Justification = "Only used for hashing and equality.")]
+ private record AbbreviationMapKey(string UnitTypeName, string UnitName, string CultureName);
+#else
+ ///
+ /// Key for looking up unit abbreviations for a given unit and culture.
+ ///
+ ///
+ /// TODO Use quantity name instead of unit enum name, as part of moving from enums to string-based lookups.
+ ///
+ private class AbbreviationMapKey : IEquatable
+ {
+ ///
+ /// The unit enum type name, such as "UnitsNet.Units.LengthUnit" or "MyApp.HowMuchUnit".
+ ///
+ public string UnitTypeName { get; }
+
+ ///
+ /// The unit name, such as "Centimeter".
+ ///
+ public string UnitName { get; }
+
+ ///
+ /// The culture name, such as "en-US".
+ ///
+ public string CultureName { get; }
+
+ [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Matches record naming.")]
+ public AbbreviationMapKey(string UnitTypeName, string UnitName, string CultureName)
+ {
+ this.UnitTypeName = UnitTypeName;
+ this.UnitName = UnitName;
+ this.CultureName = CultureName;
+ }
+
+ public bool Equals(AbbreviationMapKey? other)
+ {
+ if (ReferenceEquals(null, other)) return false;
+ if (ReferenceEquals(this, other)) return true;
+ return UnitTypeName == other.UnitTypeName && UnitName == other.UnitName && CultureName == other.CultureName;
+ }
+
+ public override bool Equals(object? obj)
+ {
+ if (ReferenceEquals(null, obj)) return false;
+ if (ReferenceEquals(this, obj)) return true;
+ if (obj.GetType() != GetType()) return false;
+ return Equals((AbbreviationMapKey)obj);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ int hashCode = UnitTypeName.GetHashCode();
+ hashCode = (hashCode * 397) ^ UnitName.GetHashCode();
+ hashCode = (hashCode * 397) ^ CultureName.GetHashCode();
+ return hashCode;
+ }
+ }
+ }
+#endif
+
}
}
diff --git a/UnitsNet/CustomCode/UnitParser.cs b/UnitsNet/CustomCode/UnitParser.cs
index 142ea9273e..4cf036b72b 100644
--- a/UnitsNet/CustomCode/UnitParser.cs
+++ b/UnitsNet/CustomCode/UnitParser.cs
@@ -23,22 +23,19 @@ public sealed class UnitParser
/// The default static instance used internally to parse quantities and units using the
/// default abbreviations cache for all units and abbreviations defined in the library.
///
- public static UnitParser Default { get; }
+ [Obsolete("Use UnitsNetSetup.Default.UnitParser instead.")]
+ public static UnitParser Default => UnitsNetSetup.Default.UnitParser;
///
/// Create a parser using the given unit abbreviations cache.
///
///
+ // TODO Change this to not fallback to built-in units abbreviations when given null, in v6: https://github.com/angularsen/UnitsNet/issues/1200
public UnitParser(UnitAbbreviationsCache? unitAbbreviationsCache)
{
_unitAbbreviationsCache = unitAbbreviationsCache ?? UnitAbbreviationsCache.Default;
}
- static UnitParser()
- {
- Default = new UnitParser(UnitAbbreviationsCache.Default);
- }
-
///
/// 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
new file mode 100644
index 0000000000..e543e1085a
--- /dev/null
+++ b/UnitsNet/CustomCode/UnitsNetSetup.cs
@@ -0,0 +1,85 @@
+// 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.Collections.Generic;
+using UnitsNet.Units;
+
+namespace UnitsNet;
+
+///
+/// UnitsNet setup of quantities, units, unit abbreviations and conversion functions.
+///
+/// For normal use, is used and can be manipulated to add new units or change default unit
+/// abbreviations.
+/// Alternatively, a setup instance may be provided for most static methods, such as
+/// and
+/// .
+///
+public sealed class UnitsNetSetup
+{
+ static UnitsNetSetup()
+ {
+ var unitConverter = UnitConverter.CreateDefault();
+ ICollection quantityInfos = Quantity.ByName.Values;
+
+ Default = new UnitsNetSetup(quantityInfos, unitConverter);
+ }
+
+ ///
+ /// Create a new UnitsNet setup with the given quantities, their units and unit conversion functions between units.
+ ///
+ /// The quantities and their units to support for unit conversions, Parse() and ToString().
+ /// The unit converter instance.
+ public UnitsNetSetup(ICollection quantityInfos, UnitConverter unitConverter)
+ {
+ var quantityInfoLookup = new QuantityInfoLookup(quantityInfos);
+ var unitAbbreviations = new UnitAbbreviationsCache(quantityInfoLookup);
+
+ UnitConverter = unitConverter;
+ UnitAbbreviations = unitAbbreviations;
+ UnitParser = new UnitParser(unitAbbreviations);
+ QuantityParser = new QuantityParser(unitAbbreviations);
+ QuantityInfoLookup = quantityInfoLookup;
+ }
+
+ ///
+ /// The global default UnitsNet setup of quantities, units, unit abbreviations and conversion functions.
+ /// This setup is used by default in static Parse and ToString methods of quantities unless a setup instance is
+ /// provided.
+ ///
+ ///
+ /// Manipulating this instance, such as adding new units or changing default unit abbreviations, will affect most
+ /// usages of UnitsNet in the
+ /// current AppDomain since the typical use is via static members and not providing a setup instance.
+ ///
+ public static UnitsNetSetup Default { get; }
+
+ ///
+ /// Converts between units of a quantity, such as from meters to centimeters of a given length.
+ ///
+ public UnitConverter UnitConverter { get; }
+
+ ///
+ /// Maps unit enums to unit abbreviation strings for one or more cultures, used by ToString() and Parse() methods of
+ /// quantities.
+ ///
+ public UnitAbbreviationsCache UnitAbbreviations { get; }
+
+ ///
+ /// Parses units from strings, such as from "cm".
+ ///
+ public UnitParser UnitParser { get; }
+
+ ///
+ /// Parses quantities from strings, such as parsing from "1.2 kg".
+ ///
+ internal QuantityParser QuantityParser { get; }
+
+ ///
+ /// The quantities and units that are loaded.
+ ///
+ ///
+ /// Access type is internal until this class is matured and ready for external use.
+ ///
+ internal QuantityInfoLookup QuantityInfoLookup { get; }
+}
diff --git a/UnitsNet/QuantityInfoLookup.cs b/UnitsNet/QuantityInfoLookup.cs
index 0785497e3f..745d322239 100644
--- a/UnitsNet/QuantityInfoLookup.cs
+++ b/UnitsNet/QuantityInfoLookup.cs
@@ -9,24 +9,27 @@ namespace UnitsNet
///
/// A collection of .
///
- public class QuantityInfoLookup
+ ///
+ /// Access type is internal until this class is matured and ready for external use.
+ ///
+ internal class QuantityInfoLookup
{
- private readonly Lazy InfosLazy;
- private readonly Lazy> UnitTypeAndNameToUnitInfoLazy;
+ private readonly Lazy _infosLazy;
+ private readonly Lazy> _unitTypeAndNameToUnitInfoLazy;
///
/// New instance.
///
- public QuantityInfoLookup()
+ ///
+ public QuantityInfoLookup(ICollection quantityInfos)
{
- ICollection quantityInfos = Quantity.ByName.Values;
Names = quantityInfos.Select(qt => qt.Name).ToArray();
- InfosLazy = new Lazy(() => quantityInfos
+ _infosLazy = new Lazy(() => quantityInfos
.OrderBy(quantityInfo => quantityInfo.Name)
.ToArray());
- UnitTypeAndNameToUnitInfoLazy = new Lazy>(() =>
+ _unitTypeAndNameToUnitInfoLazy = new Lazy>(() =>
{
return Infos
.SelectMany(quantityInfo => quantityInfo.UnitInfos
@@ -45,18 +48,43 @@ public QuantityInfoLookup()
///
/// All quantity information objects, such as and .
///
- public QuantityInfo[] Infos => InfosLazy.Value;
+ public QuantityInfo[] Infos => _infosLazy.Value;
+
+ ///
+ /// Gets the for a given unit.
+ ///
+ public QuantityInfo GetQuantityInfo(UnitInfo unitInfo)
+ {
+ Type unitType = unitInfo.Value.GetType();
+ return _infosLazy.Value.First(i => i.UnitType == unitType);
+ }
+
+ ///
+ /// Try to get the for a given unit.
+ ///
+ public bool TryGetQuantityInfo(UnitInfo unitInfo, [NotNullWhen(true)] out QuantityInfo? quantityInfo)
+ {
+ Type unitType = unitInfo.Value.GetType();
+ if (_infosLazy.Value.FirstOrDefault(i => i.UnitType == unitType) is { } qi)
+ {
+ quantityInfo = qi;
+ return true;
+ }
+
+ quantityInfo = default;
+ return false;
+ }
///
/// Get for a given unit enum value.
///
- public UnitInfo GetUnitInfo(Enum unitEnum) => UnitTypeAndNameToUnitInfoLazy.Value[(unitEnum.GetType(), unitEnum.ToString())];
+ public UnitInfo GetUnitInfo(Enum unitEnum) => _unitTypeAndNameToUnitInfoLazy.Value[(unitEnum.GetType(), unitEnum.ToString())];
///
/// Try to get for a given unit enum value.
///
public bool TryGetUnitInfo(Enum unitEnum, [NotNullWhen(true)] out UnitInfo? unitInfo) =>
- UnitTypeAndNameToUnitInfoLazy.Value.TryGetValue((unitEnum.GetType(), unitEnum.ToString()), out unitInfo);
+ _unitTypeAndNameToUnitInfoLazy.Value.TryGetValue((unitEnum.GetType(), unitEnum.ToString()), out unitInfo);
///
///
@@ -65,7 +93,7 @@ public bool TryGetUnitInfo(Enum unitEnum, [NotNullWhen(true)] out UnitInfo? unit
///
public void AddUnitInfo(Enum unit, UnitInfo unitInfo)
{
- UnitTypeAndNameToUnitInfoLazy.Value.Add((unit.GetType(), unit.ToString()), unitInfo);
+ _unitTypeAndNameToUnitInfoLazy.Value.Add((unit.GetType(), unit.ToString()), unitInfo);
}
///
@@ -77,6 +105,7 @@ public void AddUnitInfo(Enum unit, UnitInfo unitInfo)
/// Unit value is not a know unit enum type.
public IQuantity From(QuantityValue value, Enum unit)
{
+ // TODO Support custom units, currently only hardcoded built-in quantities are supported.
return Quantity.TryFrom(value, unit, out IQuantity? quantity)
? quantity
: throw new UnitNotFoundException($"Unit value {unit} of type {unit.GetType()} is not a known unit enum type. Expected types like UnitsNet.Units.LengthUnit. Did you pass in a custom enum type defined outside the UnitsNet library?");
@@ -93,6 +122,7 @@ public bool TryFrom(double value, Enum unit, [NotNullWhen(true)] out IQuantity?
return false;
}
+ // TODO Support custom units, currently only hardcoded built-in quantities are supported.
return Quantity.TryFrom((QuantityValue)value, unit, out quantity);
}
@@ -112,6 +142,7 @@ public IQuantity Parse(IFormatProvider? formatProvider, Type quantityType, strin
if (!typeof(IQuantity).IsAssignableFrom(quantityType))
throw new ArgumentException($"Type {quantityType} must be of type UnitsNet.IQuantity.");
+ // TODO Support custom units, currently only hardcoded built-in quantities are supported.
if (Quantity.TryParse(formatProvider, quantityType, quantityString, out IQuantity? quantity))
return quantity;
@@ -119,8 +150,11 @@ public IQuantity Parse(IFormatProvider? formatProvider, Type quantityType, strin
}
///
- public bool TryParse(Type quantityType, string quantityString, [NotNullWhen(true)] out IQuantity? quantity) =>
- Quantity.TryParse(null, quantityType, quantityString, out quantity);
+ public bool TryParse(Type quantityType, string quantityString, [NotNullWhen(true)] out IQuantity? quantity)
+ {
+ // TODO Support custom units, currently only hardcoded built-in quantities are supported.
+ return Quantity.TryParse(null, quantityType, quantityString, out quantity);
+ }
///
/// Get a list of quantities that has the given base dimensions.
@@ -128,7 +162,7 @@ public bool TryParse(Type quantityType, string quantityString, [NotNullWhen(true
/// The base dimensions to match.
public IEnumerable GetQuantitiesWithBaseDimensions(BaseDimensions baseDimensions)
{
- return InfosLazy.Value.Where(info => info.BaseDimensions.Equals(baseDimensions));
+ return _infosLazy.Value.Where(info => info.BaseDimensions.Equals(baseDimensions));
}
}
}
diff --git a/UnitsNet/UnitConverter.cs b/UnitsNet/UnitConverter.cs
index d6bd7bc12a..af16bbe001 100644
--- a/UnitsNet/UnitConverter.cs
+++ b/UnitsNet/UnitConverter.cs
@@ -39,13 +39,8 @@ public sealed class UnitConverter
/// The static instance used by Units.NET to convert between units. Modify this to add/remove conversion functions at runtime, such
/// as adding your own third-party units and quantities to convert between.
///
- public static UnitConverter Default { get; }
-
- static UnitConverter()
- {
- Default = new UnitConverter();
- RegisterDefaultConversions(Default);
- }
+ [Obsolete("Use UnitsNetSetup.Default.UnitConverter instead.")]
+ public static UnitConverter Default => UnitsNetSetup.Default.UnitConverter;
///
/// Creates a new instance.
@@ -64,6 +59,18 @@ public UnitConverter(UnitConverter other)
ConversionFunctions = new ConcurrentDictionary(other.ConversionFunctions);
}
+ ///
+ /// Create an instance of the unit converter with all the built-in unit conversions defined in the library.
+ ///
+ /// The unit converter.
+ public static UnitConverter CreateDefault()
+ {
+ var unitConverter = new UnitConverter();
+ RegisterDefaultConversions(unitConverter);
+
+ return unitConverter;
+ }
+
private ConcurrentDictionary ConversionFunctions
{
get;
diff --git a/UnitsNet/UnitInfo.cs b/UnitsNet/UnitInfo.cs
index c174a6f1d5..9398462427 100644
--- a/UnitsNet/UnitInfo.cs
+++ b/UnitsNet/UnitInfo.cs
@@ -2,10 +2,6 @@
// Copyright 2013 Andreas Gullberg Larsen (andreas.larsen84@gmail.com). Maintained at https://github.com/angularsen/UnitsNet.
using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Resources;
using UnitsNet.Units;
namespace UnitsNet
@@ -21,8 +17,6 @@ namespace UnitsNet
///
public class UnitInfo
{
- private readonly object _syncRoot = new();
-
///
/// Creates an instance of the UnitInfo class.
///
@@ -35,8 +29,6 @@ public UnitInfo(Enum value, string pluralName, BaseUnits baseUnits)
Name = value.ToString();
PluralName = pluralName;
BaseUnits = baseUnits ?? throw new ArgumentNullException(nameof(baseUnits));
-
- AbbreviationsMap = new ConcurrentDictionary>>();
}
///
@@ -72,97 +64,10 @@ internal UnitInfo(Enum value, string pluralName, BaseUnits baseUnits, string qua
///
public BaseUnits BaseUnits { get; }
- private string? QuantityName { get; }
-
- ///
- /// Culture name to abbreviations. To add a custom default abbreviation, add to the beginning of the list.
- ///
- private IDictionary>> AbbreviationsMap { get; }
-
///
- ///
+ /// Name of the quantity this unit belongs to. May be null for custom units.
///
- ///
- ///
- ///
- public IReadOnlyList GetAbbreviations(IFormatProvider? formatProvider = null)
- {
- if(formatProvider is null || formatProvider is not CultureInfo)
- formatProvider = CultureInfo.CurrentCulture;
-
- var culture = (CultureInfo)formatProvider;
- var cultureName = GetCultureNameOrEnglish(culture);
-
- if(!AbbreviationsMap.TryGetValue(cultureName, out var abbreviations))
- AbbreviationsMap[cultureName] = abbreviations = new Lazy>(() => ReadAbbreviationsFromResourceFile(culture));
-
- if(abbreviations.Value.Count == 0 && !culture.Equals(UnitAbbreviationsCache.FallbackCulture))
- return GetAbbreviations(UnitAbbreviationsCache.FallbackCulture);
- else
- return abbreviations.Value;
- }
-
- ///
- ///
- ///
- ///
- ///
- ///
- ///
- public void AddAbbreviation(IFormatProvider? formatProvider, bool setAsDefault, bool allowAbbreviationLookup, params string[] abbreviations)
- {
- if(formatProvider is null || formatProvider is not CultureInfo)
- formatProvider = CultureInfo.CurrentCulture;
-
- var culture = (CultureInfo)formatProvider;
- var cultureName = GetCultureNameOrEnglish(culture);
-
- // Restrict concurrency on writes.
- // By using ConcurrencyDictionary and immutable IReadOnlyList instances, we don't need to lock on reads.
- lock(_syncRoot)
- {
- var currentAbbreviationsList = new List(GetAbbreviations(culture));
-
- foreach(var abbreviation in abbreviations)
- {
- if(!currentAbbreviationsList.Contains(abbreviation))
- {
- if(setAsDefault)
- currentAbbreviationsList.Insert(0, abbreviation);
- else
- currentAbbreviationsList.Add(abbreviation);
- }
- }
-
- AbbreviationsMap[cultureName] = new Lazy>(() => currentAbbreviationsList.AsReadOnly());
- }
- }
-
- private static string GetCultureNameOrEnglish(CultureInfo culture)
- {
- // Fallback culture is invariant to support DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1,
- // but we need to map that to the primary localization, English.
- return culture.Equals(CultureInfo.InvariantCulture)
- ? "en-US"
- : culture.Name;
- }
-
- private IReadOnlyList ReadAbbreviationsFromResourceFile(CultureInfo culture)
- {
- var abbreviationsList = new List();
-
- if(QuantityName is not null)
- {
- string resourceName = $"UnitsNet.GeneratedCode.Resources.{QuantityName}";
- var resourceManager = new ResourceManager(resourceName, GetType().Assembly);
-
- var abbreviationsString = resourceManager.GetString(PluralName, culture);
- if(abbreviationsString is not null)
- abbreviationsList.AddRange(abbreviationsString.Split(','));
- }
-
- return abbreviationsList.AsReadOnly();
- }
+ public string? QuantityName { get; }
}
///