Skip to content
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

properly fix #13196: json serialization with option for lossless roundtrip #13364

Closed
Closed
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
44 changes: 34 additions & 10 deletions lib/pure/json.nim
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,21 @@ type
of JArray:
elems*: seq[JsonNode]

const precisionDefault* = -1
## default precision for stringification of float; semantics are subject to
## future implementation improvements, see https://github.com/nim-lang/Nim/issues/13365
# -1 is tied to strmantle.nim and formatfloat.nim (we could also expose that symbol
# to avoid DRY issues)

const precisionRoundtrip* = 17
## precision for stringification of float that is high enough to be roundtrip safe
## to avoid issue #13196.
# this works with nextafter(1.0, Inf).
# note that 17 seems enough, unlike what is mentioned here for D which recommended 18, quoting:
# > ceil(log(pow(2.0, double.mant_dig - 1)) / log(10.0) + 1) == (double.dig + 2)
# see:
# https://github.com/dlang/phobos/blob/b885f607e26750673aba694c46899583779d2361/std/json.d#L1644

proc newJString*(s: string): JsonNode =
## Creates a new `JString JsonNode`.
result = JsonNode(kind: JString, str: s)
Expand Down Expand Up @@ -612,7 +627,7 @@ proc escapeJson*(s: string): string =
escapeJson(s, result)

proc toPretty(result: var string, node: JsonNode, indent = 2, ml = true,
lstArr = false, currIndent = 0) =
lstArr = false, currIndent = 0, precision = precisionDefault) =
case node.kind
of JObject:
if lstArr: result.indent(currIndent) # Indentation
Expand Down Expand Up @@ -647,7 +662,7 @@ proc toPretty(result: var string, node: JsonNode, indent = 2, ml = true,
if lstArr: result.indent(currIndent)
# Fixme: implement new system.add ops for the JS target
when defined(js): result.add($node.fnum)
else: result.addFloat(node.fnum)
else: result.addFloat(node.fnum, precision = precision)
of JBool:
if lstArr: result.indent(currIndent)
result.add(if node.bval: "true" else: "false")
Expand All @@ -670,14 +685,14 @@ proc toPretty(result: var string, node: JsonNode, indent = 2, ml = true,
if lstArr: result.indent(currIndent)
result.add("null")

proc pretty*(node: JsonNode, indent = 2): string =
proc pretty*(node: JsonNode, indent = 2, precision = precisionDefault): string =
## Returns a JSON Representation of `node`, with indentation and
## on multiple lines.
##
## Similar to prettyprint in Python.
runnableExamples:
let j = %* {"name": "Isaac", "books": ["Robot Dreams"],
"details": {"age": 35, "pi": 3.1415}}
"details": {"age": 35, "number": 3.125}}
timotheecour marked this conversation as resolved.
Show resolved Hide resolved
doAssert pretty(j) == """
{
"name": "Isaac",
Expand All @@ -686,13 +701,14 @@ proc pretty*(node: JsonNode, indent = 2): string =
],
"details": {
"age": 35,
"pi": 3.1415
"number": 3.125
}
}"""
doAssert pretty(j["details"]["number"], precision = 2) == "3.1"
result = ""
toPretty(result, node, indent)
toPretty(result, node, indent, precision = precision)

proc toUgly*(result: var string, node: JsonNode) =
proc toUgly*(result: var string, node: JsonNode, precision = precisionDefault) =
## Converts `node` to its JSON Representation, without
## regard for human readability. Meant to improve ``$`` string
## conversion performance.
Expand Down Expand Up @@ -726,16 +742,16 @@ proc toUgly*(result: var string, node: JsonNode) =
else: result.addInt(node.num)
of JFloat:
when defined(js): result.add($node.fnum)
else: result.addFloat(node.fnum)
else: result.addFloat(node.fnum, precision = precision)
of JBool:
result.add(if node.bval: "true" else: "false")
of JNull:
result.add "null"

proc `$`*(node: JsonNode): string =
proc `$`*(node: JsonNode, precision = precisionDefault): string =
## Converts `node` to its JSON Representation on one line.
result = newStringOfCap(node.len shl 1)
toUgly(result, node)
toUgly(result, node, precision = precision)

iterator items*(node: JsonNode): JsonNode =
## Iterator for the items of `node`. `node` has to be a JArray.
Expand Down Expand Up @@ -1486,3 +1502,11 @@ when isMainModule:
doAssert not isRefSkipDistinct(MyObject)
doAssert isRefSkipDistinct(MyDistinct)
doAssert isRefSkipDistinct(MyOtherDistinct)

block: # issue #13196
# some arbitrary float, not caring about actually significant places here
let x = 0.12345678901234567890123456789
let j = %* x
let y = (`$`(j, precision = precisionRoundtrip)).parseJson().getFloat()
doAssert x == y
doAssert $0.6 == "0.6"
5 changes: 3 additions & 2 deletions lib/system/formatfloat.nim
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ proc writeToBuffer(buf: var array[65, char]; value: cstring) =
buf[i] = value[i]
inc i

proc writeFloatToBuffer*(buf: var array[65, char]; value: BiggestFloat): int =
proc writeFloatToBuffer*(buf: var array[65, char]; value: BiggestFloat, precision = -1): int =
## This is the implementation to format floats in the Nim
## programming language. The specific format for floating point
## numbers is not specified in the Nim programming language and
Expand All @@ -29,7 +29,8 @@ proc writeFloatToBuffer*(buf: var array[65, char]; value: BiggestFloat): int =
## * `buf` - A buffer to write into. The buffer does not need to be
## initialized and it will be overridden.
##
var n: int = c_sprintf(addr buf, "%.16g", value)
let precision2 = if precision == -1: 16 else: precision
var n: int = c_sprintf(addr buf, "%.*g", precision2.cint, value)
var hasDot = false
for i in 0..n-1:
if buf[i] == ',':
Expand Down
5 changes: 3 additions & 2 deletions lib/system/strmantle.nim
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,9 @@ proc addCstringN(result: var string, buf: cstring; buflen: int) =

import formatfloat

proc addFloat*(result: var string; x: float) =
proc addFloat*(result: var string; x: float, precision = -1) =
## Converts float to its string representation and appends it to `result`.
## passing `precision >=0` can override the default precision.
##
## .. code-block:: Nim
## var
Expand All @@ -95,7 +96,7 @@ proc addFloat*(result: var string; x: float) =
result.add $x
else:
var buffer: array[65, char]
let n = writeFloatToBuffer(buffer, x)
let n = writeFloatToBuffer(buffer, x, precision = precision)
result.addCstringN(cstring(buffer[0].addr), n)

proc add*(result: var string; x: float) {.deprecated:
Expand Down