csv

package module
v0.0.0-...-c0e361e Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 4 Imported by: 0

README

go-ruby-csv/csv

csv — go-ruby-csv

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of Ruby's CSV standard library — the parser and generator behind MRI 4.0.5's csv gem (3.3.5). It parses and generates CSV/TSV with Ruby's rich option set, reproducing MRI semantics byte-for-byte: the dialect options (col_sep / row_sep with :auto detection / quote_char), RFC-style quoting with embedded quotes, separators and newlines inside quoted fields, the headers model (CSV::Row / CSV::Table), the built-in field and header converters, and CSV::MalformedCSVError with MRI-exact messages and line numbers — without any Ruby runtime.

It is the CSV backend for go-embedded-ruby, but is a standalone, reusable module with no dependency on the Ruby runtime — a sibling of go-ruby-regexp (the Onigmo engine), go-ruby-erb (the ERB compiler) and go-ruby-yaml (the Psych port).

What it is — and isn't. Parsing and generating CSV for the Ruby dialect (quoting, separators, converters, the headers model, the malformed-error grammar) is fully deterministic and needs no interpreter, so it lives here as pure Go. Binding the parsed fields to live Ruby objects — instantiating a Date, a Symbol, a CSV::Row — is the host's job; this library hands back a small, explicit value model (nil, string, int, float64, Date, DateTime, Symbol, *Row, *Table) the host maps to and from its own objects.

Features

Faithful port of MRI's CSV parser and writer, validated against the ruby binary on every supported platform:

  • ParsingParseLine (CSV.parse_line), Parse (CSV.parse) and ParseRows. RFC-style quoting with "" escapes; separators and newlines inside quoted fields; empty unquoted fields become nil, empty lines become [], an empty input line yields nil — exactly as MRI.
  • Dialect optionscol_sep (multi-byte), row_sep (explicit or :auto detection of \r\n / \r / \n), quote_char (or disabled via NoQuote).
  • Headersheaders: true / "first_row" / []string, producing a Table of Rows with access by header name or index, plus return_headers and write_headers.
  • Field converters:integer, :float, :numeric, :date, :date_time, :time, :all, with MRI-faithful Integer() / Float() acceptance (radix prefixes, _ separators, octal leading zero, hex floats) and the exact DateMatcher / DateTimeMatcher shapes.
  • Header converters:downcase, :symbol, :symbol_raw.
  • skip_blanks, skip_lines, liberal_parsing, strip, nil_value, empty_value parse options.
  • GenerationGenerateLine (CSV.generate_line) and Generate (CSV.generate) with force_quotes, quote_empty, and the precise quote-when-needed rule (quote char, separators, CR or LF).
  • MalformedCSVErrorUnclosed quoted field, Illegal quoting, Any value after quoted field isn't allowed, each with MRI-exact wording and 1-based line numbers.

Usage

import "github.com/go-ruby-csv/csv"

// Parse a record (CSV.parse_line).
row, _ := csv.ParseLine(`a,"b,c",d`, csv.Options{})
// row == []any{"a", "b,c", "d"}

// Empty unquoted fields are nil; embedded newlines survive in quotes.
row, _ = csv.ParseLine("a,,\"x\ny\"", csv.Options{})
// row == []any{"a", nil, "x\ny"}

// Converters.
row, _ = csv.ParseLine("1,2.5,foo", csv.Options{Converters: []string{"numeric"}})
// row == []any{1, 2.5, "foo"}

// Headers → Table / Row.
t, _ := csv.Parse("name,age\nAlice,30", csv.Options{Headers: true})
tbl := t.(*csv.Table)
v, _ := tbl.Rows[0].Field("name") // "Alice"

// Generate (CSV.generate_line).
line, _ := csv.GenerateLine([]any{"a", "b,c", nil}, csv.Options{})
// line == "a,\"b,c\",\n"

// Malformed input yields a CSV::MalformedCSVError-compatible error.
_, err := csv.Parse("a,\"b\nc,d", csv.Options{})
// err.Error() == "Unclosed quoted field in line 1."

Tests & coverage

The suite is split into a deterministic, Ruby-free corpus (round-trip parse/generate, quoting edge cases, headers, converters, malformed errors) and a differential MRI oracle that runs every case against the ruby binary on the ubuntu/macos lanes. The oracle gates itself on RUBY_VERSION >= 4.0 and feeds CSV inputs over binmode stdin so embedded \n / \r\n are never CRLF-mangled on Windows. The deterministic tests alone hold 100.0% coverage, so the no-ruby, Windows and qemu cross-arch lanes all pass the gate.

go test -race -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1   # total: 100.0%

