Files
troupe-standup/wizard-work/beam/json_canonical.py
T
volt 6c097bff10 volt: beam: Q82 -- json_canonical.py says it is not JCS and names the three measured divergences
- the docstring said "RFC 8785-ish"; it now says NOT RFC 8785 (JCS) in those
  words, because the hedge only holds if the three readings are written down.
- measured at this file (scratch/q82/floats.py, output in the closure record):
  -0.0 -> "-0.0" where RFC 8785 Table 1 gives "0"; 1e-06 -> "1e-06" where the
  table gives "0.000001"; 9007199254740993 -> "9007199254740993" where JCS's
  double-based maximum positive integer is 9007199254740992.
- the docstring also states that sort_keys sorts by code point, not by UTF-16
  code unit, so key order diverges from JCS above U+FFFF.
- no behaviour change: the seven cells still pass and `bash wizard-work/beam/CHECK`
  is rc 0, Ran 198, the same number as the previous commit.

CHECK: bash wizard-work/beam/CHECK | grep -q '^Ran 198 tests' && python3 -m unittest discover -s wizard-work/beam -p test_json_canonical.py
2026-10-26 20:05:45 +00:00

52 lines
1.7 KiB
Python

"""Canonical JSON serialization: sorted keys, compact separators, no NaN/Infinity,
deterministic float repr, UTF-8, ensure_ascii=False.
NOT RFC 8785 (JCS), and not claimed to be. JCS is IEEE-754 double-based with its
own number grammar; this module emits Python's `repr` for floats and passes int
through unchanged. Three measured divergences from RFC 8785 Table 1:
-0.0 -> "-0.0" (JCS: "0")
1e-06 -> "1e-06" (JCS: "0.000001")
2**53+1 -> "9007199254740993" (JCS max positive integer: 9007199254740992)
Key order is `sort_keys=True`, i.e. by Unicode code point; JCS sorts by UTF-16
code unit, so the two diverge above U+FFFF.
"""
from __future__ import annotations
import json
import math
from typing import Any
class CanonicalError(ValueError):
pass
def _normalize(obj):
if obj is None or isinstance(obj, bool) or isinstance(obj, int) or isinstance(obj, str):
return obj
if isinstance(obj, float):
if not math.isfinite(obj):
raise CanonicalError("non-finite float: %r" % obj)
return obj
if isinstance(obj, (list, tuple)):
return [_normalize(x) for x in obj]
if isinstance(obj, dict):
out = {}
for k, v in obj.items():
if not isinstance(k, str):
raise CanonicalError("non-string key: %r" % (k,))
out[k] = _normalize(v)
return out
raise CanonicalError("unsupported type: %r" % (type(obj),))
def canonical(obj) -> str:
n = _normalize(obj)
return json.dumps(n, sort_keys=True, separators=(",", ":"), ensure_ascii=False,
allow_nan=False)
def canonical_bytes(obj) -> bytes:
return canonical(obj).encode("utf-8")