CSV

repository·main·Indexed 19 days ago

https://github.com/beatrichartz/csv

An RFC 4180 compliant, composable CSV parsing and encoding library for Elixir designed for high-performance data pipelines. It supports streaming of bytes or lines, providing functions like CSV.decode/1 for resilient parsing and CSV.encode/1 for transforming tables into CSV streams. The library includes a strict mode via CSV.decode!/1, customizable separators and escape characters, and a CSV.Encode protocol for custom data types.

Tokens
3.6K
Snippets
15
Records
16
Agent score
67%

What's inside beatrichartz-csv

  1. Upgrade from CSV 2.x to 3.x

    main

    The 3.x version streamlines the API and uses binary matching. Key breaking changes include:

    • Parallelism removed: Remove :num_workers and :worker_work_ratio options.
    • Line breaks: CSV now expects line breaks. If you previously split strings manually, pass the string as a single-item list: ["a,b,c\nd,e,f"] |> CSV.decode().
    • Error Renaming: StrayQuoteError is now StrayEscapeCharacterError.
    • Field Transformation: Replace :strip_fields with :field_transform (e.g., field_transform: &String.trim/1).
    • Validation: :validate_row_length now defaults to false. Set to true for 2.x behavior.
    • Formula Handling: :escape_formulas is now :unescape_formulas for decode and decode!.
    • Replacement: :replace is removed. Use :field_transform to handle incorrect encoding manually.
    • Escape Max Lines: :escape_max_lines now defaults to 10 (previously 1000).
  2. Optimize CSV parsing performance

    main

    For large files, performance is best when streaming with :read_ahead in byte mode. While 1000 bytes is a common default, you should tune this based on your environment.

    File.stream!("data.csv", [read_ahead: 100_000], 1000) |> CSV.decode()
  3. Encode data with CSV.encode/1

    main

    Use CSV.encode/1 to transform a table (a two-dimensional list) into a stream of lines ready for output (e.g., writing to a file or IO).

    # Writing a table to a file
    table_data |> CSV.encode |> Enum.each(&IO.write(file, &1))
  4. Implement the CSV.Encode protocol

    main

    You can implement the CSV.Encode protocol to define how custom data types should be encoded into CSV format.

    defimpl CSV.Encode, for: MyData do
      def encode(%MyData{has: fun}, env \ []) do
        "so much #{fun}" |> CSV.Encode.encode(env)
      end
    end
  5. Decode CSV data in strict mode with CSV.decode!/1

    main

    Use CSV.decode!/1 for strict mode. This returns a two-dimensional list (a list of lists) and will raise an exception immediately upon encountering the first error, aborting the operation.

    You can use the unredact_exceptions: true option to ensure the source data is included in any exceptions thrown.

    # Strict mode returns a list of lists and raises on error
    File.stream!("data.csv") |> CSV.decode!
    
    # Unredact source data in exceptions
    File.stream!("data.csv") |> CSV.decode!(unredact_exceptions: true)
  6. Decode CSV data with CSV.decode/1

    main

    Use CSV.decode/1 to transform a stream of bytes or lines into a stream of row tuples.

    In normal mode, it returns a stream of {:ok, [fields]} or {:error, "message"} tuples. It is designed to be resilient by reparsing lines after an unterminated escape sequence to ensure all correctly formatted rows are captured.

    Common usage patterns include:

    • Decoding a file line by line.
    • Decoding a UTF-16 file with BOM.
    • Decoding a file in specific byte chunks.
    • Decoding a single CSV-formatted string.
    • Decoding a list of arbitrarily chunked CSV data.
    # Decode file line by line
    File.stream!("data.csv")
      |> CSV.decode()
    
    # Decode a UTF-16 file with BOM
    File.stream!([:trim_bom, encoding: {:utf16, :little}])
      |> CSV.decode()
    
    # Decode file in chunks of 1000 bytes
    File.stream!("data.csv", [], 1000) 
      |> CSV.decode()
    
    # Decode a csv formatted string
    ["long,csv,string\nwith,multiple,lines"] 
      |> CSV.decode()
    
    # Decode a list of arbitrarily chunked csv data
    ["list,", "of,arbitrarily", "\nchun", "ked,csv,data\n"] 
      |> CSV.decode()
  7. Configure CSV.decode/2 options

    main

    When calling CSV.decode/2 or CSV.decode!/2, you can pass an options keyword list to customize parsing:

    • separator: Specify a custom separator (e.g., separator: ?;).
    • escape_character: Specify a custom escape character (e.g., escape_character: ?@).
    • field_transform: A function applied to each field during parsing (e.g., field_transform: &String.trim/1).
    • unescape_formulas: Boolean to unescape formulas that have been escaped.
    • redact_errors: Boolean to redact source data in error tuples produced by decode/1.
    # Example: custom separator and field transformation
    stream |> CSV.decode(separator: ?;, field_transform: &String.trim/1)
    
    # Example: unescaping formulas
    stream |> CSV.decode(unescape_formulas: true)
    
    # Example: redacting errors
    stream |> CSV.decode(redact_errors: true)
  8. Configure CSV.encode/2 options

    main

    Customize the encoding process using CSV.encode/2:

    • separator: Specify a custom separator (e.g., separator: ?;).
    • escape_character: Specify a custom escape character (e.g., escape_character: ?@).
    • headers:
      • Provide a list of strings to encode map values into specific positions: headers: ["z", "a"].
      • Provide a keyword list where keys are the map keys and values are the header names: headers: [a: "x", b: "y"].
    # Using a semicolon separator
    your_data |> CSV.encode(separator: ?;)
    
    # Encoding maps with specific header positions
    [%{"a" => "value!"}] |> CSV.encode(headers: ["z", "a"])
    # Output: ["z,a\r\n", ",value!\r\n"]
    
    # Encoding maps with custom header names via keyword list
    [%{a: "value!"}] |> CSV.encode(headers: [a: "x", b: "y"])
    # Output: ["x,y\r\n", "value!,\r\n"]
  9. Customize separators and delimiters in CSV.Encoding.Encoder

    main

    You can override the default comma separator and CRLF delimiter by passing :separator (as a codepoint) and :delimiter (as a string) to the encode/2 function.

    [["a\nb", "\tc"], ["de", "\tf\""]]
    |> CSV.Encoding.Encoder.encode(separator: ?\t, delimiter: "\n")
    |> Enum.take(2)
    # => ["\"a\nb\"\t\"\tc\"\n", "de\t\"\tf\"\"\"\n"]
  10. Convert a stream of maps into CSV lines using headers

    main

    When your input stream consists of maps, set headers: true to automatically extract keys from the first map to create a header row, followed by the values of each map as data rows.

    [%{"a" => 1, "b" => 2}, %{"a" => 3, "b" => 4}]
    |> CSV.Encoding.Encoder.encode(headers: true)
    |> Enum.to_list()
    # => ["a,b\r\n", "1,2\r\n", "3,4\r\n"]
  11. Implement the CSV.Encode protocol for custom data types

    main

    To support custom data types in CSV generation, implement the CSV.Encode protocol. The encode/2 function receives the data to be encoded and an env keyword list containing the current encoding context (such as the separator and delimiter).

    By default, the protocol falls back to Any, which converts the data to a string and then uses the BitString encoding implementation.

    defimpl CSV.Encode, for: MyCustomType do
      def encode(data, env \ []) do
        # Your custom encoding logic here
        # env can contain :separator, :escape_character, :delimiter, etc.
      end
    end