From 67b5cc23e4721c78f50745bb9dec8f3a9866f0f7 Mon Sep 17 00:00:00 2001 From: lipchev Date: Mon, 8 Nov 2021 09:59:40 +0200 Subject: [PATCH 1/3] Implemented the AbbreviatedUnitsConverter .. with tests based on the SerializationTestsBase - no change to the DefaultDataContractJsonSerializerTests: only added Serializataion/Deserialization #regions --- .../AbbreviatedUnitsConverterTests.cs | 361 ++++++++++++++++++ .../JsonNetSerializationTestsBase.cs | 34 ++ .../AbbreviatedUnitsConverter.cs | 319 ++++++++++++++++ .../DefaultDataContractJsonSerializerTests.cs | 44 ++- 4 files changed, 740 insertions(+), 18 deletions(-) create mode 100644 UnitsNet.Serialization.JsonNet.Tests/AbbreviatedUnitsConverterTests.cs create mode 100644 UnitsNet.Serialization.JsonNet.Tests/JsonNetSerializationTestsBase.cs create mode 100644 UnitsNet.Serialization.JsonNet/AbbreviatedUnitsConverter.cs diff --git a/UnitsNet.Serialization.JsonNet.Tests/AbbreviatedUnitsConverterTests.cs b/UnitsNet.Serialization.JsonNet.Tests/AbbreviatedUnitsConverterTests.cs new file mode 100644 index 0000000000..03a4a71ad0 --- /dev/null +++ b/UnitsNet.Serialization.JsonNet.Tests/AbbreviatedUnitsConverterTests.cs @@ -0,0 +1,361 @@ +using System.Globalization; +using UnitsNet.Tests.Serialization; +using UnitsNet.Units; +using Xunit; + +namespace UnitsNet.Serialization.JsonNet.Tests +{ + public class AbbreviatedUnitsConverterTests : JsonNetSerializationTestsBase + { + public AbbreviatedUnitsConverterTests() : base(new AbbreviatedUnitsConverter()) + { + } + + #region Serialization tests + + [Fact] + public void DoubleQuantity_SerializedWithDoubleValueAndAbbreviatedUnit() + { + var quantity = new Mass(1.20, MassUnit.Milligram); + var expectedJson = "{\"Value\":1.2,\"Unit\":\"mg\",\"Type\":\"Mass\"}"; + + var json = SerializeObject(quantity); + + Assert.Equal(expectedJson, json); + } + + [Fact] + public void DecimalQuantity_SerializedWithDecimalValueValueAndAbbreviatedUnit() + { + var quantity = new Information(1.20m, InformationUnit.Exabyte); + var expectedJson = "{\"Value\":1.20,\"Unit\":\"EB\",\"Type\":\"Information\"}"; + + var json = SerializeObject(quantity); + + Assert.Equal(expectedJson, json); + } + + [Fact] + public void DoubleQuantity_InScientificNotation_SerializedWithExpandedValueAndAbbreviatedUnit() + { + var quantity = new Mass(1E+9, MassUnit.Milligram); + var expectedJson = "{\"Value\":1000000000.0,\"Unit\":\"mg\",\"Type\":\"Mass\"}"; + + var json = SerializeObject(quantity); + + Assert.Equal(expectedJson, json); + } + + [Fact] + public void DecimalQuantity_InScientificNotation_SerializedWithExpandedValueAndAbbreviatedUnit() + { + var quantity = new Information(1E+9m, InformationUnit.Exabyte); + var expectedJson = "{\"Value\":1000000000,\"Unit\":\"EB\",\"Type\":\"Information\"}"; + + var json = SerializeObject(quantity); + + Assert.Equal(expectedJson, json); + } + + [Fact] + public void InterfaceObject_IncludesTypeInformation() + { + var testObject = new TestInterfaceObject { Quantity = new Information(1.20m, InformationUnit.Exabyte) }; + var expectedJson = "{\"Quantity\":{\"Value\":1.20,\"Unit\":\"EB\",\"Type\":\"Information\"}}"; + + var json = SerializeObject(testObject); + + Assert.Equal(expectedJson, json); + } + + [Fact] + public void InterfaceObject_SerializesWithoutKnownTypeInformation() + { + var testObject = new TestInterfaceObject { Quantity = new Volume(1.2, VolumeUnit.Microliter) }; + + var expectedJson = "{\"Quantity\":{\"Value\":1.2,\"Unit\":\"µl\",\"Type\":\"Volume\"}}"; + + var json = SerializeObject(testObject); + + Assert.Equal(expectedJson, json); + } + + #endregion + + #region Deserialization tests + + [Fact] + public void DoubleIQuantity_DeserializedFromDoubleValueAndAbbreviatedUnit() + { + var json = "{\"Value\":1.2,\"Unit\":\"mg\",\"Type\":\"Mass\"}"; + + var quantity = DeserializeObject(json); + + Assert.Equal(1.2, quantity.Value); + Assert.Equal(MassUnit.Milligram, quantity.Unit); + } + + [Fact] + public void DoubleQuantity_DeserializedFromDoubleValueAndAbbreviatedUnit() + { + var json = "{\"Value\":1.2,\"Unit\":\"mg\"}"; + + var quantity = DeserializeObject(json); + + Assert.Equal(1.2, quantity.Value); + Assert.Equal(MassUnit.Milligram, quantity.Unit); + } + + [Fact] + public void DoubleIQuantity_DeserializedFromDoubleValueAndNonAmbiguousAbbreviatedUnit_WithoutQuantityType() + { + var json = "{\"Value\":1.2,\"Unit\":\"em\"}"; + + var quantity = DeserializeObject(json); + + Assert.Equal(1.2, quantity.Value); + Assert.Equal(MassUnit.EarthMass, quantity.Unit); + } + + [Fact] + public void AmbiguousUnitParseExceptionUnitsNetExceptionThrown_WhenDeserializing_WithoutQuantityType() + { + var json = "{\"Value\":1.2,\"Unit\":\"mg\"}"; + + Assert.Throws(() => DeserializeObject(json)); + } + + [Fact] + public void DoubleIQuantity_DeserializedFromDoubleValueAndAbbreviatedUnit_CaseInsensitive() + { + var json = "{\"value\":1.2,\"unit\":\"Mg\",\"type\":\"mass\"}"; + + var quantity = DeserializeObject(json); + + Assert.Equal(1.2, quantity.Value); + Assert.Equal(MassUnit.Milligram, quantity.Unit); + } + + [Fact] + public void UnitsNetExceptionThrown_WhenDeserializing_FromUnknownQuantityType() + { + var json = "{\"Value\":1.2,\"Unit\":\"mg\",\"Type\":\"invalid\"}"; + + Assert.Throws(() => DeserializeObject(json)); + } + + [Fact] + public void UnitsNotFoundExceptionThrown_WhenDeserializing_FromUnknownUnit() + { + var json = "{\"Value\":1.2,\"Unit\":\"invalid\",\"Type\":\"Mass\"}"; + + Assert.Throws(() => DeserializeObject(json)); + } + + [Fact] + public void DoubleIQuantity_DeserializedFromQuotedDoubleValueAndAbbreviatedUnit() + { + var json = "{\"Value\":\"1.2\",\"Unit\":\"mg\",\"Type\":\"Mass\"}"; + + var quantity = DeserializeObject(json); + + Assert.Equal(1.2, quantity.Value); + Assert.Equal(MassUnit.Milligram, quantity.Unit); + } + + [Fact] + public void DoubleQuantity_DeserializedFromQuotedDoubleValueAndAbbreviatedUnit() + { + var json = "{\"Value\":\"1.2\",\"Unit\":\"mg\"}"; + + var quantity = DeserializeObject(json); + + Assert.Equal(1.2, quantity.Value); + Assert.Equal(MassUnit.Milligram, quantity.Unit); + } + + [Fact] + public void DoubleZeroIQuantity_DeserializedFromAbbreviatedUnitAndNoValue() + { + var json = "{\"Unit\":\"mg\",\"Type\":\"Mass\"}"; + + var quantity = DeserializeObject(json); + + Assert.Equal(0, quantity.Value); + Assert.Equal(MassUnit.Milligram, quantity.Unit); + } + + [Fact] + public void DoubleZeroQuantity_DeserializedFromAbbreviatedUnitAndNoValue() + { + var json = "{\"Unit\":\"mg\"}"; + + var quantity = DeserializeObject(json); + + Assert.Equal(0, quantity.Value); + Assert.Equal(MassUnit.Milligram, quantity.Unit); + } + + [Fact] + public void DoubleBaseUnitQuantity_DeserializedFromValueAndNoUnit() + { + var json = "{\"Value\":1.2,\"Type\":\"Mass\"}"; + + var quantity = DeserializeObject(json); + + Assert.Equal(1.2, quantity.Value); + Assert.Equal(Mass.BaseUnit, quantity.Unit); + } + + [Fact] + public void DoubleBaseUnitIQuantity_DeserializedFromValueAndNoUnit() + { + var json = "{\"Value\":1.2}"; + + var quantity = DeserializeObject(json); + + Assert.Equal(1.2, quantity.Value); + Assert.Equal(Mass.BaseUnit, quantity.Unit); + } + + [Fact] + public void DoubleZeroBaseIQuantity_DeserializedFromQuantityTypeOnly() + { + var json = "{\"Type\":\"Mass\"}"; + + var quantity = DeserializeObject(json); + + Assert.Equal(0, quantity.Value); + Assert.Equal(Mass.BaseUnit, quantity.Unit); + } + + [Fact] + public void DoubleZeroBaseQuantity_DeserializedFromEmptyInput() + { + var json = "{}"; + + var quantity = DeserializeObject(json); + + Assert.Equal(0, quantity.Value); + Assert.Equal(Mass.BaseUnit, quantity.Unit); + } + + [Fact] + public void DecimalIQuantity_DeserializedFromDecimalValueAndAbbreviatedUnit() + { + var json = "{\"Value\":1.200,\"Unit\":\"EB\",\"Type\":\"Information\"}"; + + var quantity = (Information) DeserializeObject(json); + + Assert.Equal(1.200m, quantity.Value); + Assert.Equal("1.200", quantity.Value.ToString(CultureInfo.InvariantCulture)); + Assert.Equal(InformationUnit.Exabyte, quantity.Unit); + } + + [Fact] + public void DecimalQuantity_DeserializedFromDecimalValueAndAbbreviatedUnit() + { + var json = "{\"Value\":1.200,\"Unit\":\"EB\"}"; + + var quantity = DeserializeObject(json); + + Assert.Equal(1.200m, quantity.Value); + Assert.Equal("1.200", quantity.Value.ToString(CultureInfo.InvariantCulture)); + Assert.Equal(InformationUnit.Exabyte, quantity.Unit); + } + + [Fact] + public void DecimalIQuantity_DeserializedFromQuotedDecimalValueAndAbbreviatedUnit() + { + var json = "{\"Value\":\"1.200\",\"Unit\":\"EB\",\"Type\":\"Information\"}"; + + var quantity = (Information) DeserializeObject(json); + + Assert.Equal(1.200m, quantity.Value); + Assert.Equal("1.200", quantity.Value.ToString(CultureInfo.InvariantCulture)); + Assert.Equal(InformationUnit.Exabyte, quantity.Unit); + } + + [Fact] + public void DecimalQuantity_DeserializedFromQuotedDecimalValueAndAbbreviatedUnit() + { + var json = "{\"Value\":\"1.200\",\"Unit\":\"EB\"}"; + + var quantity = DeserializeObject(json); + + Assert.Equal(1.200m, quantity.Value); + Assert.Equal("1.200", quantity.Value.ToString(CultureInfo.InvariantCulture)); + Assert.Equal(InformationUnit.Exabyte, quantity.Unit); + } + + [Fact] + public void DecimalZeroIQuantity_DeserializedFromAbbreviatedUnitAndNoValue() + { + var json = "{\"Unit\":\"EB\",\"Type\":\"Information\"}"; + + var quantity = (Information)DeserializeObject(json); + + Assert.Equal(0, quantity.Value); + Assert.Equal(InformationUnit.Exabyte, quantity.Unit); + } + + [Fact] + public void DecimalZeroQuantity_DeserializedFromAbbreviatedUnitAndNoValue() + { + var json = "{\"Unit\":\"EB\"}"; + + var quantity = DeserializeObject(json); + + Assert.Equal(0, quantity.Value); + Assert.Equal(InformationUnit.Exabyte, quantity.Unit); + } + + [Fact] + public void DecimalBaseUnitIQuantity_DeserializedFromDecimalValueAndNoUnit() + { + var json = "{\"Value\":1.200,\"Type\":\"Information\"}"; + + var quantity = (Information)DeserializeObject(json); + + Assert.Equal(1.200m, quantity.Value); + Assert.Equal("1.200", quantity.Value.ToString(CultureInfo.InvariantCulture)); + Assert.Equal(Information.BaseUnit, quantity.Unit); + } + + [Fact] + public void DecimalBaseUnitQuantity_DeserializedFromDecimalValueAndNoUnit() + { + var json = "{\"Value\":1.200}"; + + var quantity = DeserializeObject(json); + + Assert.Equal(1.200m, quantity.Value); + Assert.Equal("1.200", quantity.Value.ToString(CultureInfo.InvariantCulture)); + Assert.Equal(Information.BaseUnit, quantity.Unit); + } + + [Fact] + public void DecimalZeroBaseIQuantity_DeserializedFromQuantityTypeOnly() + { + var json = "{\"Type\":\"Information\"}"; + + var quantity = (Information)DeserializeObject(json); + + Assert.Equal(0, quantity.Value); + Assert.Equal(Information.BaseUnit, quantity.Unit); + } + + [Fact] + public void DecimalZeroBaseQuantity_DeserializedFromEmptyInput() + { + var json = "{}"; + + var quantity = DeserializeObject(json); + + Assert.Equal(0, quantity.Value); + Assert.Equal(Information.BaseUnit, quantity.Unit); + } + + #endregion + } +} diff --git a/UnitsNet.Serialization.JsonNet.Tests/JsonNetSerializationTestsBase.cs b/UnitsNet.Serialization.JsonNet.Tests/JsonNetSerializationTestsBase.cs new file mode 100644 index 0000000000..7430a7201c --- /dev/null +++ b/UnitsNet.Serialization.JsonNet.Tests/JsonNetSerializationTestsBase.cs @@ -0,0 +1,34 @@ +// 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; +using Newtonsoft.Json; +using UnitsNet.Tests.Serialization; + +namespace UnitsNet.Serialization.JsonNet.Tests +{ + public abstract class JsonNetSerializationTestsBase : SerializationTestsBase + { + private readonly JsonSerializerSettings _jsonSerializerSettings; + + protected JsonNetSerializationTestsBase(JsonSerializerSettings jsonSerializerSettings) + { + _jsonSerializerSettings = jsonSerializerSettings; + } + + protected JsonNetSerializationTestsBase(params JsonConverter[] converters) + : this(new JsonSerializerSettings { Converters = converters.ToList()}) + { + } + + protected override string SerializeObject(object obj) + { + return JsonConvert.SerializeObject(obj, _jsonSerializerSettings); + } + + protected override T DeserializeObject(string payload) + { + return JsonConvert.DeserializeObject(payload, _jsonSerializerSettings); + } + } +} diff --git a/UnitsNet.Serialization.JsonNet/AbbreviatedUnitsConverter.cs b/UnitsNet.Serialization.JsonNet/AbbreviatedUnitsConverter.cs new file mode 100644 index 0000000000..c4dcb946d6 --- /dev/null +++ b/UnitsNet.Serialization.JsonNet/AbbreviatedUnitsConverter.cs @@ -0,0 +1,319 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Newtonsoft.Json; +using UnitsNet.Units; + +namespace UnitsNet.Serialization.JsonNet +{ + /// + /// + /// JSON.net converter for IQuantity types (e.g. all units in UnitsNet) + /// Use this converter to serialize and deserialize UnitsNet types to and from JSON using the unit abbreviation schema + /// (e.g. 1, "kg", "Mass") + /// + public class AbbreviatedUnitsConverter : JsonConverter + { + private const string ValueProperty = "Value"; + private const string UnitProperty = "Unit"; + private const string TypeProperty = "Type"; + + private readonly UnitAbbreviationsCache _abbreviations; + private readonly IEqualityComparer _propertyComparer; + private readonly IDictionary _quantities; + private readonly UnitParser _unitParser; + + /// + /// Construct a converter using the default list of quantities (case insensitive) and unit abbreviation provider + /// + public AbbreviatedUnitsConverter() + : this(StringComparer.OrdinalIgnoreCase) + { + } + /// + /// Construct a converter using the default list of quantities and unit abbreviation provider + /// + public AbbreviatedUnitsConverter(IEqualityComparer comparer) + : this(new Dictionary(Quantity.ByName, comparer), UnitAbbreviationsCache.Default, comparer) + { + } + + /// + /// Construct a converter using the provided map of {name : quantity} + /// + /// The dictionary of quantity names + /// The unit abbreviations used for the serialization + /// The comparer used to compare the property names (e.g. StringComparer.OrdinalIgnoreCase) + public AbbreviatedUnitsConverter(IDictionary quantities, UnitAbbreviationsCache abbreviations, IEqualityComparer propertyComparer) + { + _quantities = quantities; + _abbreviations = abbreviations; + _propertyComparer = propertyComparer; + _unitParser = new UnitParser(abbreviations); + } + + /// + public override void WriteJson(JsonWriter writer, IQuantity quantity, JsonSerializer serializer) + { + if (quantity is null) + { + writer.WriteNull(); + return; + } + + var unit = GetUnitAbbreviation(quantity.Unit); // by default this should be equal to quantity.ToString("a", CultureInfo.InvariantCulture); + var quantityType = GetQuantityType(quantity); // by default this should be equal to quantity.QuantityInfo.Name + + writer.WriteStartObject(); + + // write the 'Value' using the actual type + writer.WritePropertyName(ValueProperty); + if (quantity is IDecimalQuantity decimalQuantity) + { + // cannot use `writer.WriteValue(decimalQuantity.Value)`: there is a hidden EnsureDecimalPlace(..) method call inside it that converts '123' to '123.0' + writer.WriteRawValue(decimalQuantity.Value.ToString(CultureInfo.InvariantCulture)); + } + else + { + writer.WriteValue(quantity.Value); + } + + // write the 'Unit' abbreviation + writer.WritePropertyName(UnitProperty); + writer.WriteValue(unit); + + // write the quantity 'Type' + writer.WritePropertyName(TypeProperty); + writer.WriteValue(quantityType); + + writer.WriteEndObject(); + } + + /// + /// Get the string representation associated with the given quantity + /// + /// The quantity that is being serialized + /// The string representation associated with the given quantity + protected string GetQuantityType(IQuantity quantity) + { + return _quantities[quantity.QuantityInfo.Name].Name; + } + + /// + public override IQuantity ReadJson(JsonReader reader, Type objectType, IQuantity existingValue, bool hasExistingValue, JsonSerializer serializer) + { + QuantityInfo quantityInfo; + if (reader.TokenType == JsonToken.Null) + { + // return null; + return TryGetQuantity(objectType.Name, out quantityInfo) ? quantityInfo.Zero : default; + } + + string valueToken = null; + string unitAbbreviation = null, quantityName = null; + if (reader.TokenType == JsonToken.StartObject) + { + while (reader.Read() && reader.TokenType != JsonToken.EndObject) + { + if (reader.TokenType == JsonToken.PropertyName) + { + var propertyName = reader.Value as string; + if (_propertyComparer.Equals(propertyName, ValueProperty)) + { + valueToken = reader.ReadAsString(); + } + else if (_propertyComparer.Equals(propertyName, UnitProperty)) + { + unitAbbreviation = reader.ReadAsString(); + } + else if (_propertyComparer.Equals(propertyName, TypeProperty)) + { + quantityName = reader.ReadAsString(); + } + else + { + reader.Skip(); + } + } + } + } + + Enum unit; + if (quantityName is null) + { + if (TryGetQuantity(objectType.Name, out quantityInfo)) + { + unit = GetUnitOrDefault(unitAbbreviation, quantityInfo); + } + else // the objectType doesn't match any concrete quantity type (likely it is an IQuantity) + { + // failing back to an exhaustive search (it is possible that this converter was created with a short-list of non-ambiguous quantities + unit = FindUnit(unitAbbreviation, out quantityInfo); + } + } + else + { + quantityInfo = GetQuantityInfo(quantityName); + unit = GetUnitOrDefault(unitAbbreviation, quantityInfo); + } + + QuantityValue value; + if (valueToken is null) + { + value = default; + } + else if (quantityInfo.Zero is IDecimalQuantity) + { + value = decimal.Parse(valueToken, CultureInfo.InvariantCulture); + } + else + { + value = double.Parse(valueToken, CultureInfo.InvariantCulture); + } + + return Quantity.From(value, unit); + } + + /// + /// Attempt to find an a unique (non-ambiguous) unit matching the provided abbreviation. + /// + /// An exhaustive search using all quantities is very likely to fail with an + /// , so make sure you're using the minimum set of quantities + /// supported quantities. + /// + /// + /// The unit abbreviation + /// The quantity type where the resulting unit was found + /// The unit associated with the given + /// + /// + protected virtual Enum FindUnit(string unitAbbreviation, out QuantityInfo quantityInfo) + { + if (unitAbbreviation is null) // we could assume string.Empty instead + { + throw new UnitNotFoundException("The unit abbreviation and quantity type cannot both be null"); + } + + Enum unit = null; + quantityInfo = default; + foreach (var targetQuantity in _quantities.Values) + { + if (!TryParse(unitAbbreviation, targetQuantity, out var unitMatched)) + { + continue; + } + + if (unit != null && + !(targetQuantity == quantityInfo && Equals(unit, unitMatched))) // it is possible to have "synonyms": e.g. "Mass" and "Weight" + { + throw new AmbiguousUnitParseException($"Multiple quantities found matching the provided abbreviation: {unit}, {unitMatched}"); + } + + quantityInfo = targetQuantity; + unit = unitMatched; + } + + if (unit is null) + { + throw new UnitNotFoundException($"No quantity found with abbreviation [{unitAbbreviation}]."); + } + + return unit; + } + + + /// + /// Get the unit abbreviation associated with the given unit + /// + /// Unit enum value, such as . + /// The default abbreviation as provided by the associated + protected string GetUnitAbbreviation(Enum unit) + { + return _abbreviations.GetDefaultAbbreviation(unit.GetType(), Convert.ToInt32(unit), CultureInfo.InvariantCulture); + } + + /// + /// If the unit abbreviation is unspecified: returns the default (BaseUnit) unit for the + /// , otherwise attempts to the + /// + /// + /// + /// Unit abbreviation, such as "kg" or "m" for and + /// respectively. + /// + /// The associated quantity info + /// Unit enum value, such as . + /// No units match the abbreviation. + /// More than one unit matches the abbreviation. + protected virtual Enum GetUnitOrDefault(string unitAbbreviation, QuantityInfo quantityInfo) + { + return unitAbbreviation == null + ? quantityInfo.BaseUnitInfo.Value + : _unitParser.Parse(unitAbbreviation, quantityInfo.UnitType, CultureInfo.InvariantCulture); + } + + /// + /// Unit abbreviation, such as "kg" or "m" for and + /// respectively. + /// + /// The associated quantity info + /// Unit enum value, such as . + /// No units match the abbreviation. + /// More than one unit matches the abbreviation. + protected Enum Parse(string unitAbbreviation, QuantityInfo quantityInfo) + { + return _unitParser.Parse(unitAbbreviation, quantityInfo.UnitType, CultureInfo.InvariantCulture); + } + + /// + /// Unit abbreviation, such as "kg" or "m" for and + /// respectively. + /// + /// The associated quantity info + /// The unit enum value as out result. + /// True if successful. + /// No units match the abbreviation. + /// More than one unit matches the abbreviation. + protected bool TryParse(string unitAbbreviation, QuantityInfo quantityInfo, out Enum unit) + { + return _unitParser.TryParse(unitAbbreviation, quantityInfo.UnitType, CultureInfo.InvariantCulture, out unit); + } + + /// + /// Try to get the quantity info associated with a given quantity name + /// + /// The name of the quantity: i.e. + /// The quantity information associated with the given quantity name + /// + /// true + /// if a matching quantity is found or + /// false + /// otherwise + /// + protected bool TryGetQuantity(string quantityName, out QuantityInfo quantityInfo) + { + return _quantities.TryGetValue(quantityName, out quantityInfo); + } + + /// + /// Get the quantity info associated with a given quantity name + /// + /// The name of the quantity: i.e. + /// + /// true + /// if a matching quantity is found or + /// false + /// otherwise + /// + /// Quantity not found exception is thrown if no match found + protected QuantityInfo GetQuantityInfo(string quantityName) + { + if (!TryGetQuantity(quantityName, out var quantityInfo)) + { + throw new UnitsNetException($"Failed to find the quantity type: {quantityName}.") { Data = { ["type"] = quantityName } }; + } + + return quantityInfo; + } + } +} diff --git a/UnitsNet.Tests/Serialization/Json/DefaultDataContractJsonSerializerTests.cs b/UnitsNet.Tests/Serialization/Json/DefaultDataContractJsonSerializerTests.cs index 022b82f6ba..34403126dc 100644 --- a/UnitsNet.Tests/Serialization/Json/DefaultDataContractJsonSerializerTests.cs +++ b/UnitsNet.Tests/Serialization/Json/DefaultDataContractJsonSerializerTests.cs @@ -39,6 +39,8 @@ protected override T DeserializeObject(string xml) return (T)serializer.ReadObject(stream); } + #region Serialization tests + [Fact] public void DoubleQuantity_SerializedWithDoubleValueAndIntegerUnit() { @@ -83,6 +85,29 @@ public void DecimalQuantity_InScientificNotation_SerializedWithExpandedValueAndI Assert.Equal(expectedJson, json); } + [Fact] + public void InterfaceObject_IncludesTypeInformation() + { + var testObject = new TestInterfaceObject { Quantity = new Information(1.20m, InformationUnit.Exabyte) }; + var expectedJson = "{\"Quantity\":{\"__type\":\"Information:#UnitsNet\",\"Value\":1.20,\"Unit\":4}}"; + + var json = SerializeObject(testObject); + + Assert.Equal(expectedJson, json); + } + + [Fact] + public void InterfaceObject_WithMissingKnownTypeInformation_ThrowsSerializationException() + { + var testObject = new TestInterfaceObject { Quantity = new Volume(1.2, VolumeUnit.Microliter) }; + + Assert.Throws(() => SerializeObject(testObject)); + } + + #endregion + + #region Deserialization tests + [Fact] public void DoubleQuantity_DeserializedFromDoubleValueAndIntegerUnit() { @@ -196,23 +221,6 @@ public void DecimalZeroBaseQuantity_DeserializedFromEmptyInput() Assert.Equal(Information.BaseUnit, quantity.Unit); } - [Fact] - public void InterfaceObject_IncludesTypeInformation() - { - var testObject = new TestInterfaceObject { Quantity = new Information(1.20m, InformationUnit.Exabyte) }; - var expectedJson = "{\"Quantity\":{\"__type\":\"Information:#UnitsNet\",\"Value\":1.20,\"Unit\":4}}"; - - var json = SerializeObject(testObject); - - Assert.Equal(expectedJson, json); - } - - [Fact] - public void InterfaceObject_WithMissingKnownTypeInformation_ThrowsSerializationException() - { - var testObject = new TestInterfaceObject { Quantity = new Volume(1.2, VolumeUnit.Microliter) }; - - Assert.Throws(() => SerializeObject(testObject)); - } + #endregion } } From 9a9f2c8e6fb0c7ed19e0367954d5a268d5921d9a Mon Sep 17 00:00:00 2001 From: lipchev Date: Mon, 15 Nov 2021 02:17:52 +0200 Subject: [PATCH 2/3] - some cosmetic changes to the converter - added two tests ensuring the compatibility with a PlainOldQuantity (JsonObject) --- .../AbbreviatedUnitsConverterTests.cs | 46 ++++++++++++++++++- .../AbbreviatedUnitsConverter.cs | 5 +- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/UnitsNet.Serialization.JsonNet.Tests/AbbreviatedUnitsConverterTests.cs b/UnitsNet.Serialization.JsonNet.Tests/AbbreviatedUnitsConverterTests.cs index 03a4a71ad0..15bcf8f568 100644 --- a/UnitsNet.Serialization.JsonNet.Tests/AbbreviatedUnitsConverterTests.cs +++ b/UnitsNet.Serialization.JsonNet.Tests/AbbreviatedUnitsConverterTests.cs @@ -1,4 +1,5 @@ using System.Globalization; +using Newtonsoft.Json; using UnitsNet.Tests.Serialization; using UnitsNet.Units; using Xunit; @@ -25,7 +26,7 @@ public void DoubleQuantity_SerializedWithDoubleValueAndAbbreviatedUnit() } [Fact] - public void DecimalQuantity_SerializedWithDecimalValueValueAndAbbreviatedUnit() + public void DecimalQuantity_SerializedWithDecimalValueAndAbbreviatedUnit() { var quantity = new Information(1.20m, InformationUnit.Exabyte); var expectedJson = "{\"Value\":1.20,\"Unit\":\"EB\",\"Type\":\"Information\"}"; @@ -357,5 +358,48 @@ public void DecimalZeroBaseQuantity_DeserializedFromEmptyInput() } #endregion + + #region Compatability + + [JsonObject] + class PlainOldDoubleQuantity + { + public double Value { get; set; } + public string Unit { get; set; } + } + + [JsonObject] + class PlainOldDecimalQuantity + { + public decimal Value { get; set; } + public string Unit { get; set; } + } + + [Fact] + public void LargeDecimalQuantity_DeserializedTo_PlainOldDecimalQuantity() + { + var quantity = new Information(2m * long.MaxValue, InformationUnit.Exabyte); + + var json = SerializeObject(quantity); + var plainOldQuantity = JsonConvert.DeserializeObject(json); + + Assert.Equal(2m * long.MaxValue, plainOldQuantity.Value); + Assert.Equal("EB", plainOldQuantity.Unit); + } + + [Fact] + public void LargeDecimalQuantity_DeserializedTo_PlainOldDoubleQuantity() + { + var quantity = Information.FromExabytes(2m * long.MaxValue); + + var json = SerializeObject(quantity); + var plainOldQuantity = JsonConvert.DeserializeObject(json); + + Assert.Equal(2.0 * long.MaxValue, plainOldQuantity.Value); + Assert.Equal("EB", plainOldQuantity.Unit); + } + + #endregion + } } diff --git a/UnitsNet.Serialization.JsonNet/AbbreviatedUnitsConverter.cs b/UnitsNet.Serialization.JsonNet/AbbreviatedUnitsConverter.cs index c4dcb946d6..e1e600de42 100644 --- a/UnitsNet.Serialization.JsonNet/AbbreviatedUnitsConverter.cs +++ b/UnitsNet.Serialization.JsonNet/AbbreviatedUnitsConverter.cs @@ -30,9 +30,11 @@ public AbbreviatedUnitsConverter() : this(StringComparer.OrdinalIgnoreCase) { } + /// /// Construct a converter using the default list of quantities and unit abbreviation provider /// + /// 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) { @@ -178,8 +180,7 @@ public override IQuantity ReadJson(JsonReader reader, Type objectType, IQuantity /// Attempt to find an a unique (non-ambiguous) unit matching the provided abbreviation. /// /// An exhaustive search using all quantities is very likely to fail with an - /// , so make sure you're using the minimum set of quantities - /// supported quantities. + /// , so make sure you're using the minimum set of supported quantities. /// /// /// The unit abbreviation From a958e9416dc5800695e491c91a9d00749b0b13cf Mon Sep 17 00:00:00 2001 From: lipchev Date: Sat, 20 Nov 2021 23:25:04 +0200 Subject: [PATCH 3/3] - removed tests for *Quantity_InScientificNotation_SerializedWithExpandedValueAndAbbreviatedUnit - added a test for Mbar/mbar: DoubleIQuantity_DeserializedFromDoubleValueAndAbbreviatedUnit_CaseSensitiveUnits - added an example to AbbreviatedUnitsConverter xml doc --- .../AbbreviatedUnitsConverterTests.cs | 40 ++++++++----------- .../AbbreviatedUnitsConverter.cs | 16 ++++++-- .../DefaultDataContractJsonSerializerTests.cs | 22 ---------- .../Xml/DataContractSerializerTests.cs | 22 ---------- 4 files changed, 28 insertions(+), 72 deletions(-) diff --git a/UnitsNet.Serialization.JsonNet.Tests/AbbreviatedUnitsConverterTests.cs b/UnitsNet.Serialization.JsonNet.Tests/AbbreviatedUnitsConverterTests.cs index 15bcf8f568..a39e7fc5da 100644 --- a/UnitsNet.Serialization.JsonNet.Tests/AbbreviatedUnitsConverterTests.cs +++ b/UnitsNet.Serialization.JsonNet.Tests/AbbreviatedUnitsConverterTests.cs @@ -36,28 +36,6 @@ public void DecimalQuantity_SerializedWithDecimalValueAndAbbreviatedUnit() Assert.Equal(expectedJson, json); } - [Fact] - public void DoubleQuantity_InScientificNotation_SerializedWithExpandedValueAndAbbreviatedUnit() - { - var quantity = new Mass(1E+9, MassUnit.Milligram); - var expectedJson = "{\"Value\":1000000000.0,\"Unit\":\"mg\",\"Type\":\"Mass\"}"; - - var json = SerializeObject(quantity); - - Assert.Equal(expectedJson, json); - } - - [Fact] - public void DecimalQuantity_InScientificNotation_SerializedWithExpandedValueAndAbbreviatedUnit() - { - var quantity = new Information(1E+9m, InformationUnit.Exabyte); - var expectedJson = "{\"Value\":1000000000,\"Unit\":\"EB\",\"Type\":\"Information\"}"; - - var json = SerializeObject(quantity); - - Assert.Equal(expectedJson, json); - } - [Fact] public void InterfaceObject_IncludesTypeInformation() { @@ -119,7 +97,7 @@ public void DoubleIQuantity_DeserializedFromDoubleValueAndNonAmbiguousAbbreviate } [Fact] - public void AmbiguousUnitParseExceptionUnitsNetExceptionThrown_WhenDeserializing_WithoutQuantityType() + public void ThrowsAmbiguousUnitParseException_WhenDeserializingAmbiguousAbbreviation_WithoutQuantityType() { var json = "{\"Value\":1.2,\"Unit\":\"mg\"}"; @@ -137,6 +115,20 @@ public void DoubleIQuantity_DeserializedFromDoubleValueAndAbbreviatedUnit_CaseIn Assert.Equal(MassUnit.Milligram, quantity.Unit); } + [Fact] + public void DoubleIQuantity_DeserializedFromDoubleValueAndAbbreviatedUnit_CaseSensitiveUnits() + { + var json = "{\"value\":1.2,\"unit\":\"Mbar\",\"type\":\"pressure\"}"; + + var megabar = DeserializeObject(json); + var millibar = DeserializeObject(json.ToLower()); + + Assert.Equal(1.2, megabar.Value); + Assert.Equal(1.2, millibar.Value); + Assert.Equal(PressureUnit.Megabar, megabar.Unit); + Assert.Equal(PressureUnit.Millibar, millibar.Unit); + } + [Fact] public void UnitsNetExceptionThrown_WhenDeserializing_FromUnknownQuantityType() { @@ -359,7 +351,7 @@ public void DecimalZeroBaseQuantity_DeserializedFromEmptyInput() #endregion - #region Compatability + #region Compatibility [JsonObject] class PlainOldDoubleQuantity diff --git a/UnitsNet.Serialization.JsonNet/AbbreviatedUnitsConverter.cs b/UnitsNet.Serialization.JsonNet/AbbreviatedUnitsConverter.cs index e1e600de42..8f53ddf9d2 100644 --- a/UnitsNet.Serialization.JsonNet/AbbreviatedUnitsConverter.cs +++ b/UnitsNet.Serialization.JsonNet/AbbreviatedUnitsConverter.cs @@ -6,12 +6,20 @@ namespace UnitsNet.Serialization.JsonNet { - /// /// - /// JSON.net converter for IQuantity types (e.g. all units in UnitsNet) - /// Use this converter to serialize and deserialize UnitsNet types to and from JSON using the unit abbreviation schema - /// (e.g. 1, "kg", "Mass") + /// JSON.net converter for all types (e.g. all units in UnitsNet) + /// Use this converter to serialize and deserialize UnitsNet types to and from JSON using the unit abbreviation schema. /// + /// + /// + /// { + /// "Value": 1.20, + /// "Unit": "mg", + /// "Type": "Mass" + /// } + /// + /// + /// public class AbbreviatedUnitsConverter : JsonConverter { private const string ValueProperty = "Value"; diff --git a/UnitsNet.Tests/Serialization/Json/DefaultDataContractJsonSerializerTests.cs b/UnitsNet.Tests/Serialization/Json/DefaultDataContractJsonSerializerTests.cs index 34403126dc..0c6edeeeab 100644 --- a/UnitsNet.Tests/Serialization/Json/DefaultDataContractJsonSerializerTests.cs +++ b/UnitsNet.Tests/Serialization/Json/DefaultDataContractJsonSerializerTests.cs @@ -62,28 +62,6 @@ public void DecimalQuantity_SerializedWithDecimalValueValueAndIntegerUnit() Assert.Equal(expectedJson, json); } - - [Fact] - public void DoubleQuantity_InScientificNotation_SerializedWithExpandedValueAndIntegerUnit() - { - var quantity = new Mass(1E+9, MassUnit.Milligram); - var expectedJson = "{\"Value\":1000000000,\"Unit\":16}"; - - var json = SerializeObject(quantity); - - Assert.Equal(expectedJson, json); - } - - [Fact] - public void DecimalQuantity_InScientificNotation_SerializedWithExpandedValueAndIntegerUnit() - { - var quantity = new Information(1E+9m, InformationUnit.Exabyte); - var expectedJson = "{\"Value\":1000000000,\"Unit\":4}"; - - var json = SerializeObject(quantity); - - Assert.Equal(expectedJson, json); - } [Fact] public void InterfaceObject_IncludesTypeInformation() diff --git a/UnitsNet.Tests/Serialization/Xml/DataContractSerializerTests.cs b/UnitsNet.Tests/Serialization/Xml/DataContractSerializerTests.cs index 4e8b7fb860..ca45df9395 100644 --- a/UnitsNet.Tests/Serialization/Xml/DataContractSerializerTests.cs +++ b/UnitsNet.Tests/Serialization/Xml/DataContractSerializerTests.cs @@ -57,28 +57,6 @@ public void DecimalQuantity_SerializedWithValueAndMemberName() Assert.Equal(expectedXml, xml); } - [Fact] - public void DoubleQuantity_InScientificNotation_SerializedWithExpandedValueAndMemberName() - { - var quantity = new Mass(1E+9, MassUnit.Milligram); - var expectedXml = $"1000000000Milligram"; - - var xml = SerializeObject(quantity); - - Assert.Equal(expectedXml, xml); - } - - [Fact] - public void DecimalQuantity_InScientificNotation_SerializedWithExpandedValueAndMemberName() - { - var quantity = new Information(1E+9m, InformationUnit.Exabyte); - var expectedXml = $"1000000000Exabyte"; - - var xml = SerializeObject(quantity); - - Assert.Equal(expectedXml, xml); - } - [Fact] public void InterfaceObject_IncludesTypeInformation() {