ExamplesBy LevelBy TopicLearning Paths
959 Fundamental

959 Csv Writer

Functional Programming

Tutorial

The Problem

Implement a CSV writer that correctly escapes fields: any field containing a comma, double-quote, newline, or carriage return must be wrapped in double quotes, with internal double-quotes doubled ("""). Implement field escaping, single-row serialization, and multi-row document serialization.

🎯 Learning Outcomes

  • • Implement needs_quoting(s: &str) -> bool to detect when a field requires quoting
  • • Implement escape_field: wrap in quotes and double internal " characters
  • • Use fields.iter().map(escape_field).collect::<Vec<_>>().join(",") for row serialization
  • • Handle both &str and String input variants
  • • Understand the RFC 4180 CSV quoting rules: quote if and only if the field contains ,, ", \n, or \r
  • Code Example

    #![allow(clippy::all)]
    // 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")
    }
    
    #[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\"\"\"");
        }
    }

    Key Differences

    AspectRustOCaml
    Quote detection.contains('"') etc.String.contains s '"' etc.
    String builderString::with_capacity + pushBuffer + add_char
    Row joining.join(",")String.concat ","
    Multi-row.join("\n")String.concat "\n"
    Slice vs list&[&str] — zero-copy borrowed slicestring list — linked list

    The writer and parser (958) form a pair that should round-trip: any Vec<Vec<String>> serialized with write_csv and parsed with parse_csv_line row-by-row should recover the original data exactly.

    OCaml Approach

    let needs_quoting s =
      String.contains s ',' || String.contains s '"' ||
      String.contains s '\n' || String.contains s '\r'
    
    let escape_field s =
      if not (needs_quoting s) then s
      else begin
        let buf = Buffer.create (String.length s + 2) in
        Buffer.add_char buf '"';
        String.iter (fun c ->
          if c = '"' then Buffer.add_char buf '"';
          Buffer.add_char buf c
        ) s;
        Buffer.add_char buf '"';
        Buffer.contents buf
      end
    
    let write_row fields =
      String.concat "," (List.map escape_field fields)
    
    let write_csv rows =
      String.concat "\n" (List.map write_row rows)
    

    OCaml's String.contains provides a built-in single-character search, slightly cleaner than Rust's .contains(char). The Buffer-based approach avoids intermediate string allocations during escape processing.

    Full Source

    #![allow(clippy::all)]
    // 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")
    }
    
    #[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\"\"\"");
        }
    }
    ✓ Tests Rust test suite
    #[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\"\"\"");
        }
    }

    Deep 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 checkString.contains s ','s.contains(',')
    String buildingBuffer.create + add_charString::with_capacity + push
    Char iterationString.iters.chars()
    Row joinString.concat "," listvec.join(",")
    Map + join patternString.concat sep (List.map f list)list.iter().map(f).collect::<Vec<_>>().join(sep)
    Empty field"" → no quoting"" → no quoting (same)

    Exercises

  • Verify round-trip correctness: write a property test with random strings (including commas and quotes) and confirm parse(write(data)) == data.
  • Add an append_row variant that writes to a Write trait implementation (e.g., std::io::stdout()) instead of returning a String.
  • Handle the BOM (byte-order mark) that Excel inserts in CSV files: add an optional write_with_bom variant.
  • Implement write_tsv (tab-separated values) by parameterizing the delimiter.
  • Extend needs_quoting to also quote fields that start or end with whitespace.
  • Open Source Repos