diff --git a/doc/source/whatsnew/v0.20.0.txt b/doc/source/whatsnew/v0.20.0.txt index a34b9feb2b2fa..be9b52e1051f1 100644 --- a/doc/source/whatsnew/v0.20.0.txt +++ b/doc/source/whatsnew/v0.20.0.txt @@ -988,6 +988,7 @@ I/O - Bug in ``pd.read_hdf()`` passing a ``Timestamp`` to the ``where`` parameter with a non date column (:issue:`15492`) - Bug in ``DataFrame.to_stata()`` and ``StataWriter`` which produces incorrectly formatted files to be produced for some locales (:issue:`13856`) - Bug in ``StataReader`` and ``StataWriter`` which allows invalid encodings (:issue:`15723`) +- Bug in ``pd.to_json()`` for the C engine where rollover was not correctly handled for case where frac is odd and diff is exactly 0.5 (:issue:`15716`, :issue:`15864`) Plotting ^^^^^^^^ diff --git a/pandas/_libs/src/ujson/lib/ultrajsonenc.c b/pandas/_libs/src/ujson/lib/ultrajsonenc.c index 5a15071938c1a..6bf2297749006 100644 --- a/pandas/_libs/src/ujson/lib/ultrajsonenc.c +++ b/pandas/_libs/src/ujson/lib/ultrajsonenc.c @@ -823,17 +823,19 @@ int Buffer_AppendDoubleUnchecked(JSOBJ obj, JSONObjectEncoder *enc, if (diff > 0.5) { ++frac; - /* handle rollover, e.g. case 0.99 with prec 1 is 1.0 */ - if (frac >= pow10) { - frac = 0; - ++whole; - } } else if (diff == 0.5 && ((frac == 0) || (frac & 1))) { /* if halfway, round up if odd, OR if last digit is 0. That last part is strange */ ++frac; } + // handle rollover, e.g. + // case 0.99 with prec 1 is 1.0 and case 0.95 with prec is 1.0 as well + if (frac >= pow10) { + frac = 0; + ++whole; + } + if (enc->doublePrecision == 0) { diff = value - whole; diff --git a/pandas/tests/io/json/test_pandas.py b/pandas/tests/io/json/test_pandas.py index 7dbcf25c60b45..8fc8ecbdf8abc 100644 --- a/pandas/tests/io/json/test_pandas.py +++ b/pandas/tests/io/json/test_pandas.py @@ -380,6 +380,31 @@ def test_frame_from_json_nones(self): unser = read_json(df.to_json(), dtype=False) self.assertTrue(np.isnan(unser[2][0])) + def test_frame_to_json_float_precision(self): + df = pd.DataFrame([dict(a_float=0.95)]) + encoded = df.to_json(double_precision=1) + self.assertEqual(encoded, '{"a_float":{"0":1.0}}') + + df = pd.DataFrame([dict(a_float=1.95)]) + encoded = df.to_json(double_precision=1) + self.assertEqual(encoded, '{"a_float":{"0":2.0}}') + + df = pd.DataFrame([dict(a_float=-1.95)]) + encoded = df.to_json(double_precision=1) + self.assertEqual(encoded, '{"a_float":{"0":-2.0}}') + + df = pd.DataFrame([dict(a_float=0.995)]) + encoded = df.to_json(double_precision=2) + self.assertEqual(encoded, '{"a_float":{"0":1.0}}') + + df = pd.DataFrame([dict(a_float=0.9995)]) + encoded = df.to_json(double_precision=3) + self.assertEqual(encoded, '{"a_float":{"0":1.0}}') + + df = pd.DataFrame([dict(a_float=0.99999999999999944)]) + encoded = df.to_json(double_precision=15) + self.assertEqual(encoded, '{"a_float":{"0":1.0}}') + def test_frame_to_json_except(self): df = DataFrame([1, 2, 3]) self.assertRaises(ValueError, df.to_json, orient="garbage") diff --git a/pandas/tests/io/json/test_ujson.py b/pandas/tests/io/json/test_ujson.py index e66721beed288..c2cbbe1ca65ab 100644 --- a/pandas/tests/io/json/test_ujson.py +++ b/pandas/tests/io/json/test_ujson.py @@ -43,6 +43,48 @@ def test_encodeDecimal(self): decoded = ujson.decode(encoded) self.assertEqual(decoded, 1337.1337) + sut = decimal.Decimal("0.95") + encoded = ujson.encode(sut, double_precision=1) + self.assertEqual(encoded, "1.0") + decoded = ujson.decode(encoded) + self.assertEqual(decoded, 1.0) + + sut = decimal.Decimal("0.94") + encoded = ujson.encode(sut, double_precision=1) + self.assertEqual(encoded, "0.9") + decoded = ujson.decode(encoded) + self.assertEqual(decoded, 0.9) + + sut = decimal.Decimal("1.95") + encoded = ujson.encode(sut, double_precision=1) + self.assertEqual(encoded, "2.0") + decoded = ujson.decode(encoded) + self.assertEqual(decoded, 2.0) + + sut = decimal.Decimal("-1.95") + encoded = ujson.encode(sut, double_precision=1) + self.assertEqual(encoded, "-2.0") + decoded = ujson.decode(encoded) + self.assertEqual(decoded, -2.0) + + sut = decimal.Decimal("0.995") + encoded = ujson.encode(sut, double_precision=2) + self.assertEqual(encoded, "1.0") + decoded = ujson.decode(encoded) + self.assertEqual(decoded, 1.0) + + sut = decimal.Decimal("0.9995") + encoded = ujson.encode(sut, double_precision=3) + self.assertEqual(encoded, "1.0") + decoded = ujson.decode(encoded) + self.assertEqual(decoded, 1.0) + + sut = decimal.Decimal("0.99999999999999944") + encoded = ujson.encode(sut, double_precision=15) + self.assertEqual(encoded, "1.0") + decoded = ujson.decode(encoded) + self.assertEqual(decoded, 1.0) + def test_encodeStringConversion(self): input = "A string \\ / \b \f \n \r \t &" not_html_encoded = ('"A string \\\\ \\/ \\b \\f \\n '