CGO is never used; the library builds and tests clean on all six supported 64-bit targets (amd64, arm64, riscv64, loong64, ppc64le, and big-endian s390x).

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-csv/csv authors.

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

Documentation

Overview

Package csv is a pure-Go (no cgo) reimplementation of Ruby's CSV standard library — the parser and generator behind MRI 4.0.5's csv gem (3.3.5). It reproduces Ruby's dialect options (col_sep / row_sep / quote_char), RFC-style quoting with embedded quotes, separators and newlines inside quoted fields, the headers model (CSV::Row / CSV::Table), the built-in field and header converters, and CSV::MalformedCSVError with MRI-exact messages and line numbers — all without any Ruby runtime.

It is the CSV backend for go-embedded-ruby, but is a standalone, reusable module: a sibling of go-ruby-regexp, go-ruby-erb and go-ruby-yaml.

Value model

A parsed field is one of: nil (an empty, unquoted field — Ruby's nil), a string, or — when field converters run — an int, float64, a Date or a DateTime. Headers turn a parsed document into a Table of [Row]s offering access by header name or by index, mirroring CSV::Table / CSV::Row.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Generate

func Generate(rows [][]any, opts Options) (string, error)

Generate renders many rows, mirroring Ruby's CSV.generate. When WriteHeaders is set with a []string Headers, the header row is emitted first.

func GenerateLine

func GenerateLine(row []any, opts Options) (string, error)

GenerateLine renders one row to a CSV record (with the row separator), mirroring Ruby's CSV.generate_line. A nil field becomes an empty (unquoted) string; other fields are stringified with Ruby's to_s rules for the common scalar types (the binding pre-stringifies anything exotic).

func Parse

func Parse(data string, opts Options) (any, error)

Parse parses an entire CSV document, mirroring Ruby's CSV.parse. Without headers it returns [][]any. With headers set it returns a *Table; callers that want the rows can use Table.Rows or the convenience Parse result type switch. The returned value is `any` because Ruby's CSV.parse returns either an Array of Arrays or a CSV::Table depending on :headers.

func ParseLine

func ParseLine(line string, opts Options) ([]any, error)

ParseLine parses a single CSV record, mirroring Ruby's CSV.parse_line. It returns the row's fields ([]any) or nil when the line is empty (Ruby returns nil for an empty string). A record may legitimately span several physical lines when a field is quoted and contains the row separator; ParseLine reads exactly one logical record and ignores any trailing records.

func ParseRows

func ParseRows(data string, opts Options) ([][]any, error)

ParseRows parses a document with no header handling and returns the raw rows. It is the typed entry point the binding uses when it knows headers are off.

Types

type Date

type Date struct {
	Year, Month, Day int
	// Raw is the original field text Date.parse consumed (for the binding to
	// re-parse faithfully if it needs the exact Ruby object).
	Raw string
}

Date mirrors a Ruby Date produced by the :date converter. It carries the calendar date; the binding maps it to a Ruby Date object.

type DateTime

type DateTime struct {
	Raw  string
	Time bool
}

DateTime mirrors a Ruby DateTime / Time produced by the :date_time / :time converters. Time reports whether it came from the :time converter (Ruby Time) rather than :date_time (Ruby DateTime).

type MalformedCSVError

type MalformedCSVError struct {
	// Reason is the human-readable cause, without the trailing " in line N.".
	Reason string
	// Line is the 1-based line number MRI reports for the error.
	Line int
}

MalformedCSVError mirrors Ruby's CSV::MalformedCSVError. Its message and the 1-based line number match MRI byte-for-byte (e.g. "Unclosed quoted field in line 2.").

func (*MalformedCSVError) Error

func (e *MalformedCSVError) Error() string

Error renders the message exactly as MRI's CSV::MalformedCSVError#message.

type Options

type Options struct {
	// ColSep is Ruby's :col_sep (the field separator). Empty means ",".
	ColSep string
	// RowSep is Ruby's :row_sep (the record separator). Empty means :auto,
	// which detects "\r\n", "\r" or "\n" from the data when parsing and uses
	// "\n" when generating.
	RowSep string
	// QuoteChar is Ruby's :quote_char. Empty means '"'. Set NoQuote to disable
	// quoting entirely (Ruby's quote_char: nil).
	QuoteChar string
	// NoQuote disables quote processing (Ruby quote_char: nil): quotes are then
	// ordinary characters on both parse and generate.
	NoQuote bool

	// Headers selects header handling, mirroring Ruby's :headers:
	//   nil / false      → no headers (plain rows)
	//   true             → first row is the header row
	//   "first_row"      → same as true
	//   []string{...}    → explicit header names
	Headers any
	// ReturnHeaders mirrors :return_headers — when true and Headers is set, the
	// header row is itself emitted as a Row (with HeaderRow true).
	ReturnHeaders bool
	// WriteHeaders mirrors :write_headers — when generating with Headers set,
	// emit the header row first.
	WriteHeaders bool

	// Converters lists built-in field converter names (e.g. "integer",
	// "float", "numeric", "date", "date_time", "time", "all") applied to each
	// parsed field. Unknown names are ignored, matching nothing.
	Converters []string
	// HeaderConverters lists built-in header converter names ("downcase",
	// "symbol", "symbol_raw") applied to each header.
	HeaderConverters []string

	// SkipBlanks mirrors :skip_blanks — drop wholly empty rows when parsing.
	SkipBlanks bool
	// SkipLines mirrors :skip_lines — a Go regexp (RE2) source; lines whose raw
	// text matches are skipped. Empty disables it.
	SkipLines string
	// LiberalParsing mirrors :liberal_parsing — tolerate quotes that would
	// otherwise be malformed, keeping the raw text instead of erroring.
	LiberalParsing bool

	// Strip mirrors :strip. StripSpace strips leading/trailing whitespace from
	// unquoted fields; StripChars (when non-empty) strips those specific
	// characters instead (Ruby strip: "x").
	StripSpace bool
	StripChars string

	// ForceQuotes mirrors :force_quotes — quote every generated field.
	ForceQuotes bool
	// QuoteEmpty mirrors :quote_empty (default true) — quote empty (but
	// non-nil) string fields when generating. Set QuoteEmptySet to make a false
	// value meaningful (otherwise the zero value is treated as the default true).
	QuoteEmpty    bool
	QuoteEmptySet bool

	// NilValue mirrors :nil_value — value substituted for an empty unquoted
	// field on parse (default nil). NilValueSet distinguishes "set to nil"
	// from "unset".
	NilValue    any
	NilValueSet bool
	// EmptyValue mirrors :empty_value — value substituted for an empty *quoted*
	// field on parse (default ""). EmptyValueSet distinguishes "set to nil".
	EmptyValue    any
	EmptyValueSet bool
}

Options holds the subset of Ruby CSV options this library understands. The zero value is *not* MRI's default; use DefaultOptions (or leave the relevant fields blank and let [normalize] fill them) to get Ruby's defaults (col_sep ",", row_sep :auto, quote_char '"').

func DefaultOptions

func DefaultOptions(opts Options) Options

DefaultOptions returns a copy of opts with Ruby's defaults filled in for any blank dialect field, so callers can reason about effective separators.

type Row

type Row struct {
	// Headers are the header values (string or Symbol after header conversion),
	// shared with the owning Table.
	Headers []any
	// Fields are this row's values, positionally aligned with Headers.
	Fields []any
	// HeaderRow reports whether this Row is the header row itself (emitted only
	// when return_headers is set), mirroring CSV::Row#header_row?.
	HeaderRow bool
}

Row mirrors CSV::Row: an ordered list of fields paired with the table's headers, offering access by header name or by index. It is the per-record type the binding maps to a CSV::Row.

func (*Row) At

func (r *Row) At(i int) (any, bool)

At returns the value at index i, mirroring Row#[] with an integer argument. A negative index counts from the end (Ruby semantics). ok is false when out of range.

func (*Row) Field

func (r *Row) Field(name any) (any, bool)

Field returns the value for the given header name, mirroring Row#[] with a header argument. When a header repeats, the first match wins (as in MRI). ok is false when no such header exists.

func (*Row) ToHash

func (r *Row) ToHash() map[any]any

ToHash returns the row as an ordered header→value pairing, mirroring Row#to_h. When a header repeats, the last value wins (MRI's to_h behaviour).

type Symbol

type Symbol string

Symbol mirrors a Ruby Symbol produced by the :symbol / :symbol_raw header converters.

type Table

type Table struct {
	// Headers are the (possibly converted) header values.
	Headers []any
	// Rows are the data rows. When ReturnHeaders was set, the first element is
	// the header Row (HeaderRow true).
	Rows []*Row
}

Table mirrors CSV::Table: a header row plus its data Rows.

func (*Table) Row

func (t *Table) Row(i int) (*Row, bool)

Row returns the i-th data row, mirroring Table#[] with an integer.

func (*Table) ToArray

func (t *Table) ToArray() [][]any

ToArray renders the table as Ruby's CSV::Table#to_a: the header row followed by each data row's fields. When the header Row is already present (return_ headers), it is not duplicated.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL