Skip to content

Fix JSON serialization of quantities with decimal values #868

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 17, 2020
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
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