๐Ÿฆ€ Functional Rust

956: JSON Pretty Print

Difficulty: Intermediate Category: Serialization / Recursion Concept: Recursive pretty-printing of a recursive data structure with indentation Key Insight: OCaml uses `Buffer.add_string` for mutable accumulation; Rust builds `String` directly using `format!` and `join` โ€” both recurse on the same enum/type structure
// 956: JSON Pretty Print
// Recursive pretty-printer: OCaml uses Buffer, Rust builds String

#[derive(Debug, Clone, PartialEq)]
pub enum JsonValue {
    Null,
    Bool(bool),
    Number(f64),
    Str(String),
    Array(Vec<JsonValue>),
    Object(Vec<(String, JsonValue)>),
}

// Approach 1: Pretty-print with indentation (recursive, builds String)
fn escape_string(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    for c in s.chars() {
        match c {
            '"' => out.push_str("\\\""),
            '\\' => out.push_str("\\\\"),
            '\n' => out.push_str("\\n"),
            '\t' => out.push_str("\\t"),
            '\r' => out.push_str("\\r"),
            c => out.push(c),
        }
    }
    out
}

fn pretty_print(j: &JsonValue, indent: usize) -> String {
    let pad = " ".repeat(indent * 2);
    let pad2 = " ".repeat((indent + 1) * 2);
    match j {
        JsonValue::Null => "null".to_string(),
        JsonValue::Bool(true) => "true".to_string(),
        JsonValue::Bool(false) => "false".to_string(),
        JsonValue::Number(n) => {
            if n.fract() == 0.0 && n.is_finite() {
                format!("{}", *n as i64)
            } else {
                format!("{}", n)
            }
        }
        JsonValue::Str(s) => format!("\"{}\"", escape_string(s)),
        JsonValue::Array(items) if items.is_empty() => "[]".to_string(),
        JsonValue::Array(items) => {
            let inner: Vec<String> = items
                .iter()
                .map(|item| format!("{}{}", pad2, pretty_print(item, indent + 1)))
                .collect();
            format!("[\n{}\n{}]", inner.join(",\n"), pad)
        }
        JsonValue::Object(pairs) if pairs.is_empty() => "{}".to_string(),
        JsonValue::Object(pairs) => {
            let inner: Vec<String> = pairs
                .iter()
                .map(|(k, v)| {
                    format!(
                        "{}\"{}\": {}",
                        pad2,
                        escape_string(k),
                        pretty_print(v, indent + 1)
                    )
                })
                .collect();
            format!("{{\n{}\n{}}}", inner.join(",\n"), pad)
        }
    }
}

// Approach 2: Compact (single-line) printer
fn compact(j: &JsonValue) -> String {
    match j {
        JsonValue::Null => "null".to_string(),
        JsonValue::Bool(b) => b.to_string(),
        JsonValue::Number(n) => {
            if n.fract() == 0.0 && n.is_finite() {
                format!("{}", *n as i64)
            } else {
                format!("{}", n)
            }
        }
        JsonValue::Str(s) => format!("\"{}\"", escape_string(s)),
        JsonValue::Array(items) => {
            let inner: Vec<String> = items.iter().map(compact).collect();
            format!("[{}]", inner.join(","))
        }
        JsonValue::Object(pairs) => {
            let inner: Vec<String> = pairs
                .iter()
                .map(|(k, v)| format!("\"{}\":{}", escape_string(k), compact(v)))
                .collect();
            format!("{{{}}}", inner.join(","))
        }
    }
}

fn main() {
    let json = JsonValue::Object(vec![
        ("name".to_string(), JsonValue::Str("Alice".to_string())),
        ("age".to_string(), JsonValue::Number(30.0)),
        (
            "scores".to_string(),
            JsonValue::Array(vec![
                JsonValue::Number(95.0),
                JsonValue::Number(87.0),
                JsonValue::Number(92.0),
            ]),
        ),
        (
            "address".to_string(),
            JsonValue::Object(vec![
                ("city".to_string(), JsonValue::Str("Amsterdam".to_string())),
                ("zip".to_string(), JsonValue::Str("1234AB".to_string())),
            ]),
        ),
        ("active".to_string(), JsonValue::Bool(true)),
        ("note".to_string(), JsonValue::Null),
    ]);

    println!("=== Pretty ===");
    println!("{}", pretty_print(&json, 0));
    println!("\n=== Compact ===");
    println!("{}", compact(&json));
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_primitives() {
        assert_eq!(pretty_print(&JsonValue::Null, 0), "null");
        assert_eq!(pretty_print(&JsonValue::Bool(true), 0), "true");
        assert_eq!(pretty_print(&JsonValue::Bool(false), 0), "false");
        assert_eq!(pretty_print(&JsonValue::Number(42.0), 0), "42");
        assert_eq!(pretty_print(&JsonValue::Str("hi".into()), 0), "\"hi\"");
    }

    #[test]
    fn test_escape() {
        let s = JsonValue::Str("hello \"world\"\nnewline".to_string());
        assert_eq!(pretty_print(&s, 0), "\"hello \\\"world\\\"\\nnewline\"");
    }

    #[test]
    fn test_empty_array_object() {
        assert_eq!(pretty_print(&JsonValue::Array(vec![]), 0), "[]");
        assert_eq!(pretty_print(&JsonValue::Object(vec![]), 0), "{}");
    }

    #[test]
    fn test_compact_no_newlines() {
        let json = JsonValue::Object(vec![
            ("a".to_string(), JsonValue::Number(1.0)),
            ("b".to_string(), JsonValue::Bool(false)),
        ]);
        let c = compact(&json);
        assert!(!c.contains('\n'));
        assert!(c.contains("\"a\":1"));
        assert!(c.contains("\"b\":false"));
    }

    #[test]
    fn test_nested_pretty() {
        let json = JsonValue::Array(vec![
            JsonValue::Number(1.0),
            JsonValue::Array(vec![JsonValue::Number(2.0), JsonValue::Number(3.0)]),
        ]);
        let p = pretty_print(&json, 0);
        assert!(p.contains('\n'));
        assert!(p.starts_with('['));
        assert!(p.ends_with(']'));
    }
}
(* 956: JSON Pretty Print *)

