Skip to content

Commit 2afdccd

Browse files
authored
Fix parsed decimal losing trailing zeroes (#2769)
1 parent 4fba53a commit 2afdccd

File tree

2 files changed

+188
-1
lines changed

2 files changed

+188
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
#region License
2+
// Copyright (c) 2007 James Newton-King
3+
//
4+
// Permission is hereby granted, free of charge, to any person
5+
// obtaining a copy of this software and associated documentation
6+
// files (the "Software"), to deal in the Software without
7+
// restriction, including without limitation the rights to use,
8+
// copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
// copies of the Software, and to permit persons to whom the
10+
// Software is furnished to do so, subject to the following
11+
// conditions:
12+
//
13+
// The above copyright notice and this permission notice shall be
14+
// included in all copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17+
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18+
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19+
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20+
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21+
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22+
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
23+
// OTHER DEALINGS IN THE SOFTWARE.
24+
#endregion
25+
26+
#if !NET20
27+
using System;
28+
using System.Collections.Generic;
29+
using System.IO;
30+
using System.Linq;
31+
using System.Runtime.Serialization;
32+
using System.Text;
33+
using System.Threading;
34+
using Newtonsoft.Json.Linq;
35+
using Newtonsoft.Json.Tests.Documentation.Samples.Serializer;
36+
using System.Globalization;
37+
#if DNXCORE50
38+
using System.Reflection;
39+
using Xunit;
40+
using Test = Xunit.FactAttribute;
41+
using Assert = Newtonsoft.Json.Tests.XUnitAssert;
42+
#else
43+
using NUnit.Framework;
44+
#endif
45+
46+
namespace Newtonsoft.Json.Tests.Issues
47+
{
48+
[TestFixture]
49+
public class Issue2768 : TestFixtureBase
50+
{
51+
[Test]
52+
public void Test_Serialize()
53+
{
54+
decimal d = 0.0m;
55+
string json = JsonConvert.SerializeObject(d);
56+
57+
Assert.AreEqual("0.0", json);
58+
}
59+
60+
[Test]
61+
public void Test_Serialize_NoTrailingZero()
62+
{
63+
decimal d = 0m;
64+
string json = JsonConvert.SerializeObject(d);
65+
66+
Assert.AreEqual("0.0", json);
67+
}
68+
69+
[Test]
70+
public void Test_Deserialize()
71+
{
72+
decimal d = JsonConvert.DeserializeObject<decimal>("0.0");
73+
74+
Assert.AreEqual("0.0", d.ToString());
75+
}
76+
77+
[Test]
78+
public void Test_Deserialize_MultipleTrailingZeroes()
79+
{
80+
decimal d = JsonConvert.DeserializeObject<decimal>("0.00");
81+
82+
Assert.AreEqual("0.00", d.ToString());
83+
}
84+
85+
[Test]
86+
public void Test_Deserialize_NoTrailingZero()
87+
{
88+
decimal d = JsonConvert.DeserializeObject<decimal>("0");
89+
90+
Assert.AreEqual("0", d.ToString());
91+
}
92+
93+
[Test]
94+
public void ParseJsonDecimal()
95+
{
96+
var json = @"{ ""property"": 0.0 }";
97+
98+
var reader = new JsonTextReader(new StringReader(json))
99+
{
100+
FloatParseHandling = FloatParseHandling.Decimal
101+
};
102+
103+
decimal? parsedDecimal = null;
104+
105+
while (reader.Read())
106+
{
107+
if (reader.TokenType == JsonToken.Float)
108+
{
109+
parsedDecimal = (decimal)reader.Value;
110+
break;
111+
}
112+
}
113+
114+
Assert.AreEqual("0.0", parsedDecimal.ToString());
115+
}
116+
117+
[Test]
118+
public void ParseJsonDecimal_IsBoxedInstanceSame()
119+
{
120+
var json = @"[ 0.0, 0.0 ]";
121+
122+
var reader = new JsonTextReader(new StringReader(json))
123+
{
124+
FloatParseHandling = FloatParseHandling.Decimal
125+
};
126+
127+
List<object> boxedDecimals = new List<object>();
128+
129+
// Start array
130+
Assert.IsTrue(reader.Read());
131+
132+
Assert.IsTrue(reader.Read());
133+
boxedDecimals.Add(reader.Value);
134+
135+
Assert.IsTrue(reader.Read());
136+
boxedDecimals.Add(reader.Value);
137+
138+
Assert.IsTrue(reader.Read());
139+
Assert.IsFalse(reader.Read());
140+
141+
// Boxed values will match or not depending on whether framework supports.
142+
#if NET6_0_OR_GREATER
143+
Assert.IsTrue(object.ReferenceEquals(boxedDecimals[0], boxedDecimals[1]));
144+
#else
145+
Assert.IsFalse(object.ReferenceEquals(boxedDecimals[0], boxedDecimals[1]));
146+
#endif
147+
148+
}
149+
}
150+
}
151+
#endif

Src/Newtonsoft.Json/Utilities/BoxedPrimitives.cs

+37-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
// OTHER DEALINGS IN THE SOFTWARE.
2424
#endregion
2525

26+
using System;
27+
using System.Diagnostics;
28+
2629
namespace Newtonsoft.Json.Utilities
2730
{
2831
internal static class BoxedPrimitives
@@ -87,9 +90,42 @@ internal static class BoxedPrimitives
8790
internal static readonly object Int64_7 = 7L;
8891
internal static readonly object Int64_8 = 8L;
8992

90-
internal static object Get(decimal value) => value == decimal.Zero ? DecimalZero : value;
93+
internal static object Get(decimal value)
94+
{
95+
// Decimals can contain trailing zeros. For example 1 vs 1.0. Unfortunatly, Equals doesn't check for trailing zeros.
96+
// There isn't a way to find out if a decimal has trailing zeros in older frameworks without calling ToString.
97+
// Don't provide a cached boxed decimal value in older frameworks.
98+
99+
#if NET6_0_OR_GREATER
100+
// Number of bits scale is shifted by.
101+
const int ScaleShift = 16;
102+
103+
if (value == decimal.Zero)
104+
{
105+
Span<int> bits = stackalloc int[4];
106+
int written = decimal.GetBits(value, bits);
107+
MiscellaneousUtils.Assert(written == 4);
108+
109+
byte scale = (byte)(bits[3] >> ScaleShift);
110+
// Only use cached boxed value if value is zero and there is zero or one trailing zeros.
111+
if (scale == 0)
112+
{
113+
return DecimalZero;
114+
}
115+
else if (scale == 1)
116+
{
117+
return DecimalZeroWithTrailingZero;
118+
}
119+
}
120+
#endif
121+
122+
return value;
123+
}
91124

125+
#if NET6_0_OR_GREATER
92126
private static readonly object DecimalZero = decimal.Zero;
127+
private static readonly object DecimalZeroWithTrailingZero = 0.0m;
128+
#endif
93129

94130
internal static object Get(double value)
95131
{

0 commit comments

Comments
 (0)