Skip to content

Commit e9a8161

Browse files
committed
Fix JSON serialization of quantities with decimal values
Add IDecimalQuantity interface to expose the decimal value Serialize decimal values as string to keep number of decimal places
1 parent 4993a2a commit e9a8161

File tree

14 files changed

+302
-76
lines changed

14 files changed

+302
-76
lines changed

CodeGen/Generators/UnitsNetGen/QuantityGenerator.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,15 @@ namespace UnitsNet
6969
/// {_quantity.XmlDocRemarks}
7070
/// </remarks>");
7171

72+
Writer.W(@$"
73+
public partial struct {_quantity.Name} : IQuantity<{_unitEnumName}>, ");
74+
if (_quantity.BaseType == "decimal")
75+
{
76+
Writer.W("IDecimalQuantity, ");
77+
}
78+
79+
Writer.WL($"IEquatable<{_quantity.Name}>, IComparable, IComparable<{_quantity.Name}>, IConvertible, IFormattable");
7280
Writer.WL($@"
73-
public partial struct {_quantity.Name} : IQuantity<{_unitEnumName}>, IEquatable<{_quantity.Name}>, IComparable, IComparable<{_quantity.Name}>, IConvertible, IFormattable
7481
{{
7582
/// <summary>
7683
/// The numeric value this quantity was constructed with.
@@ -268,6 +275,11 @@ private void GenerateProperties()
268275
Writer.WL(@"
269276
double IQuantity.Value => (double) _value;
270277
");
278+
if (_quantity.BaseType == "decimal")
279+
Writer.WL(@"
280+
/// <inheritdoc cref=""IDecimalQuantity.Value""/>
281+
decimal IDecimalQuantity.Value => _value;
282+
");
271283

272284
Writer.WL($@"
273285
Enum IQuantity.Unit => Unit;

CodeGen/Generators/UnitsNetGen/UnitTestBaseClassGenerator.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,10 @@ public void Ctor_WithUndefinedUnit_ThrowsArgumentException()
110110
public void DefaultCtor_ReturnsQuantityWithZeroValueAndBaseUnit()
111111
{{
112112
var quantity = new {_quantity.Name}();
113-
Assert.Equal(0, quantity.Value);
113+
Assert.Equal(0, quantity.Value);");
114+
if (_quantity.BaseType == "decimal") Writer.WL($@"
115+
Assert.Equal(0m, ((IDecimalQuantity)quantity).Value);");
116+
Writer.WL($@"
114117
Assert.Equal({_baseUnitFullName}, quantity.Unit);
115118
}}
116119

UnitsNet.Serialization.JsonNet.Tests/UnitsNetBaseJsonConverterTest.cs

Lines changed: 112 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Globalization;
67
using Newtonsoft.Json;
78
using Newtonsoft.Json.Converters;
89
using Newtonsoft.Json.Linq;
@@ -13,38 +14,47 @@ namespace UnitsNet.Serialization.JsonNet.Tests
1314
{
1415
public sealed class UnitsNetBaseJsonConverterTest
1516
{
16-
private TestConverter _sut;
17+
private readonly TestConverter _sut;
1718

1819
public UnitsNetBaseJsonConverterTest()
1920
{
2021
_sut = new TestConverter();
2122
}
2223

2324
[Fact]
24-
public void UnitsNetBaseJsonConverter_ConvertIQuantity_works_as_expected()
25+
public void UnitsNetBaseJsonConverter_ConvertIQuantity_works_with_double_type()
2526
{
26-
var result = _sut.Test_ConvertIQuantity(Power.FromWatts(10.2365D));
27+
var result = _sut.Test_ConvertDoubleIQuantity(Length.FromMeters(10.2365));
28+
29+
Assert.Equal("LengthUnit.Meter", result.Unit);
30+
Assert.Equal(10.2365, result.Value);
31+
}
32+
33+
[Fact]
34+
public void UnitsNetBaseJsonConverter_ConvertIQuantity_works_with_decimal_type()
35+
{
36+
var result = _sut.Test_ConvertDecimalIQuantity(Power.FromWatts(10.2365m));
2737

2838
Assert.Equal("PowerUnit.Watt", result.Unit);
29-
Assert.Equal(10.2365D, result.Value);
39+
Assert.Equal(10.2365m, result.Value);
3040
}
3141

3242
[Fact]
3343
public void UnitsNetBaseJsonConverter_ConvertIQuantity_throws_ArgumentNullException_when_quantity_is_NULL()
3444
{
35-
var result = Assert.Throws<ArgumentNullException>(() => _sut.Test_ConvertIQuantity(null));
45+
var result = Assert.Throws<ArgumentNullException>(() => _sut.Test_ConvertDoubleIQuantity(null));
3646

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

4050
[Fact]
4151
public void UnitsNetBaseJsonConverter_ConvertValueUnit_works_as_expected()
4252
{
43-
var result = _sut.Test_ConvertValueUnit("PowerUnit.Watt", 10.2365D);
53+
var result = _sut.Test_ConvertDecimalValueUnit("PowerUnit.Watt", 10.2365m);
4454

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

4959
}
5060

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

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

7484
Assert.Equal("\"PowerUnit Watt\" is not a valid unit.", result.Message);
7585
Assert.True(result.Data.Contains("type"));
@@ -85,7 +95,7 @@ public void UnitsNetBaseJsonConverter_CreateLocalSerializer_works_as_expected()
8595
TypeNameHandling = TypeNameHandling.Arrays,
8696
Converters = new List<JsonConverter>()
8797
{
88-
98+
8999
new BinaryConverter(),
90100
_sut,
91101
new DataTableConverter()
@@ -104,26 +114,56 @@ public void UnitsNetBaseJsonConverter_CreateLocalSerializer_works_as_expected()
104114
}
105115

106116
[Fact]
107-
public void UnitsNetBaseJsonConverter_ReadValueUnit_work_as_expected()
117+
public void UnitsNetBaseJsonConverter_ReadValueUnit_works_with_double_quantity()
108118
{
109-
var token = new JObject();
119+
var token = new JObject {{"Unit", "LengthUnit.Meter"}, {"Value", 10.2365}};
110120

111-
token.Add("Unit", "PowerUnit.Watt");
112-
token.Add("Value", 10.2365D);
121+
var result = _sut.Test_ReadDoubleValueUnit(token);
122+
123+
Assert.NotNull(result);
124+
Assert.Equal("LengthUnit.Meter", result?.Unit);
125+
Assert.Equal(10.2365, result?.Value);
126+
}
127+
128+
[Fact]
129+
public void UnitsNetBaseJsonConverter_ReadValueUnit_works_with_decimal_quantity()
130+
{
131+
var token = new JObject {{"Unit", "PowerUnit.Watt"}, {"Value", 10.2365m}, {"ValueString", "10.2365"}, {"ValueType", "decimal"}};
113132

114-
var result = _sut.Test_ReadValueUnit(token);
133+
var result = _sut.Test_ReadDecimalValueUnit(token);
115134

116135
Assert.NotNull(result);
117136
Assert.Equal("PowerUnit.Watt", result?.Unit);
118-
Assert.Equal(10.2365D, result?.Value);
137+
Assert.Equal(10.2365m, result?.Value);
138+
}
139+
140+
[Fact]
141+
public void UnitsNetBaseJsonConverter_ReadValueUnit_returns_null_when_value_is_a_string()
142+
{
143+
var token = new JObject {{"Unit", "PowerUnit.Watt"}, {"Value", "10.2365"}};
144+
145+
var result = _sut.Test_ReadDecimalValueUnit(token);
146+
147+
Assert.Null(result);
148+
}
149+
150+
[Fact]
151+
public void UnitsNetBaseJsonConverter_ReadValueUnit_returns_null_when_value_type_is_not_a_string()
152+
{
153+
var token = new JObject {{"Unit", "PowerUnit.Watt"}, {"Value", 10.2365}, {"ValueType", 123}};
154+
155+
var result = _sut.Test_ReadDecimalValueUnit(token);
156+
157+
Assert.Null(result);
119158
}
120159

160+
121161
[Fact]
122-
public void UnitsNetBaseJsonConverter_ReadValueUnit_works_with_empty_token()
162+
public void UnitsNetBaseJsonConverter_ReadDoubleValueUnit_works_with_empty_token()
123163
{
124164
var token = new JObject();
125165

126-
var result = _sut.Test_ReadValueUnit(token);
166+
var result = _sut.Test_ReadDoubleValueUnit(token);
127167

128168
Assert.Null(result);
129169
}
@@ -142,32 +182,40 @@ public void UnitsNetBaseJsonConverter_ReadValueUnit_returns_null_when_unit_or_va
142182

143183
if (withValue)
144184
{
145-
token.Add("Value", 10.2365D);
185+
token.Add("Value", 10.2365m);
146186
}
147187

148-
var result = _sut.Test_ReadValueUnit(token);
188+
var result = _sut.Test_ReadDecimalValueUnit(token);
149189

150190
Assert.Null(result);
151191
}
152192

153193
[Theory]
154-
[InlineData("Unit", "Value")]
155-
[InlineData("unit", "Value")]
156-
[InlineData("Unit", "value")]
157-
[InlineData("unit", "value")]
158-
[InlineData("unIT", "vAlUe")]
159-
public void UnitsNetBaseJsonConverter_ReadValueUnit_works_case_insensitive(string unitPropertyName, string valuePropertyName)
194+
[InlineData("Unit", "Value", "ValueString", "ValueType")]
195+
[InlineData("unit", "Value", "ValueString", "ValueType")]
196+
[InlineData("Unit", "value", "valueString", "valueType")]
197+
[InlineData("unit", "value", "valueString", "valueType")]
198+
[InlineData("unIT", "vAlUe", "vAlUeString", "vAlUeType")]
199+
public void UnitsNetBaseJsonConverter_ReadValueUnit_works_case_insensitive(
200+
string unitPropertyName,
201+
string valuePropertyName,
202+
string valueStringPropertyName,
203+
string valueTypePropertyName)
160204
{
161-
var token = new JObject();
205+
var token = new JObject
206+
{
207+
{unitPropertyName, "PowerUnit.Watt"},
208+
{valuePropertyName, 10.2365m},
209+
{valueStringPropertyName, 10.2365m.ToString(CultureInfo.InvariantCulture)},
210+
{valueTypePropertyName, "decimal"}
211+
};
162212

163-
token.Add(unitPropertyName, "PowerUnit.Watt");
164-
token.Add(valuePropertyName, 10.2365D);
165213

166-
var result = _sut.Test_ReadValueUnit(token);
214+
var result = _sut.Test_ReadDecimalValueUnit(token);
167215

168216
Assert.NotNull(result);
169217
Assert.Equal("PowerUnit.Watt", result?.Unit);
170-
Assert.Equal(10.2365D, result?.Value);
218+
Assert.Equal(10.2365m, result?.Value);
171219
}
172220

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

183-
public (string Unit, double Value) Test_ConvertIQuantity(IQuantity value)
231+
public (string Unit, double Value) Test_ConvertDoubleIQuantity(IQuantity value)
184232
{
185233
var result = ConvertIQuantity(value);
186-
187234
return (result.Unit, result.Value);
188235
}
189236

190-
public IQuantity Test_ConvertValueUnit(string unit, double value) => Test_ConvertValueUnit(new ValueUnit() {Unit = unit, Value = value});
237+
public (string Unit, decimal Value) Test_ConvertDecimalIQuantity(IQuantity value)
238+
{
239+
var result = ConvertIQuantity(value);
240+
if (result is ExtendedValueUnit {ValueType: "decimal"} decimalResult)
241+
{
242+
return (result.Unit, decimal.Parse(decimalResult.ValueString));
243+
}
244+
245+
throw new ArgumentException("The quantity does not have a decimal value", nameof(value));
246+
}
247+
248+
public IQuantity Test_ConvertDoubleValueUnit(string unit, double value) => Test_ConvertValueUnit(new ValueUnit {Unit = unit, Value = value});
249+
250+
public IQuantity Test_ConvertDecimalValueUnit(string unit, decimal value) => Test_ConvertValueUnit(new ExtendedValueUnit
251+
{
252+
Unit = unit, Value = (double) value, ValueString = value.ToString(CultureInfo.InvariantCulture), ValueType = "decimal"
253+
});
254+
191255
public IQuantity Test_ConvertValueUnit() => Test_ConvertValueUnit(null);
192256
private IQuantity Test_ConvertValueUnit(ValueUnit valueUnit) => ConvertValueUnit(valueUnit);
193257

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

196-
public (string Unit, double Value)? Test_ReadValueUnit(JToken jsonToken)
260+
public (string Unit, double Value)? Test_ReadDoubleValueUnit(JToken jsonToken)
197261
{
198262
var result = ReadValueUnit(jsonToken);
199-
200263
if (result == null)
201264
{
202265
return null;
203266
}
204267

205268
return (result.Unit, result.Value);
206269
}
270+
271+
public (string Unit, decimal Value)? Test_ReadDecimalValueUnit(JToken jsonToken)
272+
{
273+
var result = ReadValueUnit(jsonToken);
274+
275+
if (result is ExtendedValueUnit {ValueType: "decimal"} decimalResult)
276+
{
277+
return (result.Unit, decimal.Parse(decimalResult.ValueString));
278+
}
279+
280+
return null;
281+
}
282+
207283
}
208284
}
209285
}

UnitsNet.Serialization.JsonNet.Tests/UnitsNetIQuantityJsonConverterTest.cs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,34 @@ public void UnitsNetIQuantityJsonConverter_WriteJson_works_with_NULL_value()
5656
}
5757

5858
[Fact]
59-
public void UnitsNetIQuantityJsonConverter_WriteJson_works_as_expected()
59+
public void UnitsNetIQuantityJsonConverter_WriteJson_works_with_double_quantity()
60+
{
61+
var result = new StringBuilder();
62+
63+
using (var stringWriter = new StringWriter(result))
64+
using(var writer = new JsonTextWriter(stringWriter))
65+
{
66+
_sut.WriteJson(writer, Length.FromMeters(10.2365D), JsonSerializer.CreateDefault());
67+
}
68+
69+
Assert.Equal("{\"Unit\":\"LengthUnit.Meter\",\"Value\":10.2365}", result.ToString());
70+
}
71+
72+
[Theory]
73+
[InlineData(10.2365, "10.2365", "10.2365")]
74+
[InlineData(10, "10.0", "10")] // Json.NET adds .0
75+
public void UnitsNetIQuantityJsonConverter_WriteJson_works_with_decimal_quantity(decimal value, string expectedValue, string expectedValueString)
6076
{
6177
var result = new StringBuilder();
6278

6379
using (var stringWriter = new StringWriter(result))
6480
using(var writer = new JsonTextWriter(stringWriter))
6581
{
66-
_sut.WriteJson(writer, Power.FromWatts(10.2365D), JsonSerializer.CreateDefault());
82+
_sut.WriteJson(writer, Power.FromWatts(value), JsonSerializer.CreateDefault());
6783
}
6884

69-
Assert.Equal("{\"Unit\":\"PowerUnit.Watt\",\"Value\":10.2365}", result.ToString());
85+
Assert.Equal($"{{\"Unit\":\"PowerUnit.Watt\",\"Value\":{expectedValue},\"ValueString\":\"{expectedValueString}\",\"ValueType\":\"decimal\"}}",
86+
result.ToString());
7087
}
7188

7289
[Fact]

UnitsNet.Serialization.JsonNet.Tests/UnitsNetJsonDeserializationTests.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,46 @@ public void Information_CanDeserializeVeryLargeValues()
1919
Assert.Equal(original, deserialized);
2020
}
2121

22+
[Fact]
23+
public void Information_CanDeserializeMaxValue()
24+
{
25+
var original = Information.MaxValue;
26+
var json = SerializeObject(original);
27+
var deserialized = DeserializeObject<Information>(json);
28+
29+
Assert.Equal(original, deserialized);
30+
}
31+
32+
[Fact]
33+
public void Information_CanDeserializeMinValue()
34+
{
35+
var original = Information.MinValue;
36+
var json = SerializeObject(original);
37+
var deserialized = DeserializeObject<Information>(json);
38+
39+
Assert.Equal(original, deserialized);
40+
}
41+
42+
[Fact]
43+
public void Length_CanDeserializeMaxValue()
44+
{
45+
var original = Length.MaxValue;
46+
var json = SerializeObject(original);
47+
var deserialized = DeserializeObject<Length>(json);
48+
49+
Assert.Equal(original, deserialized);
50+
}
51+
52+
[Fact]
53+
public void Length_CanDeserializeMinValue()
54+
{
55+
var original = Length.MinValue;
56+
var json = SerializeObject(original);
57+
var deserialized = DeserializeObject<Length>(json);
58+
59+
Assert.Equal(original, deserialized);
60+
}
61+
2262
[Fact]
2363
public void Mass_ExpectJsonCorrectlyDeserialized()
2464
{

0 commit comments

Comments
 (0)