Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 14 additions & 23 deletions UnitsNet.Tests/UnitAbbreviationsCacheTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -213,29 +213,20 @@ public void GetDefaultAbbreviationThrowsNotImplementedExceptionIfNoneExist()
[Fact]
public void GetDefaultAbbreviationFallsBackToUsEnglishCulture()
{
var oldCurrentCulture = CultureInfo.CurrentCulture;

try
{
// CurrentCulture affects number formatting, such as comma or dot as decimal separator.
// CurrentCulture affects localization, in this case the abbreviation.
// Zulu (South Africa)
var zuluCulture = CultureInfo.GetCultureInfo("zu-ZA");
CultureInfo.CurrentCulture = zuluCulture;

var abbreviationsCache = new UnitAbbreviationsCache();
abbreviationsCache.MapUnitToAbbreviation(CustomUnit.Unit1, AmericanCulture, "US english abbreviation for Unit1");

// Act
string abbreviation = abbreviationsCache.GetDefaultAbbreviation(CustomUnit.Unit1, zuluCulture);

// Assert
Assert.Equal("US english abbreviation for Unit1", abbreviation);
}
finally
{
CultureInfo.CurrentCulture = oldCurrentCulture;
}
// CurrentCulture affects number formatting, such as comma or dot as decimal separator.
// CurrentCulture also affects localization of unit abbreviations.
// Zulu (South Africa)
var zuluCulture = CultureInfo.GetCultureInfo("zu-ZA");
// CultureInfo.CurrentCulture = zuluCulture;

var abbreviationsCache = new UnitAbbreviationsCache();
abbreviationsCache.MapUnitToAbbreviation(CustomUnit.Unit1, AmericanCulture, "US english abbreviation for Unit1");

// Act
string abbreviation = abbreviationsCache.GetDefaultAbbreviation(CustomUnit.Unit1, zuluCulture);

// Assert
Assert.Equal("US english abbreviation for Unit1", abbreviation);
}

[Fact]
Expand Down
2 changes: 1 addition & 1 deletion UnitsNet/CustomCode/UnitAbbreviationsCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public sealed class UnitAbbreviationsCache
/// culture, but no translation is defined, so we return the US English definition as a last resort. If it's not
/// defined there either, an exception is thrown.
/// </example>
internal static readonly CultureInfo FallbackCulture = CultureInfo.GetCultureInfo("en-US");
internal static readonly CultureInfo FallbackCulture = CultureInfo.InvariantCulture;

/// <summary>
/// The static instance used internally for ToString() and Parse() of quantities and units.
Expand Down
45 changes: 45 additions & 0 deletions UnitsNet/InternalHelpers/CultureHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// 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.Collections.Concurrent;
using System.Globalization;

namespace UnitsNet.InternalHelpers;

/// <summary>
/// Helper class for <see cref="CultureInfo"/> and related operations.
/// </summary>
internal static class CultureHelper
{
private static readonly ConcurrentDictionary<string, CultureInfo> CultureCache = new();

/// <summary>
/// Attempts to get the culture by name, with fallback to invariant culture if not found.<br/>
/// <br/>
/// This is particularly useful for Linux and Raspberry PI environments, where cultures may not always be installed.
/// To simulate the behavior, set environment variable DOTNET_SYSTEM_GLOBALIZATION_INVARIANT='1' when running the application.
/// </summary>
/// <param name="cultureName">The culture name.</param>
/// <returns><see cref="CultureInfo.CurrentCulture"/> if given <c>null</c>, or the culture with the given name if the culture is available, otherwise <see cref="CultureInfo.InvariantCulture"/>.</returns>
internal static CultureInfo GetCultureOrInvariant(string? cultureName)
{
if (cultureName is null) return CultureInfo.CurrentCulture;

try
{
// Use cache to avoid exception and diagnostic log events every time.
return CultureCache.GetOrAdd(cultureName, CultureInfo.GetCultureInfo);
}
catch (CultureNotFoundException)
{
Console.Error.WriteLine($"Failed to get culture '{cultureName}', falling back to invariant culture.");
System.Diagnostics.Debug.WriteLine($"Failed to get culture '{cultureName}', falling back to invariant culture.");

// Cache it, to avoid exception next time.
CultureCache.TryAdd(cultureName, CultureInfo.InvariantCulture);

return CultureInfo.InvariantCulture;
}
}
}
5 changes: 3 additions & 2 deletions UnitsNet/UnitConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Globalization;
using System.Reflection;
using System.Linq;
using UnitsNet.InternalHelpers;
using UnitsNet.Units;