type json =
  | Null
  | Bool of bool
  | Number of float
  | Str of string
  | Array of json list
  | Object of (string * json) list

(* Approach 1: Simple recursive pretty-printer with indentation *)

let escape_string s =
  let buf = Buffer.create (String.length s + 2) in
  String.iter (fun c ->
    match c with
    | '"' -> Buffer.add_string buf "\\\""
    | '\\' -> Buffer.add_string buf "\\\\"
    | '\n' -> Buffer.add_string buf "\\n"
    | '\t' -> Buffer.add_string buf "\\t"
    | '\r' -> Buffer.add_string buf "\\r"
    | c -> Buffer.add_char buf c
  ) s;
  Buffer.contents buf

let rec pretty_print ?(indent=0) j =
  let pad = String.make (indent * 2) ' ' in
  let pad2 = String.make ((indent + 1) * 2) ' ' in
  match j with
  | Null -> "null"
  | Bool true -> "true"
  | Bool false -> "false"
  | Number n ->
    if Float.is_integer n then string_of_int (int_of_float n)
    else Printf.sprintf "%g" n
  | Str s -> Printf.sprintf "\"%s\"" (escape_string s)
  | Array [] -> "[]"
  | Array items ->
    let items_str = List.map (fun item ->
      pad2 ^ pretty_print ~indent:(indent+1) item
    ) items in
    "[\n" ^ String.concat ",\n" items_str ^ "\n" ^ pad ^ "]"
  | Object [] -> "{}"
  | Object pairs ->
    let pairs_str = List.map (fun (k, v) ->
      pad2 ^ Printf.sprintf "\"%s\": %s" (escape_string k) (pretty_print ~indent:(indent+1) v)
    ) pairs in
    "{\n" ^ String.concat ",\n" pairs_str ^ "\n" ^ pad ^ "}"

(* Approach 2: Compact (single-line) printer *)

let rec compact j =
  match j with
  | Null -> "null"
  | Bool true -> "true"
  | Bool false -> "false"
  | Number n ->
    if Float.is_integer n then string_of_int (int_of_float n)
    else Printf.sprintf "%g" n
  | Str s -> Printf.sprintf "\"%s\"" (escape_string s)
  | Array items ->
    "[" ^ String.concat "," (List.map compact items) ^ "]"
  | Object pairs ->
    "{" ^ String.concat "," (List.map (fun (k,v) ->
      Printf.sprintf "\"%s\":%s" k (compact v)
    ) pairs) ^ "}"

let () =
  let json = Object [
    ("name", Str "Alice");
    ("age", Number 30.0);
    ("scores", Array [Number 95.0; Number 87.0; Number 92.0]);
    ("address", Object [("city", Str "Amsterdam"); ("zip", Str "1234AB")]);
    ("active", Bool true);
    ("note", Null);
  ] in

  let pretty = pretty_print json in
  assert (String.length pretty > 0);
  assert (String.sub pretty 0 1 = "{");

  let c = compact json in
  assert (String.length c > 0);
  assert (not (String.contains c '\n'));

  (* Test escape *)
  let with_special = Str "hello \"world\"\nnewline" in
  let escaped = pretty_print with_special in
  assert (escaped = "\"hello \\\"world\\\"\\nnewline\"");

  Printf.printf "%s\n" pretty;
  Printf.printf "โœ“ All tests passed\n"

๐Ÿ“Š Detailed Comparison

JSON Pretty Print โ€” Comparison

Core Insight

Pretty-printing is a classic recursive problem. Both languages follow the same algorithm: recurse into nested structures, track indentation depth, and concatenate output. OCaml tends toward `Buffer` for efficiency; Rust's `String::with_capacity` + `format!` + `Vec::join` achieves the same result idiomatically.

OCaml Approach

  • Optional labeled argument `?(indent=0)` gives default indentation cleanly
  • `String.make n ' '` builds padding strings
  • `String.concat` joins lists of strings with separator
  • `Buffer` used for escape_string to avoid O(nยฒ) string concatenation
  • Pattern matching handles empty vs non-empty arrays/objects separately

Rust Approach

  • Plain `usize` parameter for indent (no default args โ€” use wrapper fn if needed)
  • `" ".repeat(n)` builds padding strings
  • `.collect::<Vec<String>>()` then `.join(",\n")` mirrors `String.concat`
  • `format!` for string interpolation instead of `Printf.sprintf`
  • `if items.is_empty()` guard before match arm (or use pattern guard)

Comparison Table

AspectOCamlRust
String building`Buffer.add_string` / `^``String::push_str` / `format!`
Joining list`String.concat sep list``vec.join(sep)`
Default args`?(indent=0)`No default args โ€” overload or wrapper
Padding`String.make n ' '``" ".repeat(n)`
Char escaping`String.iter` + `match c``for c in s.chars()` + `match c`
Empty collectionPattern `Array []`Pattern guard `if items.is_empty()`
Float formatting`Printf.sprintf "%g"``format!("{}", n)`