use crate::renderable::{RenderableTreeNode, RenderableTreeNodes, Scalar};
const OBJECT: &str = "[object Object]";
#[must_use]
pub(crate) fn string_nodes(value: &RenderableTreeNodes) -> String {
match value {
RenderableTreeNodes::One(RenderableTreeNode::Scalar(scalar)) => string(scalar),
RenderableTreeNodes::One(RenderableTreeNode::Tag(_)) => OBJECT.to_owned(),
RenderableTreeNodes::Many(nodes) => join_nodes(nodes),
}
}
fn join_nodes(nodes: &[RenderableTreeNode]) -> String {
let mut out = String::new();
for (index, node) in nodes.iter().enumerate() {
if index > 0 {
out.push(',');
}
match node {
RenderableTreeNode::Scalar(Scalar::Null) => {}
RenderableTreeNode::Scalar(Scalar::Array(nested)) => out.push_str(&join(nested)),
RenderableTreeNode::Scalar(scalar) => out.push_str(&string(scalar)),
RenderableTreeNode::Tag(_) => out.push_str(OBJECT),
}
}
out
}
#[must_use]
pub(crate) fn string(value: &Scalar) -> String {
match value {
Scalar::Null => "null".to_owned(),
Scalar::Boolean(true) => "true".to_owned(),
Scalar::Boolean(false) => "false".to_owned(),
Scalar::Number(n) => number(*n),
Scalar::String(s) => s.clone(),
Scalar::Array(items) => join(items),
Scalar::Object(_) => OBJECT.to_owned(),
}
}
enum Step<'a> {
Element(&'a Scalar),
Comma,
}
fn join(items: &[Scalar]) -> String {
let mut out = String::new();
let mut stack: Vec<Step<'_>> = Vec::new();
push_elements(&mut stack, items);
while let Some(step) = stack.pop() {
match step {
Step::Comma => out.push(','),
Step::Element(Scalar::Null) => {}
Step::Element(Scalar::Array(nested)) => push_elements(&mut stack, nested),
Step::Element(leaf) => out.push_str(&string(leaf)),
}
}
out
}
fn push_elements<'a>(stack: &mut Vec<Step<'a>>, items: &'a [Scalar]) {
for (index, item) in items.iter().enumerate().rev() {
stack.push(Step::Element(item));
if index > 0 {
stack.push(Step::Comma);
}
}
}
#[must_use]
pub(crate) fn number(value: f64) -> String {
if value.is_nan() {
return "NaN".to_owned();
}
if value == 0.0 {
return "0".to_owned();
}
if value < 0.0 {
return format!("-{}", number(-value));
}
if value.is_infinite() {
return "Infinity".to_owned();
}
let scientific = format!("{value:e}");
let Some((mantissa, exponent)) = scientific.split_once('e') else {
return format!("{value}");
};
let Ok(exponent) = exponent.parse::<i32>() else {
return format!("{value}");
};
let digits: String = mantissa.chars().filter(|ch| *ch != '.').collect();
let Ok(k) = i32::try_from(digits.len()) else {
return format!("{value}");
};
let n = exponent + 1;
if (k..=21).contains(&n) {
let Ok(padding) = usize::try_from(n - k) else {
return format!("{value}");
};
return digits + &"0".repeat(padding);
}
if (1..=21).contains(&n) {
let Ok(point) = usize::try_from(n) else {
return format!("{value}");
};
let Some((whole, fraction)) = digits.split_at_checked(point) else {
return format!("{value}");
};
return format!("{whole}.{fraction}");
}
if (-5..=0).contains(&n) {
let Ok(zeros) = usize::try_from(-n) else {
return format!("{value}");
};
return format!("0.{}{digits}", "0".repeat(zeros));
}
let power = n - 1;
let sign = if power < 0 { '-' } else { '+' };
let magnitude = power.unsigned_abs();
if k == 1 {
return format!("{digits}e{sign}{magnitude}");
}
let Some((first, rest)) = digits.split_at_checked(1) else {
return format!("{value}");
};
format!("{first}.{rest}e{sign}{magnitude}")
}
#[cfg(test)]
mod tests {
use super::*;
use indexmap::IndexMap;
#[test]
fn integers_print_without_a_decimal_point() {
assert_eq!(number(0.0), "0");
assert_eq!(number(1.0), "1");
assert_eq!(number(2.0), "2");
assert_eq!(number(42.0), "42");
assert_eq!(number(100.0), "100");
assert_eq!(number(1234.0), "1234");
}
#[test]
fn negative_zero_loses_its_sign() {
assert_eq!(number(-0.0), "0");
}
#[test]
fn negatives_carry_one_minus() {
assert_eq!(number(-1.0), "-1");
assert_eq!(number(-1.5), "-1.5");
assert_eq!(number(-0.1), "-0.1");
}
#[test]
fn fractions_print_their_shortest_round_trip() {
assert_eq!(number(1.5), "1.5");
assert_eq!(number(0.1), "0.1");
assert_eq!(number(123.456), "123.456");
assert_eq!(number(0.300_000_000_000_000_04), "0.30000000000000004");
}
#[test]
fn the_upper_threshold_is_1e21_and_it_is_exclusive() {
assert_eq!(number(1e20), "100000000000000000000");
assert_eq!(number(1e21), "1e+21");
assert_eq!(number(1.5e21), "1.5e+21");
assert_eq!(number(1e100), "1e+100");
}
#[test]
fn the_lower_threshold_is_1e_minus_6_and_it_is_inclusive() {
assert_eq!(number(1e-6), "0.000001");
assert_eq!(number(1.5e-6), "0.0000015");
assert_eq!(number(1e-7), "1e-7");
assert_eq!(number(1.5e-7), "1.5e-7");
assert_eq!(number(5e-324), "5e-324");
}
#[test]
fn non_finite_values_use_javascript_names() {
assert_eq!(number(f64::NAN), "NaN");
assert_eq!(number(f64::INFINITY), "Infinity");
assert_eq!(number(f64::NEG_INFINITY), "-Infinity");
}
#[test]
fn the_extremes_of_f64_match_javascript() {
assert_eq!(number(f64::MAX), "1.7976931348623157e+308");
assert_eq!(number(f64::MIN_POSITIVE), "2.2250738585072014e-308");
assert_eq!(number(9_007_199_254_740_991.0), "9007199254740991");
}
#[test]
fn scalars_coerce_the_way_string_does() {
assert_eq!(string(&Scalar::Null), "null");
assert_eq!(string(&Scalar::Boolean(true)), "true");
assert_eq!(string(&Scalar::Boolean(false)), "false");
assert_eq!(string(&Scalar::Number(42.0)), "42");
assert_eq!(string(&Scalar::String("hi".to_owned())), "hi");
}
#[test]
fn an_object_is_the_useless_string_upstream_writes() {
let mut object = IndexMap::new();
object.insert("foo".to_owned(), Scalar::String("bar".to_owned()));
assert_eq!(string(&Scalar::Object(object)), "[object Object]");
}
#[test]
fn an_array_joins_with_commas() {
let array = Scalar::Array(vec![
Scalar::Number(1.0),
Scalar::Number(2.0),
Scalar::Number(3.0),
]);
assert_eq!(string(&array), "1,2,3");
}
#[test]
fn an_empty_array_is_the_empty_string() {
assert_eq!(string(&Scalar::Array(vec![])), "");
assert_eq!(string(&Scalar::Array(vec![Scalar::Array(vec![])])), "");
}
#[test]
fn null_inside_an_array_contributes_nothing() {
assert_eq!(string(&Scalar::Null), "null");
assert_eq!(string(&Scalar::Array(vec![Scalar::Null])), "");
assert_eq!(
string(&Scalar::Array(vec![
Scalar::Null,
Scalar::Number(1.0),
Scalar::Null
])),
",1,"
);
}
#[test]
fn nested_arrays_flatten() {
let array = Scalar::Array(vec![
Scalar::Number(1.0),
Scalar::Array(vec![Scalar::Number(2.0), Scalar::Number(3.0)]),
Scalar::Number(4.0),
]);
assert_eq!(string(&array), "1,2,3,4");
}
#[test]
fn an_object_inside_an_array_is_still_useless() {
let array = Scalar::Array(vec![
Scalar::String("a".to_owned()),
Scalar::Object(IndexMap::new()),
]);
assert_eq!(string(&array), "a,[object Object]");
}
#[test]
fn a_deeply_nested_array_does_not_overflow_the_stack() {
let mut value = Scalar::Array(vec![Scalar::Number(1.0)]);
for _ in 0..100_000 {
value = Scalar::Array(vec![value]);
}
assert_eq!(string(&value), "1");
}
}