Skip to content

Commit

Permalink
Fix JSON serialization of quantities with decimal values
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 committed Dec 17, 2020
1 parent 4993a2a commit e9a8161
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 @@ -268,6 +275,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 e9a8161

Please sign in to comment.