use std::fmt::Write;
pub fn number_to_string(m: f64) -> String {
if m.is_nan() {
return "NaN".to_string();
}
if m == 0.0 {
return "0".to_string();
}
if m == f64::INFINITY {
return "Infinity".to_string();
}
if m == f64::NEG_INFINITY {
return "-Infinity".to_string();
}
let mut out = String::new();
if m < 0.0 {
out.push('-');
}
let sci = format!("{:e}", m.abs());
let (mantissa, exp_str) = sci.split_once('e').expect("`{:e}` always has 'e'");
let e: i32 = exp_str.parse().expect("valid exponent");
let digits: Vec<u8> = mantissa.bytes().filter(|&b| b != b'.').collect();
let k = digits.len() as i32;
let n = e + 1;
if (-5..=21).contains(&n) {
if n >= k {
for &d in &digits {
out.push(d as char);
}
for _ in 0..(n - k) {
out.push('0');
}
} else if n > 0 {
for i in 0..n {
out.push(digits[i as usize] as char);
}
out.push('.');
for i in n..k {
out.push(digits[i as usize] as char);
}
} else {
out.push('0');
out.push('.');
for _ in 0..(-n) {
out.push('0');
}
for &d in &digits {
out.push(d as char);
}
}
} else {
let exponent_sign = if n < 0 { '-' } else { '+' };
let exp_val = (n - 1).unsigned_abs();
out.push(digits[0] as char);
if k != 1 {
out.push('.');
for i in 1..k {
out.push(digits[i as usize] as char);
}
}
out.push('e');
out.push(exponent_sign);
out.push_str(&exp_val.to_string());
}
out
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum StateType {
Dict,
Array,
}
#[derive(Debug)]
struct State {
ty: StateType,
needs_comma: bool,
needs_key: bool,
needs_value: bool,
is_empty: bool,
}
impl State {
fn new(ty: StateType) -> State {
State {
ty,
needs_comma: false,
needs_key: ty == StateType::Dict,
needs_value: false,
is_empty: true,
}
}
}
pub struct JSONEmitter<'w> {
out: &'w mut String,
pretty: bool,
indent: u32,
states: Vec<State>,
}
impl<'w> JSONEmitter<'w> {
pub fn new(out: &'w mut String, pretty: bool) -> JSONEmitter<'w> {
JSONEmitter { out, pretty, indent: 0, states: Vec::new() }
}
fn in_dict(&self) -> bool {
matches!(self.states.last(), Some(s) if s.ty == StateType::Dict)
}
fn in_array(&self) -> bool {
matches!(self.states.last(), Some(s) if s.ty == StateType::Array)
}
fn will_emit_value(&mut self) {
if self.states.is_empty() {
return;
}
let is_array;
{
let state = self.states.last_mut().unwrap();
debug_assert!(!state.needs_key, "Expected a key");
if state.needs_comma {
self.out.push(',');
}
state.needs_key = state.ty == StateType::Dict;
state.needs_comma = true;
state.needs_value = false;
state.is_empty = false;
is_array = state.ty == StateType::Array;
}
if is_array {
self.pretty_new_line();
}
}
pub fn emit_bool(&mut self, val: bool) {
self.will_emit_value();
self.out.push_str(if val { "true" } else { "false" });
}
pub fn emit_i64(&mut self, val: i64) {
self.will_emit_value();
let _ = write!(self.out, "{val}");
}
pub fn emit_u64(&mut self, val: u64) {
self.will_emit_value();
let _ = write!(self.out, "{val}");
}
pub fn emit_f64(&mut self, val: f64) {
self.will_emit_value();
if val.is_finite() {
self.out.push_str(&number_to_string(val));
} else {
self.out.push_str("null");
}
}
pub fn emit_str(&mut self, val: &str) {
self.will_emit_value();
self.primitive_emit_string(val);
}
pub fn emit_u16(&mut self, val: &[u16]) {
self.will_emit_value();
self.out.push('"');
for &curr in val {
self.emit_one_escaped_unit(curr);
}
self.out.push('"');
}
pub fn emit_null_value(&mut self) {
self.will_emit_value();
self.out.push_str("null");
}
pub fn emit_key_u16(&mut self, key: &[u16]) {
debug_assert!(self.in_dict(), "Not emitting a dictionary");
{
let state = self.states.last_mut().unwrap();
debug_assert!(state.needs_key, "Not expecting a key");
debug_assert!(!state.needs_value, "Missing a value for a key.");
if state.needs_comma {
self.out.push(',');
}
state.needs_comma = false;
state.needs_key = false;
state.needs_value = true;
}
self.pretty_new_line();
self.out.push('"');
for &unit in key {
self.emit_one_escaped_unit(unit);
}
self.out.push('"');
self.out.push(':');
if self.pretty {
self.out.push(' ');
}
}
pub fn emit_key(&mut self, key: &str) {
debug_assert!(self.in_dict(), "Not emitting a dictionary");
{
let state = self.states.last_mut().unwrap();
debug_assert!(state.needs_key, "Not expecting a key");
debug_assert!(!state.needs_value, "Missing a value for a key.");
if state.needs_comma {
self.out.push(',');
}
state.needs_comma = false;
state.needs_key = false;
state.needs_value = true;
}
self.pretty_new_line();
self.primitive_emit_string(key);
self.out.push(':');
if self.pretty {
self.out.push(' ');
}
}
pub fn open_dict(&mut self) {
self.will_emit_value();
self.out.push('{');
self.indent_more();
self.states.push(State::new(StateType::Dict));
}
pub fn close_dict(&mut self) {
debug_assert!(self.in_dict(), "Not currently emitting a dictionary");
debug_assert!(!self.states.last().unwrap().needs_value, "Missing a value for a key.");
self.indent_less();
if !self.states.last().unwrap().is_empty {
self.pretty_new_line();
}
self.out.push('}');
self.states.pop();
}
pub fn open_array(&mut self) {
self.will_emit_value();
self.indent_more();
self.out.push('[');
self.states.push(State::new(StateType::Array));
}
pub fn close_array(&mut self) {
debug_assert!(self.in_array(), "Not currently emitting an array");
self.indent_less();
if !self.states.last().unwrap().is_empty {
self.pretty_new_line();
}
self.out.push(']');
self.states.pop();
}
pub fn end_jsonl(&mut self) {
debug_assert!(self.states.is_empty(), "Previous object was not terminated.");
self.out.push('\n');
}
fn primitive_emit_string(&mut self, s: &str) {
self.out.push('"');
for ch in s.chars() {
let cp = ch as u32;
if cp > 0x7F {
if cp <= 0xFFFF {
self.write_u_escape(cp as u16);
} else {
let c = cp - 0x10000;
self.write_u_escape(0xD800 + (c >> 10) as u16);
self.write_u_escape(0xDC00 + (c & 0x3FF) as u16);
}
continue;
}
if cp == 0x22 || cp == 0x5C || cp == 0x2F {
self.out.push('\\');
}
if cp >= 0x20 {
self.out.push(cp as u8 as char);
continue;
}
match cp {
0x08 => self.out.push_str("\\b"),
0x0C => self.out.push_str("\\f"),
0x0A => self.out.push_str("\\n"),
0x0D => self.out.push_str("\\r"),
0x09 => self.out.push_str("\\t"),
_ => self.write_u_escape(cp as u16),
}
}
self.out.push('"');
}
fn emit_one_escaped_unit(&mut self, curr: u16) {
let c = curr as u32;
if c > 0x7F {
self.write_u_escape(curr);
return;
}
if c >= 0x20 {
if c == 0x22 || c == 0x5C || c == 0x2F {
self.out.push('\\');
}
self.out.push(c as u8 as char);
return;
}
match c {
0x08 => self.out.push_str("\\b"),
0x0C => self.out.push_str("\\f"),
0x0A => self.out.push_str("\\n"),
0x0D => self.out.push_str("\\r"),
0x09 => self.out.push_str("\\t"),
_ => self.write_u_escape(curr),
}
}
fn write_u_escape(&mut self, u: u16) {
let _ = write!(self.out, "\\u{u:04x}");
}
fn pretty_new_line(&mut self) {
if !self.pretty {
return;
}
self.out.push('\n');
for _ in 0..self.indent {
self.out.push(' ');
}
}
fn indent_more(&mut self) {
if self.pretty {
self.indent += 2;
}
}
fn indent_less(&mut self) {
if self.pretty {
debug_assert!(self.indent >= 2, "Unbalanced indentation.");
self.indent -= 2;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn emit<F: FnOnce(&mut JSONEmitter)>(f: F) -> String {
let mut s = String::new();
{
let mut j = JSONEmitter::new(&mut s, false);
f(&mut j);
}
s
}
#[test]
fn empty_array() {
assert_eq!(emit(|j| { j.open_array(); j.close_array(); }), "[]");
}
#[test]
fn empty_dict() {
assert_eq!(emit(|j| { j.open_dict(); j.close_dict(); }), "{}");
}
#[test]
fn sample() {
let s = emit(|j| {
j.open_dict();
j.emit_key("name"); j.emit_str("hermes");
j.emit_key("age"); j.emit_i64(2);
j.emit_key("hot"); j.emit_bool(true);
j.emit_key("cold"); j.emit_bool(false);
j.emit_key("tags");
j.open_array();
j.emit_str("small"); j.emit_str("light");
j.close_array();
j.close_dict();
});
assert_eq!(s, r#"{"name":"hermes","age":2,"hot":true,"cold":false,"tags":["small","light"]}"#);
}
#[test]
fn smoke_with_double_and_escapes() {
let s = emit(|j| {
j.open_dict();
j.emit_key("a"); j.emit_i64(123);
j.emit_key("b"); j.emit_f64(456.7);
j.emit_key("dict1");
j.open_dict();
j.emit_key("dict1_arr1");
j.open_array();
j.emit_str("val1"); j.emit_str("val2"); j.emit_str("val3");
j.close_array();
j.emit_key("dict1_empty"); j.open_dict(); j.close_dict();
j.emit_key("dict1_empty2"); j.open_array(); j.close_array();
j.emit_key("str1"); j.emit_str("\"ABC\u{8}DEF\\");
j.close_dict();
j.close_dict();
});
assert_eq!(s, r#"{"a":123,"b":456.7,"dict1":{"dict1_arr1":["val1","val2","val3"],"dict1_empty":{},"dict1_empty2":[],"str1":"\"ABC\bDEF\\"}}"#);
}
#[test]
fn escapes() {
let s = emit(|j| j.emit_str("x\"\\/\u{8}\u{c}\n\r\tx"));
assert_eq!(s, r#""x\"\\\/\b\f\n\r\tx""#);
}
#[test]
fn forward_slashes() {
let s = emit(|j| {
j.open_dict();
j.emit_key("url"); j.emit_str("http://www.example.com");
j.close_dict();
});
assert_eq!(s, r#"{"url":"http:\/\/www.example.com"}"#);
}
#[test]
fn non_ascii_and_astral() {
let s = emit(|j| {
j.open_dict();
j.emit_key("ha"); j.emit_str("\u{54C8}");
j.emit_key("gClef"); j.emit_str("\u{1D11E}");
j.emit_key("wave"); j.emit_str("hi\u{1F44B}");
j.close_dict();
});
assert_eq!(s, r#"{"ha":"\u54c8","gClef":"\ud834\udd1e","wave":"hi\ud83d\udc4b"}"#);
}
#[test]
fn non_finite_is_null() {
let s = emit(|j| {
j.open_array();
j.emit_f64(f64::INFINITY); j.emit_f64(f64::NEG_INFINITY); j.emit_f64(f64::NAN);
j.close_array();
});
assert_eq!(s, "[null,null,null]");
}
#[test]
fn null_value() {
assert_eq!(emit(|j| j.emit_null_value()), "null");
}
#[test]
fn jsonl() {
let mut s = String::new();
{
let mut j = JSONEmitter::new(&mut s, false);
j.open_dict(); j.close_dict(); j.end_jsonl();
j.open_dict(); j.close_dict(); j.end_jsonl();
}
assert_eq!(s, "{}\n{}\n");
}
#[test]
fn emit_utf16() {
let units: Vec<u16> = vec![b'h' as u16, b'i' as u16, 0xd83d, 0xdc4b];
let mut s = String::new();
{
let mut j = JSONEmitter::new(&mut s, false);
j.open_dict();
j.emit_key("str"); j.emit_u16(&units);
j.close_dict();
}
assert_eq!(s, r#"{"str":"hi\ud83d\udc4b"}"#);
}
#[test]
fn pretty_print() {
let mut s = String::new();
{
let mut j = JSONEmitter::new(&mut s, true);
j.open_dict();
j.emit_key("artist"); j.emit_str("prince");
j.emit_key("instruments");
j.open_array();
j.emit_str("piano");
j.open_dict();
j.emit_key("guitars");
j.open_array();
j.emit_str("cloud"); j.emit_str("love symbol"); j.emit_str("telecaster");
j.close_array();
j.close_dict();
j.emit_str("drums");
j.close_array();
j.emit_key("songs");
j.open_dict();
j.emit_key("purple rain"); j.emit_i64(1984);
j.emit_key("1999"); j.emit_i64(1982);
j.close_dict();
j.emit_key("color"); j.emit_str("purple");
j.emit_key("emptyDict"); j.open_dict(); j.close_dict();
j.emit_key("emptyArray"); j.open_array(); j.close_array();
j.close_dict();
}
let expected = "{\n \"artist\": \"prince\",\n \"instruments\": [\n \"piano\",\n {\n \"guitars\": [\n \"cloud\",\n \"love symbol\",\n \"telecaster\"\n ]\n },\n \"drums\"\n ],\n \"songs\": {\n \"purple rain\": 1984,\n \"1999\": 1982\n },\n \"color\": \"purple\",\n \"emptyDict\": {},\n \"emptyArray\": []\n}";
assert_eq!(s, expected);
}
#[test]
fn emit_u16_astral_and_lone_surrogate() {
let mut s = String::new();
{
let mut j = JSONEmitter::new(&mut s, false);
j.open_dict();
j.emit_key_u16(&[0xD800, 0xDC00]); j.emit_u16(&[0xD800]); j.close_dict();
}
assert_eq!(s, "{\"\\ud800\\udc00\":\"\\ud800\"}");
}
#[test]
fn number_to_string_matches_ecmascript() {
let cases: &[(f64, &str)] = &[
(0.0, "0"),
(-0.0, "0"),
(1.0, "1"),
(-1.0, "-1"),
(456.7, "456.7"),
(100.0, "100"),
(0.1, "0.1"),
(0.0001, "0.0001"), (1e-6, "0.000001"), (1e-7, "1e-7"), (1e20, "100000000000000000000"), (1e21, "1e+21"), (123.45, "123.45"),
(5e-324, "5e-324"), (f64::NAN, "NaN"),
(f64::INFINITY, "Infinity"),
(f64::NEG_INFINITY, "-Infinity"),
];
for &(v, expected) in cases {
assert_eq!(number_to_string(v), expected, "for {v:?}");
}
}
}