๐Ÿฆ€ Functional Rust

959: CSV Writer

Difficulty: Beginner Category: Serialization / String Processing Concept: Escaping special characters and producing valid RFC 4180 CSV output Key Insight: The escaping algorithm is identical in both languages โ€” wrap fields containing commas/quotes/newlines in double-quotes, and double any embedded quotes; Rust's `String::push` + iterator mirrors OCaml's `Buffer.add_char` + `String.iter`
// 959: CSV Writer
// Escape quotes, handle commas/newlines in fields, produce valid CSV output

// Approach 1: Escape a single field
pub fn needs_quoting(s: &str) -> bool {
    s.contains(',') || s.contains('"') || s.contains('\n') || s.contains('\r')
}

pub fn escape_field(s: &str) -> String {
    if needs_quoting(s) {
        let mut out = String::with_capacity(s.len() + 2);
        out.push('"');
        for c in s.chars() {
            if c == '"' {
                out.push('"');
                out.push('"');
            } else {
                out.push(c);
            }
        }
        out.push('"');
        out
    } else {
        s.to_string()
    }
}

// Approach 2: Write a single row
pub fn write_row(fields: &[&str]) -> String {
    fields
        .iter()
        .map(|f| escape_field(f))
        .collect::<Vec<_>>()
        .join(",")
}

pub fn write_row_owned(fields: &[String]) -> String {
    fields
        .iter()
        .map(|f| escape_field(f))
        .collect::<Vec<_>>()
        .join(",")
}

// Approach 3: Write complete CSV (rows of string slices)
pub fn write_csv(rows: &[Vec<&str>]) -> String {
    rows.iter()
        .map(|row| write_row(row))
        .collect::<Vec<_>>()
        .join("\n")
}

fn main() {
    println!("simple:    {}", escape_field("hello"));
    println!("comma:     {}", escape_field("one, two"));
    println!("quoted:    {}", escape_field("say \"hi\""));
    println!("newline:   {}", escape_field("line1\nline2").replace('\n', "\\n"));

    let rows = vec![
        vec!["name", "age", "city"],
        vec!["Alice, Smith", "30", "Amsterdam"],
        vec!["Bob", "25", "say \"hi\""],
    ];
    println!("\nCSV output:");
    println!("{}", write_csv(&rows));
}

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

    #[test]
    fn test_no_quoting_needed() {
        assert_eq!(escape_field("hello"), "hello");
        assert_eq!(escape_field("42"), "42");
        assert_eq!(escape_field(""), "");
    }

    #[test]
    fn test_comma_quoting() {
        assert_eq!(escape_field("one, two"), "\"one, two\"");
    }

    #[test]
    fn test_quote_escaping() {
        assert_eq!(escape_field("say \"hi\""), "\"say \"\"hi\"\"\"");
    }

    #[test]
    fn test_newline_quoting() {
        assert_eq!(escape_field("line1\nline2"), "\"line1\nline2\"");
    }

    #[test]
    fn test_write_row_plain() {
        assert_eq!(write_row(&["name", "age", "city"]), "name,age,city");
    }

    #[test]
    fn test_write_row_with_special() {
        assert_eq!(
            write_row(&["Alice, Smith", "30", "Amsterdam"]),
            "\"Alice, Smith\",30,Amsterdam"
        );
    }

    #[test]
    fn test_write_csv() {
        let rows = vec![
            vec!["name", "age", "city"],
            vec!["Alice, Smith", "30", "Amsterdam"],
            vec!["Bob", "25", "say \"hi\""],
        ];
        let csv = write_csv(&rows);
        let lines: Vec<&str> = csv.lines().collect();
        assert_eq!(lines.len(), 3);
        assert_eq!(lines[0], "name,age,city");
        assert_eq!(lines[1], "\"Alice, Smith\",30,Amsterdam");
        assert_eq!(lines[2], "Bob,25,\"say \"\"hi\"\"\"");
    }
}
(* 959: CSV Writer *)
(* Escape quotes and handle special characters *)