namespace UnitsNet
Expand Down Expand Up @@ -420,7 +421,7 @@ public static double ConvertByAbbreviation(QuantityValue fromValue, string quant
if (!TryGetUnitType(quantityName, out Type? unitType))
throw new UnitNotFoundException($"The unit type for the given quantity was not found: {quantityName}");

var cultureInfo = string.IsNullOrWhiteSpace(culture) ? CultureInfo.CurrentCulture : CultureInfo.GetCultureInfo(culture);
var cultureInfo = CultureHelper.GetCultureOrInvariant(culture);

var fromUnit = UnitParser.Default.Parse(fromUnitAbbrev, unitType, cultureInfo); // ex: ("m", LengthUnit) => LengthUnit.Meter
var fromQuantity = Quantity.From(fromValue, fromUnit);
Expand Down Expand Up @@ -479,7 +480,7 @@ public static bool TryConvertByAbbreviation(QuantityValue fromValue, string quan
if (!TryGetUnitType(quantityName, out Type? unitType))
return false;

var cultureInfo = string.IsNullOrWhiteSpace(culture) ? CultureInfo.CurrentCulture : CultureInfo.GetCultureInfo(culture);
var cultureInfo = CultureHelper.GetCultureOrInvariant(culture);

if (!UnitParser.Default.TryParse(fromUnitAbbrev, unitType, cultureInfo, out Enum? fromUnit)) // ex: ("m", LengthUnit) => LengthUnit.Meter
return false;
Expand Down
27 changes: 19 additions & 8 deletions UnitsNet/UnitInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public UnitInfo(Enum value, string pluralName, BaseUnits baseUnits)
PluralName = pluralName;
BaseUnits = baseUnits ?? throw new ArgumentNullException(nameof(baseUnits));

AbbreviationsMap = new ConcurrentDictionary<CultureInfo, Lazy<IReadOnlyList<string>>>();
AbbreviationsMap = new ConcurrentDictionary<string, Lazy<IReadOnlyList<string>>>();
}

/// <summary>
Expand Down Expand Up @@ -75,12 +75,12 @@ internal UnitInfo(Enum value, string pluralName, BaseUnits baseUnits, string qua
private string? QuantityName { get; }

/// <summary>
/// The per-culture abbreviations. To add a custom default abbreviation, add to the beginning of the list.
/// Culture name to abbreviations. To add a custom default abbreviation, add to the beginning of the list.
/// </summary>
private IDictionary<CultureInfo, Lazy<IReadOnlyList<string>>> AbbreviationsMap { get; }
private IDictionary<string, Lazy<IReadOnlyList<string>>> AbbreviationsMap { get; }

/// <summary>
///
///
/// </summary>
/// <param name="formatProvider"></param>
/// <returns></returns>
Expand All @@ -91,9 +91,10 @@ public IReadOnlyList<string> GetAbbreviations(IFormatProvider? formatProvider =
formatProvider = CultureInfo.CurrentCulture;

var culture = (CultureInfo)formatProvider;
var cultureName = GetCultureNameOrEnglish(culture);

if(!AbbreviationsMap.TryGetValue(culture, out var abbreviations))
AbbreviationsMap[culture] = abbreviations = new Lazy<IReadOnlyList<string>>(() => ReadAbbreviationsFromResourceFile(culture));
if(!AbbreviationsMap.TryGetValue(cultureName, out var abbreviations))
AbbreviationsMap[cultureName] = abbreviations = new Lazy<IReadOnlyList<string>>(() => ReadAbbreviationsFromResourceFile(culture));

if(abbreviations.Value.Count == 0 && !culture.Equals(UnitAbbreviationsCache.FallbackCulture))
return GetAbbreviations(UnitAbbreviationsCache.FallbackCulture);
Expand All @@ -102,7 +103,7 @@ public IReadOnlyList<string> GetAbbreviations(IFormatProvider? formatProvider =
}

/// <summary>
///
///
/// </summary>
/// <param name="formatProvider"></param>
/// <param name="setAsDefault"></param>
Expand All @@ -114,6 +115,7 @@ public void AddAbbreviation(IFormatProvider? formatProvider, bool setAsDefault,
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.
Expand All @@ -132,10 +134,19 @@ public void AddAbbreviation(IFormatProvider? formatProvider, bool setAsDefault,
}
}

AbbreviationsMap[culture] = new Lazy<IReadOnlyList<string>>(() => currentAbbreviationsList.AsReadOnly());
AbbreviationsMap[cultureName] = new Lazy<IReadOnlyList<string>>(() => 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<string> ReadAbbreviationsFromResourceFile(CultureInfo culture)
{
var abbreviationsList = new List<string>();
Expand Down