Skip to content

Commit

Permalink
Fix JSON serialization of quantities with decimal values (#868)
Browse files Browse the repository at this point in the history
Add IDecimalQuantity interface to expose the decimal value
Serialize decimal values as string to keep number of decimal places
  • Loading branch information
rohahn authored Dec 17, 2020
1 parent c46d064 commit 25b6558
Show file tree
Hide file tree
Showing 14 changed files with 302 additions and 76 deletions.
14 changes: 13 additions & 1 deletion CodeGen/Generators/UnitsNetGen/QuantityGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,15 @@ namespace UnitsNet
/// {_quantity.XmlDocRemarks}
/// </remarks>");

Writer.W(@$"
public partial struct {_quantity.Name} : IQuantity<{_unitEnumName}>, ");
if (_quantity.BaseType == "decimal")
{
Writer.W("IDecimalQuantity, ");
}

Writer.WL($"IEquatable<{_quantity.Name}>, IComparable, IComparable<{_quantity.Name}>, IConvertible, IFormattable");
Writer.WL($@"
public partial struct {_quantity.Name} : IQuantity<{_unitEnumName}>, IEquatable<{_quantity.Name}>, IComparable, IComparable<{_quantity.Name}>, IConvertible, IFormattable
{{
/// <summary>
/// The numeric value this quantity was constructed with.
Expand Down Expand Up @@ -269,6 +276,11 @@ private void GenerateProperties()
Writer.WL(@"
double IQuantity.Value => (double) _value;
");
if (_quantity.BaseType == "decimal")
Writer.WL(@"
/// <inheritdoc cref=""IDecimalQuantity.Value""/>
decimal IDecimalQuantity.Value => _value;
");

Writer.WL($@"
Enum IQuantity.Unit => Unit;
Expand Down
5 changes: 4 additions & 1 deletion CodeGen/Generators/UnitsNetGen/UnitTestBaseClassGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,10 @@ public void Ctor_WithUndefinedUnit_ThrowsArgumentException()
public void DefaultCtor_ReturnsQuantityWithZeroValueAndBaseUnit()
{{
var quantity = new {_quantity.Name}();
Assert.Equal(0, quantity.Value);
Assert.Equal(0, quantity.Value);");
if (_quantity.BaseType == "decimal") Writer.WL($@"
Assert.Equal(0m, ((IDecimalQuantity)quantity).Value);");
Writer.WL($@"
Assert.Equal({_baseUnitFullName}, quantity.Unit);
}}
Expand Down
148 changes: 112 additions & 36 deletions UnitsNet.Serialization.JsonNet.Tests/UnitsNetBaseJsonConverterTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Globalization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
Expand All @@ -13,38 +14,47 @@ namespace UnitsNet.Serialization.JsonNet.Tests
{
public sealed class UnitsNetBaseJsonConverterTest
{
private TestConverter _sut;
private readonly TestConverter _sut;

public UnitsNetBaseJsonConverterTest()
{
_sut = new TestConverter();
}

[Fact]
public void UnitsNetBaseJsonConverter_ConvertIQuantity_works_as_expected()
public void UnitsNetBaseJsonConverter_ConvertIQuantity_works_with_double_type()
{
var result = _sut.Test_ConvertIQuantity(Power.FromWatts(10.2365D));
var result = _sut.Test_ConvertDoubleIQuantity(Length.FromMeters(10.2365));

Assert.Equal("LengthUnit.Meter", result.Unit);
Assert.Equal(10.2365, result.Value);
}

[Fact]
public void UnitsNetBaseJsonConverter_ConvertIQuantity_works_with_decimal_type()
{
var result = _sut.Test_ConvertDecimalIQuantity(Power.FromWatts(10.2365m));

Assert.Equal("PowerUnit.Watt", result.Unit);
Assert.Equal(10.2365D, result.Value);
Assert.Equal(10.2365m, result.Value);
}

[Fact]
public void UnitsNetBaseJsonConverter_ConvertIQuantity_throws_ArgumentNullException_when_quantity_is_NULL()
{
var result = Assert.Throws<ArgumentNullException>(() => _sut.Test_ConvertIQuantity(null));
var result = Assert.Throws<ArgumentNullException>(() => _sut.Test_ConvertDoubleIQuantity(null));

Assert.Equal("Value cannot be null.\r\nParameter name: quantity", result.Message);
}

[Fact]
public void UnitsNetBaseJsonConverter_ConvertValueUnit_works_as_expected()
{
var result = _sut.Test_ConvertValueUnit("PowerUnit.Watt", 10.2365D);
var result = _sut.Test_ConvertDecimalValueUnit("PowerUnit.Watt", 10.2365m);

Assert.NotNull(result);
Assert.IsType<Power>(result);
Assert.True(Power.FromWatts(10.2365D).Equals((Power)result, 1E-5, ComparisonType.Absolute));
Assert.True(Power.FromWatts(10.2365m).Equals((Power)result, 1E-5, ComparisonType.Absolute));

}

Expand All @@ -59,7 +69,7 @@ public void UnitsNetBaseJsonConverter_ConvertValueUnit_works_with_NULL_value()
[Fact]
public void UnitsNetBaseJsonConverter_ConvertValueUnit_throws_UnitsNetException_when_unit_does_not_exist()
{
var result = Assert.Throws<UnitsNetException>(() => _sut.Test_ConvertValueUnit("SomeImaginaryUnit.Watt", 10.2365D));
var result = Assert.Throws<UnitsNetException>(() => _sut.Test_ConvertDoubleValueUnit("SomeImaginaryUnit.Watt", 10.2365D));

Assert.Equal("Unable to find enum type.", result.Message);
Assert.True(result.Data.Contains("type"));
Expand All @@ -69,7 +79,7 @@ public void UnitsNetBaseJsonConverter_ConvertValueUnit_throws_UnitsNetException_
[Fact]
public void UnitsNetBaseJsonConverter_ConvertValueUnit_throws_UnitsNetException_when_unit_is_in_unexpected_format()
{
var result = Assert.Throws<UnitsNetException>(() => _sut.Test_ConvertValueUnit("PowerUnit Watt", 10.2365D));
var result = Assert.Throws<UnitsNetException>(() => _sut.Test_ConvertDecimalValueUnit("PowerUnit Watt", 10.2365m));

Assert.Equal("\"PowerUnit Watt\" is not a valid unit.", result.Message);
Assert.True(result.Data.Contains("type"));
Expand All @@ -85,7 +95,7 @@ public void UnitsNetBaseJsonConverter_CreateLocalSerializer_works_as_expected()
TypeNameHandling = TypeNameHandling.Arrays,
Converters = new List<JsonConverter>()
{

new BinaryConverter(),
_sut,
new DataTableConverter()
Expand All @@ -104,26 +114,56 @@ public void UnitsNetBaseJsonConverter_CreateLocalSerializer_works_as_expected()
}

[Fact]
public void UnitsNetBaseJsonConverter_ReadValueUnit_work_as_expected()
public void UnitsNetBaseJsonConverter_ReadValueUnit_works_with_double_quantity()
{
var token = new JObject();
var token = new JObject {{"Unit", "LengthUnit.Meter"}, {"Value", 10.2365}};

token.Add("Unit", "PowerUnit.Watt");
token.Add("Value", 10.2365D);
var result = _sut.Test_ReadDoubleValueUnit(token);

Assert.NotNull(result);
Assert.Equal("LengthUnit.Meter", result?.Unit);
Assert.Equal(10.2365, result?.Value);
}

[Fact]
public void UnitsNetBaseJsonConverter_ReadValueUnit_works_with_decimal_quantity()
{
var token = new JObject {{"Unit", "PowerUnit.Watt"}, {"Value", 10.2365m}, {"ValueString", "10.2365"}, {"ValueType", "decimal"}};

var result = _sut.Test_ReadValueUnit(token);
var result = _sut.Test_ReadDecimalValueUnit(token);

Assert.NotNull(result);
Assert.Equal("PowerUnit.Watt", result?.Unit);
Assert.Equal(10.2365D, result?.Value);
Assert.Equal(10.2365m, result?.Value);
}

[Fact]
public void UnitsNetBaseJsonConverter_ReadValueUnit_returns_null_when_value_is_a_string()
{
var token = new JObject {{"Unit", "PowerUnit.Watt"}, {"Value", "10.2365"}};

var result = _sut.Test_ReadDecimalValueUnit(token);

Assert.Null(result);
}

[Fact]
public void UnitsNetBaseJsonConverter_ReadValueUnit_returns_null_when_value_type_is_not_a_string()
{
var token = new JObject {{"Unit", "PowerUnit.Watt"}, {"Value", 10.2365}, {"ValueType", 123}};

var result = _sut.Test_ReadDecimalValueUnit(token);

Assert.Null(result);
}


[Fact]
public void UnitsNetBaseJsonConverter_ReadValueUnit_works_with_empty_token()
public void UnitsNetBaseJsonConverter_ReadDoubleValueUnit_works_with_empty_token()
{
var token = new JObject();

var result = _sut.Test_ReadValueUnit(token);
var result = _sut.Test_ReadDoubleValueUnit(token);

Assert.Null(result);
}
Expand All @@ -142,32 +182,40 @@ public void UnitsNetBaseJsonConverter_ReadValueUnit_returns_null_when_unit_or_va

if (withValue)
{
token.Add("Value", 10.2365D);
token.Add("Value", 10.2365m);
}

var result = _sut.Test_ReadValueUnit(token);
var result = _sut.Test_ReadDecimalValueUnit(token);

Assert.Null(result);
}

[Theory]
[InlineData("Unit", "Value")]
[InlineData("unit", "Value")]
[InlineData("Unit", "value")]
[InlineData("unit", "value")]
[InlineData("unIT", "vAlUe")]
public void UnitsNetBaseJsonConverter_ReadValueUnit_works_case_insensitive(string unitPropertyName, string valuePropertyName)
[InlineData("Unit", "Value", "ValueString", "ValueType")]
[InlineData("unit", "Value", "ValueString", "ValueType")]
[InlineData("Unit", "value", "valueString", "valueType")]
[InlineData("unit", "value", "valueString", "valueType")]
[InlineData("unIT", "vAlUe", "vAlUeString", "vAlUeType")]
public void UnitsNetBaseJsonConverter_ReadValueUnit_works_case_insensitive(
string unitPropertyName,
string valuePropertyName,
string valueStringPropertyName,
string valueTypePropertyName)
{
var token = new JObject();
var token = new JObject
{
{unitPropertyName, "PowerUnit.Watt"},
{valuePropertyName, 10.2365m},
{valueStringPropertyName, 10.2365m.ToString(CultureInfo.InvariantCulture)},
{valueTypePropertyName, "decimal"}
};

token.Add(unitPropertyName, "PowerUnit.Watt");
token.Add(valuePropertyName, 10.2365D);

var result = _sut.Test_ReadValueUnit(token);
var result = _sut.Test_ReadDecimalValueUnit(token);

Assert.NotNull(result);
Assert.Equal("PowerUnit.Watt", result?.Unit);
Assert.Equal(10.2365D, result?.Value);
Assert.Equal(10.2365m, result?.Value);
}

/// <summary>
Expand All @@ -180,30 +228,58 @@ private class TestConverter : UnitsNetBaseJsonConverter<string>
public override void WriteJson(JsonWriter writer, string value, JsonSerializer serializer) => throw new NotImplementedException();
public override string ReadJson(JsonReader reader, Type objectType, string existingValue, bool hasExistingValue, JsonSerializer serializer) => throw new NotImplementedException();

public (string Unit, double Value) Test_ConvertIQuantity(IQuantity value)
public (string Unit, double Value) Test_ConvertDoubleIQuantity(IQuantity value)
{
var result = ConvertIQuantity(value);

return (result.Unit, result.Value);
}

public IQuantity Test_ConvertValueUnit(string unit, double value) => Test_ConvertValueUnit(new ValueUnit() {Unit = unit, Value = value});
public (string Unit, decimal Value) Test_ConvertDecimalIQuantity(IQuantity value)
{
var result = ConvertIQuantity(value);
if (result is ExtendedValueUnit {ValueType: "decimal"} decimalResult)
{
return (result.Unit, decimal.Parse(decimalResult.ValueString));
}

throw new ArgumentException("The quantity does not have a decimal value", nameof(value));
}

public IQuantity Test_ConvertDoubleValueUnit(string unit, double value) => Test_ConvertValueUnit(new ValueUnit {Unit = unit, Value = value});

public IQuantity Test_ConvertDecimalValueUnit(string unit, decimal value) => Test_ConvertValueUnit(new ExtendedValueUnit
{
Unit = unit, Value = (double) value, ValueString = value.ToString(CultureInfo.InvariantCulture), ValueType = "decimal"
});

public IQuantity Test_ConvertValueUnit() => Test_ConvertValueUnit(null);
private IQuantity Test_ConvertValueUnit(ValueUnit valueUnit) => ConvertValueUnit(valueUnit);

public JsonSerializer Test_CreateLocalSerializer(JsonSerializer serializer) => CreateLocalSerializer(serializer, this);

public (string Unit, double Value)? Test_ReadValueUnit(JToken jsonToken)
public (string Unit, double Value)? Test_ReadDoubleValueUnit(JToken jsonToken)
{
var result = ReadValueUnit(jsonToken);

if (result == null)
{
return null;
}

return (result.Unit, result.Value);
}

public (string Unit, decimal Value)? Test_ReadDecimalValueUnit(JToken jsonToken)
{
var result = ReadValueUnit(jsonToken);

if (result is ExtendedValueUnit {ValueType: "decimal"} decimalResult)
{
return (result.Unit, decimal.Parse(decimalResult.ValueString));
}

return null;
}

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,34 @@ public void UnitsNetIQuantityJsonConverter_WriteJson_works_with_NULL_value()
}

[Fact]
public void UnitsNetIQuantityJsonConverter_WriteJson_works_as_expected()
public void UnitsNetIQuantityJsonConverter_WriteJson_works_with_double_quantity()
{
var result = new StringBuilder();

using (var stringWriter = new StringWriter(result))
using(var writer = new JsonTextWriter(stringWriter))
{
_sut.WriteJson(writer, Length.FromMeters(10.2365D), JsonSerializer.CreateDefault());
}

Assert.Equal("{\"Unit\":\"LengthUnit.Meter\",\"Value\":10.2365}", result.ToString());
}

[Theory]
[InlineData(10.2365, "10.2365", "10.2365")]
[InlineData(10, "10.0", "10")] // Json.NET adds .0
public void UnitsNetIQuantityJsonConverter_WriteJson_works_with_decimal_quantity(decimal value, string expectedValue, string expectedValueString)
{
var result = new StringBuilder();

using (var stringWriter = new StringWriter(result))
using(var writer = new JsonTextWriter(stringWriter))
{
_sut.WriteJson(writer, Power.FromWatts(10.2365D), JsonSerializer.CreateDefault());
_sut.WriteJson(writer, Power.FromWatts(value), JsonSerializer.CreateDefault());
}

Assert.Equal("{\"Unit\":\"PowerUnit.Watt\",\"Value\":10.2365}", result.ToString());
Assert.Equal($"{{\"Unit\":\"PowerUnit.Watt\",\"Value\":{expectedValue},\"ValueString\":\"{expectedValueString}\",\"ValueType\":\"decimal\"}}",
result.ToString());
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,46 @@ public void Information_CanDeserializeVeryLargeValues()
Assert.Equal(original, deserialized);
}

[Fact]
public void Information_CanDeserializeMaxValue()
{
var original = Information.MaxValue;
var json = SerializeObject(original);
var deserialized = DeserializeObject<Information>(json);

Assert.Equal(original, deserialized);
}

[Fact]
public void Information_CanDeserializeMinValue()
{
var original = Information.MinValue;
var json = SerializeObject(original);
var deserialized = DeserializeObject<Information>(json);

Assert.Equal(original, deserialized);
}

[Fact]
public void Length_CanDeserializeMaxValue()
{
var original = Length.MaxValue;
var json = SerializeObject(original);
var deserialized = DeserializeObject<Length>(json);

Assert.Equal(original, deserialized);
}

[Fact]
public void Length_CanDeserializeMinValue()
{
var original = Length.MinValue;
var json = SerializeObject(original);
var deserialized = DeserializeObject<Length>(json);

Assert.Equal(original, deserialized);
}

[Fact]
public void Mass_ExpectJsonCorrectlyDeserialized()
{
Expand Down
Loading

0 comments on commit 25b6558

Please sign in to comment.