(* Approach 1: Escape a single field *)

let needs_quoting s =
  String.contains s ',' ||
  String.contains s '"' ||
  String.contains s '\n' ||
  String.contains s '\r'

let escape_field s =
  if needs_quoting s then begin
    (* Replace " with "" and wrap in quotes *)
    let buf = Buffer.create (String.length s + 2) in
    Buffer.add_char buf '"';
    String.iter (fun c ->
      if c = '"' then Buffer.add_string buf "\"\""
      else Buffer.add_char buf c
    ) s;
    Buffer.add_char buf '"';
    Buffer.contents buf
  end else
    s

(* Approach 2: Write a row *)

let write_row fields =
  String.concat "," (List.map escape_field fields)

(* Approach 3: Write a complete CSV *)

let write_csv rows =
  String.concat "\n" (List.map write_row rows)

let () =
  (* Simple field - no quoting needed *)
  assert (escape_field "hello" = "hello");
  assert (escape_field "42" = "42");

  (* Comma triggers quoting *)
  assert (escape_field "one, two" = "\"one, two\"");

  (* Quote gets doubled *)
  assert (escape_field "say \"hi\"" = "\"say \"\"hi\"\"\"");

  (* Newline triggers quoting *)
  assert (escape_field "line1\nline2" = "\"line1\nline2\"");

  (* Write rows *)
  assert (write_row ["name"; "age"; "city"] = "name,age,city");
  assert (write_row ["Alice, Smith"; "30"; "Amsterdam"] = "\"Alice, Smith\",30,Amsterdam");

  (* Full CSV *)
  let rows = [
    ["name"; "age"; "city"];
    ["Alice, Smith"; "30"; "Amsterdam"];
    ["Bob"; "25"; "say \"hi\""];
  ] in
  let csv = write_csv rows in
  assert (String.length csv > 0);
  let lines = String.split_on_char '\n' csv in
  assert (List.length lines = 3);
  assert (List.nth lines 0 = "name,age,city");
  assert (List.nth lines 1 = "\"Alice, Smith\",30,Amsterdam");
  assert (List.nth lines 2 = "Bob,25,\"say \"\"hi\"\"\"");

  Printf.printf "โœ“ All tests passed\n"

๐Ÿ“Š Detailed Comparison

CSV Writer โ€” Comparison

Core Insight

CSV writing is the inverse of parsing: turn structured data back into escaped text. Both languages use the same algorithm (check if quoting is needed, double embedded quotes, wrap in outer quotes). OCaml's `Buffer` is the mutable accumulator equivalent to Rust's `String::with_capacity`.

OCaml Approach

  • `String.contains s ','` โ€” check if quoting is needed
  • `Buffer.create` + `Buffer.add_char` + `Buffer.contents` for efficient building
  • `String.iter` to iterate characters
  • `String.concat "," (List.map escape_field fields)` โ€” functional pipeline for rows
  • `String.concat "\n" (List.map write_row rows)` โ€” functional pipeline for CSV

Rust Approach

  • `s.contains(',')` โ€” idiomatic contains check
  • `String::with_capacity` pre-allocates for efficiency
  • `for c in s.chars()` iterates characters (Unicode-aware)
  • `.map(|f| escape_field(f)).collect::<Vec<_>>().join(",")` โ€” functional pipeline
  • `rows.iter().map(write_row).collect::<Vec<_>>().join("\n")`

Comparison Table

AspectOCamlRust
Contains check`String.contains s ','``s.contains(',')`
String building`Buffer.create` + `add_char``String::with_capacity` + `push`
Char iteration`String.iter``s.chars()`
Row join`String.concat "," list``vec.join(",")`
Map + join pattern`String.concat sep (List.map f list)``list.iter().map(f).collect::<Vec<_>>().join(sep)`
Empty field`""` โ†’ no quoting`""` โ†’ no quoting (same)