simplecsv

package module
v1.0.1 Latest Latest
Warning

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

Go to latest
Published: Aug 5, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

README

Simplecsv

The Go package simplecsv is a library to handle csv files without using a database.

Some notes:

  • all read methods return the value in the csv and a second true/false value that is true if the value exists
  • all write methods that change the csv return the changed csv and a true/false value if the operation was successful
  • all cells are strings
  • methods that return a csv never modify the original csv and share no data with it: the returned csv is an independent copy. Get methods like GetRow and GetHeaders return copies
  • header names (the first row) must be unique, like database columns. Reading a file whose header row has duplicate names, creating a csv with duplicate headers, or renaming a header to a name that already exists is rejected. This guarantees name-based lookups (GetCellByField, FindInField, SortByField, GetRowAsMap, the *FromMap functions, etc.) address exactly one column.
  • all rows have the same number of cells as the header row (uniform row width). Reading a ragged file (rows with more or fewer fields than the header row) is rejected with an error, and AddRow / SetRow reject a row whose length is not the header width. This makes "the number of columns" unambiguous and keeps row operations consistent.
  • ReadCsv, ReadCsvFile, ReadCsvFileE and ReadCsvFileComma read the whole input into memory. For large or attacker-controlled input, use ReadCsvLimit with positive record and byte limits.
  • find and match (FindInColumn, FindInField, MatchInColumn, MatchInField) are case-insensitive by default: Foo = foo = FOO. Use the *CaseSensitive variants (FindInColumnCaseSensitive, FindInFieldCaseSensitive, MatchInColumnCaseSensitive, MatchInFieldCaseSensitive) when case matters.
  • CSV / formula injection: values that begin with =, +, -, @, a tab or a carriage return are interpreted as formulas by spreadsheet applications (Excel, LibreOffice Calc, Google Sheets). The Write* functions write values verbatim and do not neutralize such values, because escaping changes data. When the csv may contain attacker-controlled data (exported logs, form input, scraped content) and may be opened in a spreadsheet, call SanitizeFormulas first to prefix those cells with a single quote '. Do not open untrusted CSVs in a spreadsheet without sanitization. Callers that do not want their bytes changed should not call it.
  • writes are atomic and not world-readable: WriteCsvFile / WriteCsvFileE / WriteCsvFileComma write the csv to a temporary file in the same directory as the destination and then rename it over the destination, so a failed or interrupted write cannot truncate or corrupt the destination. The file (new or overwritten) ends up with mode 0600 (owner-readable only); callers that need different permissions can os.Chmod the result. Because the destination is replaced with os.Rename rather than opened, a symlink planted at the destination is replaced instead of followed.

Installation

go get github.com/osvik/simplecsv

Then import it in your code:

import "github.com/osvik/simplecsv"

Examples

Csv file

Simplecsv works with comma separated csv files.

Read

Reads file and parses as a SimpleCsv object. fileRead is false if there's an error reading the file or parsing the CSV, if the file's header row contains duplicate names (headers must be unique), or if the file is ragged (rows with a different number of fields than the header row are rejected; uniform row width is an invariant).

var x simplecsv.SimpleCsv
var fileRead bool
x, fileRead = simplecsv.ReadCsvFile("my1file.csv")

All read functions (file-based and reader-based, limited or not) strip a UTF-8 byte order mark (BOM) from the first cell of the first row if present. Excel and many Windows tools write one (EF BB BF) at the start of the file; without stripping, the first header would become "\ufeffid" and every field lookup would silently fail. Only the first cell of the first row is touched.

Create

Create empty file and define csv headers. Header names must be unique; if they are not, the returned error is non-nil and the returned csv is nil:

var u simplecsv.SimpleCsv
var err error
u, err = simplecsv.CreateEmptyCsv([]string{"Age", "Gender", "ID"})
if err != nil {
	log.Fatal(err)
}

MustCreateEmptyCsv is the same but panics when the headers are invalid (empty or duplicated). Handy in tests and small scripts where a valid header list is part of the program, not a runtime condition:

u := simplecsv.MustCreateEmptyCsv([]string{"Age", "Gender", "ID"})
Write

Write the SimpleCsv object to my2file.csv. If there's an error, wasWritten is false.

wasWritten := u.WriteCsvFile("my2file.csv")

The write is atomic: the csv is written to a temp file in the same directory and renamed over the destination, so a failed or interrupted write cannot truncate or corrupt the destination. The file (new or overwritten) ends up with mode 0600 (owner-readable only); callers that need different permissions can os.Chmod the result. A symlink at the destination is replaced rather than followed.

Sanitize (CSV / formula injection)

The Write* functions write values verbatim. A value that begins with =, +, -, @, a tab or a carriage return is interpreted as a formula by spreadsheet applications (Excel, LibreOffice Calc, Google Sheets), and can be used to execute commands or read other cells when the file is opened. This is a real risk when the csv contains data that may be attacker-controlled (exported logs, form input, scraped content) and is later opened in a spreadsheet. Do not open untrusted CSVs in a spreadsheet without sanitization.

SanitizeFormulas returns an independent copy of the csv in which every such cell is prefixed with a single quote ' (the spreadsheet convention), so the value is treated as text. Escaping changes data (for example -5 becomes '-5, though the spreadsheet hides the quote and displays -5 as text), so sanitizing is opt-in. The original csv is not modified.

safe := u.SanitizeFormulas()
wasWritten := safe.WriteCsvFile("export.csv")
err := safe.WriteTo(os.Stdout, ',')
Errors

ReadCsvFileE and WriteCsvFileE work like ReadCsvFile and WriteCsvFile but return an error with the reason of the failure:

x, err := simplecsv.ReadCsvFileE("my1file.csv")
if err != nil {
	log.Fatal(err)
}

The joins, GroupBy and Concat have their own E variants; see Error variants.

err := u.WriteCsvFileE("my2file.csv")

ReadCsv and the file-reading wrappers read the whole input into memory. For large or attacker-controlled input, ReadCsvLimit reads one record at a time and rejects input over the configured limits. maxRecords includes the header row, maxBytes limits input bytes, zero disables the corresponding limit, and negative limits are rejected:

file, err := os.Open("large.csv")
if err != nil {
	log.Fatal(err)
}
defer file.Close()
x, err := simplecsv.ReadCsvLimit(file, ',', 10000, 10*1024*1024)
if err != nil {
	log.Fatal(err)
}
Delimiters, readers and writers

Simplecsv uses , as the default field separator, but it can read and write files with other separators, like ; or tabs, and it can read from any io.Reader (for example os.Stdin) and write to any io.Writer (for example os.Stdout):

x, err := simplecsv.ReadCsvFileComma("semicolonfile.csv", ';')
x, err := simplecsv.ReadCsv(os.Stdin, '\t')
err := x.WriteCsvFileComma("semicolonfile.csv", ';')
err := x.WriteTo(os.Stdout, ',')
Headers

The cells of the first row are considered headers.

Get

Get all headers:

headers := x.GetHeaders()

Get header at position one (second position as it starts from 0):

headerName, headerExists := x.GetHeader(1)

Get header position: (it returns -1 if the header does not exist)

position := x.GetHeaderPosition("Gender")
Rename

Rename header: (old header, new header)

x, headerExists := x.RenameHeader("ID", "IDnumber")

headerExists is false if the old header does not exist, or if IDnumber already exists as another column: header names must stay unique, so a rename that would produce a duplicate is rejected. Renaming a header to its own name is a no-op and is allowed.

Rows
Get

Get number of rows:

numberOfRows := x.GetNumberRows()

Get second row:

row, rowExists := x.GetRow(1)

Get second row as a map:

row, rowExists := x.GetRowAsMap(1)
Add

Add a slice to a row. The slice must have the same size as the CSV number of columns. If not wasSuccessful is false.

var wasSuccessful bool
x, wasSuccessful = x.AddRow([]string{"24", "M", "2986732"})

Add row from map: (If the map keys don't exist as columns, the value will be discarded. If a key does not exist, it will create empty cells.)

mymap := make(map[string]string)
mymap["Age"] = "62"
mymap["Gender"] = "F"
mymap["ID"] = "6463246"
var wasAdded bool 
x, wasAdded = x.AddRowFromMap(mymap)
Set

Set second row (1) from a slice. The length of the slice must be the same as the number of columns and the row must already exist. If there’s an error wasSet is false.

var wasSet bool
x, wasSet = x.SetRow(1, []string{"45", "F", "8356138"})

Set second row from map: If the map keys don't exist as columns, the value will be discarded. If a key does not exist, it will create empty cells.)

mymap2 := make(map[string]string)
mymap2["Age"] = "62"
mymap2["Gender"] = "F"
mymap2["ID"] = "6463246"
var wasSet bool
x, wasSet = x.SetRowFromMap(1, mymap2)
Update

Unlike SetRowFromMap, UpdateRowCellsFromMap does not erase the cells value just because the column names are not keys in the map. It updates the cells that have the column name in the map and maintains the value of all the others.

To update the age in row 1:

mymap3 := make(map[string]string)
mymap3["Age"] = "63"
var wasUpdated bool
x, wasUpdated = x.UpdateRowCellsFromMap(1, mymap3)
Delete

Delete second row: (If the row number is invalid, wasDeleted is false)

var wasDeleted bool
x, wasDeleted = x.DeleteRow(1)
Data rows

Get the number of data rows, excluding the header row (unlike GetNumberRows, which includes it):

numberOfDataRows := x.GetNumberDataRows()

Get all the data rows as copies (the header row is not included; changes to the returned rows don't affect the csv):

dataRows := x.GetDataRows()

Iterate over the data rows without touching the header: EachDataRow calls fn once per data row, in csv order, with the row index (starting at 1), a copy of the row, and a map view of it (header name to cell value). Mutating the row or the map inside fn does not affect the csv. fn may return false to stop the iteration early:

x.EachDataRow(func(rowIndex int, row []string, asMap map[string]string) bool {
    if asMap["status"] == "stop" {
        return false // stop the iteration
    }
    fmt.Printf("row %d: %v\n", rowIndex, row)
    return true
})
Slice

Head returns the header row plus the first n data rows (or all of them if there are fewer). n negative or zero means no data rows:

sample := x.Head(10)

Tail returns the header row plus the last n data rows:

recent := x.Tail(10)

SliceRows returns the header row plus the data rows whose csv row index is in [start, end) (half-open, data rows start at index 1). end beyond the last row is clamped. wasSliced is false if the csv is empty, start < 1 or end < start:

var wasSliced bool
x, wasSliced = x.SliceRows(1, 3) // data rows 1 and 2
Append

AppendRows appends the data rows of another csv with the same headers (same names, same order). wasAppended is false if either csv is empty or the headers differ:

var wasAppended bool
x, wasAppended = x.AppendRows(anotherCsv)
Concat

Concat stacks several csvs vertically in one call: the first non-empty csv supplies the headers and every csv with a header row must have the same headers (same names, same order); empty csvs are skipped. If there is no non-empty csv or the headers differ, it returns nil and false:

combined, ok := simplecsv.Concat(day1, day2, day3)
Concat by name

Monthly or vendor exports often add or reorder columns, and strict Concat rejects them. ConcatByName stacks csvs vertically aligning the columns by header name instead of by position: the result has the header row of the first non-empty csv, followed by the header names that only appear in later csvs (in order of first appearance). Each data row is placed under its header name, with empty strings where the csv has no column with that name:

january, _ := simplecsv.ReadCsvFile("january.csv") // columns: id, name
february, _ := simplecsv.ReadCsvFile("february.csv") // columns: name, bonus
combined, ok := simplecsv.ConcatByName(january, february)

ok is false if there is no non-empty csv, or a csv has duplicate header names (the result would be ambiguous). When every csv has the same headers in the same order, ConcatByName is equivalent to Concat.

Deduplicate

Unique removes exact duplicate data rows: for each full row (all columns) only the first occurrence is kept, in csv order. The header row is kept:

unique := x.Unique()

UniqueByFields removes data rows that share the same key built from the named columns; the first row per key is kept. wasUnique is false if the csv is empty, no fields are given, or a field name does not exist:

var wasUnique bool
onePerEmail, wasUnique = x.UniqueByFields("email")
onePerCountryCity, wasUnique = x.UniqueByFields("country", "city")
Distinct and counts

Distinct returns the distinct values of a column (data cells only, the header cell is never included), in order of first appearance. The returned slice is a copy:

countries, wasRead := x.Distinct("Country")

wasRead is false if the csv is empty or the field name does not exist. A csv with only the header row returns an empty slice and true.

ValueCounts returns a new csv with the fixed headers value and count: one row per distinct value of the column, in order of first appearance, with the number of data rows that have that value, as a decimal string:

counts, wasRead := x.ValueCounts("Country")

The original csv is not modified. wasRead is false if the csv is empty or the field name does not exist; in that case a copy of the csv is returned.

Group by

GroupBy collapses the data rows into one output row per group, where a group is all the data rows whose key fields have the same values. The output header row is the key field names followed by the As name of each aggregation; each output data row is the key values followed by one cell per aggregation. Groups appear in order of first occurrence:

var summary simplecsv.SimpleCsv
var wasGrouped bool
summary, wasGrouped = sales.GroupBy(
	[]string{"country"},
	[]simplecsv.Agg{{Op: simplecsv.AggCount, As: "orders"}, {Field: "amount", Op: simplecsv.AggSum, As: "total"}},
)

An aggregation is Field (the source column, ignored for AggCount), Op (the operation) and As (the output header, which must be unique among the key names and the other As names). The operations are:

  • AggCount — counts the data rows of the group. Field may be empty.
  • AggSum — adds the Field cells parsed with strconv.ParseFloat; cells that don't parse as numbers count as 0.
  • AggMin / AggMax — the smallest / largest finite numeric Field cell; a group without finite numeric cells produces an empty cell.
  • AggFirst / AggLast — the Field value of the first / last data row of the group.
  • AggJoin — the non-empty Field values of the group joined with ,.

Numeric results are formatted with strconv.FormatFloat(f, 'f', -1, 64), so integers have no decimal noise.

wasGrouped is false if the csv is empty, no keys or no aggregations are given, a key or aggregation field does not exist, an As name is empty or duplicates another key or As name, or an aggregation operation is not one of the AggOp constants; in that case a copy of the csv is returned.

Column
Add

To add a column at the end of the CSV:

var wasSuccessful bool
x, wasSuccessful = x.AddEmptyColumn("NewColumn")
Remove

To remove a column at position 1 (second column, because it's zero based):

var wasRemoved bool
x, wasRemoved = x.RemoveColumn(1)

To remove a column by name:

var wasRemoved bool
x, wasRemoved = x.RemoveColumnByName("Gender")
Get

Get a copy of all the data cells of a column, skipping the header cell. The returned slice is a copy: changes to it don't affect the csv. columnExists is false if the csv is empty or the column position is not valid:

columnCells, columnExists := x.GetColumn(1)

The same by column name:

columnCells, fieldExists := x.GetColumnByField("Age")

fieldExists is false if the field does not exist.

Set

Replace the data cells of a column with a slice. The slice must have the same length as the number of data rows (the csv length minus the header row), because every data row must keep exactly one cell in the column. The header cell is not modified:

var wasSet bool
x, wasSet = x.SetColumn(1, []string{"24", "62", "45"})

The same by column name:

var wasSet bool
x, wasSet = x.SetColumnByField("Age", []string{"24", "62", "45"})
Map

MapColumnByField transforms the data cells of a column with a function. The function receives the cell value and the csv row index (1 for the first data row); the header cell is not transformed. The original csv is not modified:

var wasMapped bool
x, wasMapped = x.MapColumnByField("Name", func(value string, row int) string {
	return strings.ToUpper(value)
})
Fill

FillColumnByField sets every data cell of a column to the same value. Useful for constants and defaults:

var wasFilled bool
x, wasFilled = x.FillColumnByField("Source", "import")
Column shape
Add at position

AddEmptyColumnAt inserts an empty column at a position: index is the position of the new column and must be between 0 and the number of columns (inclusive; inserting at the number of columns appends, like AddEmptyColumn). The header cell of the new column is columnName and every data cell is empty:

var wasAdded bool
x, wasAdded = x.AddEmptyColumnAt("Source", 0)
x, wasAdded = x.AddEmptyColumnAt("Notes", 3)

wasAdded is false if the csv is empty, the column name already exists or index is out of range.

Add with values

AddColumnByField appends a column with the header name columnName and the given data cells. The values slice must have the same length as the number of data rows (the csv length minus the header row), because every data row must keep exactly one cell in the column; a csv with only the header row takes an empty values slice. The values are copied:

var wasAdded bool
x, wasAdded = x.AddColumnByField("Total", []string{"24", "62", "45"})

wasAdded is false if the csv is empty, the column name already exists or the length of the values slice does not match the number of data rows.

Compute from row map

AddComputedColumn appends a column whose data cells are computed by a function from the values of the other columns. The function is called once per data row, in csv order, with a map from header name to cell value: the map is built fresh for every row and is a copy, so the callback can read field names instead of column indexes, and mutating the map does not affect the csv. The string the function returns becomes the cell of the new column:

var wasComputed bool
x, wasComputed = x.AddComputedColumn("full_name", func(row map[string]string) string {
	return row["first"] + " " + row["last"]
})

wasComputed is false if the csv is empty or the column name already exists; in that case a copy of the csv is returned. A csv with only the header row takes the new header cell only and the function is not called.

Rename batch

RenameHeaders renames several headers in one call. Every key of the map must be an existing header, and the result must not contain duplicate header names: a rename fails if a key does not exist, two keys map to the same new name, or a renamed column collides with a header that is not renamed. The renames are applied simultaneously, so a swap ("a""b", "b""a") is applied correctly and map iteration order does not matter:

var wasRenamed bool
x, wasRenamed = x.RenameHeaders(map[string]string{"ID": "IdNumber", "Date": "Fecha"})

wasRenamed is false if the csv is empty or the renamed headers would contain duplicates. Renaming a header to its own name is a no-op for that column, and an empty map is a no-op that succeeds.

Move

MoveColumn moves the column with the header name columnName to position newIndex (0 to the number of columns minus 1), shifting the columns in between: the header and data cells of the moved column move together:

var wasMoved bool
x, wasMoved = x.MoveColumn("Age", 0)

wasMoved is false if the csv is empty, the column name does not exist or newIndex is out of range. Moving a column to its own position is a no-op and succeeds.

Split a field

SplitField replaces the column with the header name name by len(newNames) new columns in its position. Every data cell is split on sep with strings.Split, and the parts fill the new columns in order: if a cell has fewer parts than newNames, the remaining new columns get empty cells; if it has more parts, the extra parts are dropped. The original column is removed, so a new name may reuse it but must not collide with any other header:

var wasSplit bool
x, wasSplit = x.SplitField("coord", ",", []string{"lat", "lon"})
x, wasSplit = x.SplitField("last;first", ";", []string{"last", "first"})

wasSplit is false if the csv is empty, the field name does not exist, newNames is empty or contains duplicates, or a new name collides with another header.

Combine fields

CombineFields appends a column with the header name as whose data cells join the values of the named columns with sep (via strings.Join). The source columns stay in place:

var wasCombined bool
x, wasCombined = x.CombineFields([]string{"first", "last"}, " ", "full_name")
x, wasCombined = x.CombineFields([]string{"country", "sku"}, "-", "country_sku")

wasCombined is false if the csv is empty, names is empty, a name does not exist, names repeats a field, or as already exists as a header.

Cells
Get

Get value of the cell in the second column, second row:

cellValue, cellExists := x.GetCell(1, 1)

Get the value of the cell in the column Age, second row:

cellValue, cellExists := x.GetCellByField("Age", 1)
Set

Changes the value of the cell in the first column (0) and the second row (1) to "27":

var wasChanged bool 
x, wasChanged = x.SetCell(0, 1, "27")

The same, using the column name instead of the column position:

var wasChanged bool 
x, wasChanged = x.SetCellByField("Age", 1, "27")
Replace

ReplaceInField replaces the data cells of a column whose value is exactly equal to old (case-sensitive, whole cell) with new. The header cell is not modified. Useful to fix known bad values (N/A → empty) or rename codes:

var wasReplaced bool
x, wasReplaced = x.ReplaceInField("Status", "N/A", "")
Trim

TrimSpace removes the surrounding whitespace of every cell, including the header cells, with strings.TrimSpace:

var wasTrimmed bool
x, wasTrimmed = x.TrimSpace()

wasTrimmed is false if trimming makes two header names equal: header names must stay unique.

Find
Find in column

Find the word "27" in the first column (column 0):

rowsWithWord, validColumn := x.FindInColumn(0, "27")

It returns a slice of rownumbers (int) where you can find the word in the column position. Please note that in simplecsv all cells are strings.

If it doesn't find the value, it returns an empty slice.

In FindInColumn and in FindInField case does not matter. Foo = foo = FOO.

Find in field

The same as FindInColumn but using a column/field name instead of position. Please note that FindInField, unlike FindInColumn never includes the header in the search result.

rowsWithWord, validFieldName := x.FindInField("Age", "27")

If the field name does not exist, the second value returned (validFieldName) is false.

Match in column

Find where results match a regular expression in the third column (column 2):

rowsWithWord, areParamsOk := x.MatchInColumn(2, "p([a-z]+)ch$")

Use ^ and $ in the regular expression to match exact results.

Matching is case-insensitive by default: Foo = foo = FOO. Use MatchInColumnCaseSensitive when case matters.

Match in field

Same as with MatchInColumn, but with a field (column name). Find where results match a regular expression in the third column (column "ID"):

rowsWithWord, areParamsOk := x.MatchInField("ID", "p([a-z]+)ch$")

Use ^ and $ in the regular expression to match exact results.

Please note that MatchInField, unlike MatchInColumn never includes the header in the search result.

Matching is case-insensitive by default: Foo = foo = FOO. Use MatchInFieldCaseSensitive when case matters.

Case sensitive find

FindInColumn, FindInField, MatchInColumn and MatchInField ignore case by default. Use the *CaseSensitive variants when case matters:

rowsWithWord, validFieldName := x.FindInFieldCaseSensitive("Name", "Ana")
rowsWithWord, areParamsOk := x.MatchInFieldCaseSensitive("ID", "p([a-z]+)ch$")
Sort
Sort by field

Sorts the csv by a column name and returns a new sorted csv. The header row stays at the top and the original csv is not modified. Numbers are sorted numerically ("7" comes before "30"), other values as strings. The sort is stable.

sortedCsv, fieldExists := x.SortByField("Age", true) // true: ascending, false: descending
Sort by column

The same as SortByField but using a column position. It sorts all the rows, including the first one:

sortedCsv, validColumn := x.SortByColumn(0, true)
Sort by multiple fields

Sorts the csv by several columns, one level per field, like ORDER BY in SQL: a later field is only compared when every earlier field compares equal. The header row stays at the top and the original csv is not modified. Each field uses the same ordering as SortByField (numbers numerically, other values as strings), and the sort is stable. An empty ascending sorts every field ascending; otherwise it has one bool per field (true ascending, false descending):

var sortedCsv simplecsv.SimpleCsv
var wereFieldsFound bool
sortedCsv, wereFieldsFound = x.SortByFields([]string{"Country", "City"}, nil)
sortedCsv, wereFieldsFound = x.SortByFields([]string{"Country", "City"}, []bool{true, false})

wereFieldsFound is false if the csv is empty, no fields are given, a field name does not exist, or the length of ascending is neither 0 nor the number of fields; in that case a copy of the csv is returned.

Sort an index

SortIndex returns a sorted copy of an index:

sortedIndex := simplecsv.SortIndex([]int{5, 1, 3})
Filter rows

FilterRows returns a new csv with the rows where the predicate function returns true. If header is true, the first row is kept as the header:

adults := x.FilterRows(func(row []string) bool {
	age, _ := strconv.Atoi(row[1])
	return age >= 18
}, true)
Field filters

FilterByField keeps the data rows where the predicate returns true for the named column. The header cell is never passed to the predicate, and the field name is used instead of a column index:

var wasFiltered bool
adults, wasFiltered = x.FilterByField("age", func(value string) bool {
	n, _ := strconv.Atoi(value)
	return n >= 18
})

Where keeps the data rows where the named column is exactly equal to value (case-sensitive). wasWhere is false if the csv is empty or the field name does not exist:

var wasWhere bool
active, wasWhere = x.Where("status", "active")

WhereFold is the same but case-insensitive: Active, active and ACTIVE all match:

var wasWhereFold bool
active, wasWhereFold = x.WhereFold("status", "active")
Join
Inner join

JoinByField joins two csvs by a common field and returns a new csv with the rows where the field has the same value in both csvs. The result has all the columns of the first csv, followed by the columns of the second csv except the join column. If a join value shows up more than once, all combinations of rows are in the result. Values are compared exactly: case matters. The source csvs are not modified.

people, _ := simplecsv.ReadCsvFile("people.csv") // columns: ID, Name
ages, _ := simplecsv.ReadCsvFile("ages.csv")     // columns: ID, Age
joined, fieldExistsInBoth := people.JoinByField(ages, "ID")

fieldExistsInBoth is false if the field does not exist in one of the csvs.

Left join

LeftJoinByField is like JoinByField, but the rows of the first csv without a match in the second one are also in the result, with empty cells in the columns of the second csv:

joined, fieldExistsInBoth := people.LeftJoinByField(ages, "ID")
Join on different key names

Real files rarely share the same key name (id vs customer_id). Join and LeftJoin join by one field name per csv, so no rename is needed: the result is the same as joining on a common name. The join column of the second csv is dropped from the result; the join column of the first csv is kept.

orders, _ := simplecsv.ReadCsvFile("orders.csv") // columns: id, amount
joined, bothFieldsExist := people.Join(orders, "id", "customer_id")

bothFieldsExist is false if one of the fields does not exist in its csv.

LeftJoin is like Join, but the rows of the first csv without a match in the second one are also in the result, with empty cells in the columns of the second csv:

joined, bothFieldsExist := people.LeftJoin(orders, "id", "customer_id")
Right join

RightJoin is like Join, but every row of the second csv is in the result: rows without a match in the first csv are included with empty cells in the columns of the first csv, and rows of the first csv without a match are dropped. The rows of the second csv that had no match are appended at the end. Use it when the second file is the driver (e.g. a master product list):

joined, bothFieldsExist := people.RightJoin(orders, "id", "customer_id")
Full join

FullJoin is like Join, but every row of both csvs is in the result: rows without a match are included with empty cells in the columns of the other side. Use it to find unmatched keys on either side without two passes:

joined, bothFieldsExist := people.FullJoin(orders, "id", "customer_id")
Same name join

RightJoinByField and FullJoinByField are the same-name variants of RightJoin and FullJoin, like JoinByField:

joined, fieldExistsInBoth := people.RightJoinByField(ages, "ID")
joined, fieldExistsInBoth := people.FullJoinByField(ages, "ID")
Join on multiple columns

Composite keys (country + sku, date + store) are common. JoinOn, LeftJoinOn, RightJoinOn and FullJoinOn join on several columns at once: the i-th name of the first list must be a column of the first csv, the i-th name of the second list a column of the second csv, and the lists must have the same length (at least one name). A row of the first csv is joined to a row of the second when the values of all its left key columns are exactly equal to the values of the corresponding right key columns (case matters). The join key is built with a length-prefixed encoding, so values containing separators cannot collide. All the right key columns are dropped from the result; the left key columns are kept.

orders, _ := simplecsv.ReadCsvFile("orders.csv") // columns: country, sku, amount
prices, _ := simplecsv.ReadCsvFile("prices.csv") // columns: country, sku, price
joined, keysExist := orders.JoinOn(prices, []string{"country", "sku"}, []string{"country", "sku"})

keysExist is false if a csv is empty, the lists have different lengths or are empty, one of the fields does not exist in its csv, or the result headers would collide.

LeftJoinOn keeps the rows of the first csv without a match; RightJoinOn keeps every row of the second csv, appending the unmatched ones at the end with empty cells in the columns of the first csv; FullJoinOn keeps every row of both csvs. When both field lists have a single name, the *On functions are equivalent to their Join / LeftJoin / RightJoin / FullJoin counterparts.

Merge columns

Sometimes two csvs are already row-aligned (same order, same length) and only need their columns glued side by side; a join would be wrong or wasteful. MergeColumns places the columns of the second csv next to the columns of the first, row by row: the result has the header row of the first csv followed by the header row of the second, and each data row is the data row of the first csv followed by the data row of the second in the same position. Both csvs must have the same number of rows (header included) — padding is not allowed — and no header name of the second csv may collide with a header name of the first:

customers, _ := simplecsv.ReadCsvFile("customers.csv") // columns: id, name
emails, _ := simplecsv.ReadCsvFile("emails.csv")       // columns: email, verified
merged, wasMerged := customers.MergeColumns(emails)

wasMerged is false if a csv is empty, the row counts differ, or the result headers would collide.

Diff

"What changed between yesterday's export and today's?" DiffByKey compares the data rows of two csvs keyed by a column. added are the data rows of the second csv whose key is not in the first, removed are the data rows of the first csv whose key is not in the second, and changed are the data rows of the first csv whose key exists in the second but whose full row differs. changed keeps the first csv's headers and values; join on the key if you need the second csv's values:

yesterday, _ := simplecsv.ReadCsvFile("yesterday.csv")
today, _ := simplecsv.ReadCsvFile("today.csv")
added, removed, changed, ok := yesterday.DiffByKey(today, "id")

Each result keeps the rows in order of appearance in its source csv and is an independent csv, so it can be written out or inspected on its own. The key values must be unique within each csv: a key repeated in either csv makes the diff ambiguous and ok is false. ok is also false if either csv is empty or the key is not a header of either csv. A csv with only the header row is a valid input: all the other side's data rows are then added or removed.

Error variants

A boolean ok tells you that something failed but not why: "did the join fail because a field is missing or because the result headers would collide?" The E variants of the functions where the reason matters most return an error instead, with a message prefixed with simplecsv: that describes the problem. The bool-returning functions are unchanged, and the E variants return the same csv the bool variants return on failure (an independent copy of the receiver for methods; nil for ConcatE / ConcatByNameE):

joined, err := people.JoinE(orders, "id", "customer_id")
if err != nil {
	log.Fatal(err) // e.g. simplecsv: cannot join: key field "customer_id" not found in the right csv
}

LeftJoinE is the error variant of LeftJoin. The join errors say which csv is empty, which key field is missing in which csv, or which right columns would collide with left columns.

GroupByE is the error variant of GroupBy: it says whether the csv is empty, no keys or aggregations are given, a key appears more than once, a key or aggregation field does not exist, an As name is empty or duplicates another key or As name, or an aggregation operation is invalid:

summary, err := sales.GroupByE(
	[]string{"country"},
	[]simplecsv.Agg{{Field: "amount", Op: simplecsv.AggSum, As: "total"}},
)
if err != nil {
	log.Fatal(err)
}

ConcatE and ConcatByNameE are the error variants of Concat and ConcatByName: they say whether no csv has a header row, a csv has different headers (ConcatE), or a csv has duplicate header names (ConcatByNameE):

combined, err := simplecsv.ConcatE(day1, day2, day3)
if err != nil {
	log.Fatal(err)
}
Boolean functions

Use boolean functions AND, OR and NOT to combine indexes and produce other complex indexes that point to rows in the csv. Very useful to produce search results.

Indexes are slices of row numbers (integers between 0 or 1 and the length of the csv -1). The indexes returned by OrIndex, AndIndex and NotIndex are sorted in ascending order.

OR indexes
var w []int
w = simplecsv.OrIndex(a,b)
w = simplecsv.OrIndex(a,b,c,d,e)

OrIndex accepts any number of operands. With no operands it returns an empty slice; with one operand it returns a sorted, de-duplicated copy. With 2 or more operands it returns their union.

AND indexes
var p []int
p = simplecsv.AndIndex(a,b)
p = simplecsv.AndIndex(a,b,c,d,e)

AndIndex accepts any number of operands. With no operands it returns an empty slice; with one operand it returns a sorted, de-duplicated copy. With 2 or more operands it returns their intersection.

NOT an index

The code below returns the negative of the index g, between row 1 and row 4. If g is an index with the values {1, 2} the negative of g is {3, 4}. Because 3 and 4 are the integers between 1 and 4 that are not in g.

var g []int
min := 1
max := 4
p = simplecsv.NotIndex(g, min, max)

Note: For csvs with headers the min value is usually 1 and for csvs without headers the min value is usually 0.

Only

Only are 2 functions to simplify and sort a csv.

Only this rows

It removes rows that are not in the index and reorders a CSV by the index order. If header is true, it starts by the csv header. Note: if header is true and the index contains 0, the header row appears twice in the result (once as the header, once as row 0).

newIndex := []int{1,3}
header := true
x, _ = x.OnlyThisRows(newIndex, header)
Only this fields

Removes fields that are not in the list of fields, reorders the CSV by the list of fields and adds fields that do not exist as blank fields.

fieldsList := []string{"Age","ID"}
x, _ = x.OnlyThisFields(fieldsList)

Documentation

Overview

© 2017-2026 Osvaldo Gago

The Go package simplecsv is a library to handle csv files without using a database.

Some notes:

- all read methods return the value in the csv and a second true/false value that is true if the value exists

- all write methods that change the csv return the changed csv and a true/false value if the operation was successful

- all cells are strings

- methods that return a SimpleCsv never modify the original csv and share no data with it: the returned csv is an independent copy, and get methods like GetRow and GetHeaders return copies

- Simplecsv works with comma separated csv files.

- header names (the first row) must be unique, like database columns. Creating a csv with duplicate headers, reading a file whose first row has duplicate headers, or renaming a header to a name that already exists is rejected. This guarantees that name-based lookups (GetCellByField, FindInField, SortByField, GetRowAsMap, the *FromMap functions, etc.) address exactly one column.

- all rows have the same number of cells as the header row (uniform row width). Reading a ragged file (rows with more or fewer fields than the header row) is rejected with an error, and Write methods that add or set rows reject a row whose length is not the header width. This makes "the number of columns" unambiguous and keeps AddRow, SetRow, the *FromMap functions, joins, OnlyThisFields, etc. consistent.

- ReadCsv, ReadCsvFile, ReadCsvFileE and ReadCsvFileComma read the whole input into memory. Use ReadCsvLimit with positive record and byte limits for large or attacker-controlled input.

- find and match (FindInColumn, FindInField, MatchInColumn, MatchInField) are case-insensitive by default: Foo = foo = FOO. Use the *CaseSensitive variants (FindInColumnCaseSensitive, FindInFieldCaseSensitive, MatchInColumnCaseSensitive, MatchInFieldCaseSensitive) when case matters.

- header names require at least one column: CreateEmptyCsv and OnlyThisFields reject an empty header list.

- CSV / formula injection: values that begin with =, +, -, @, a tab, a carriage return, a line feed, or the full-width variants =, +, -, @ are interpreted as formulas by spreadsheet applications (Excel, LibreOffice Calc, Google Sheets). The Write* functions write values verbatim and so do not neutralize such values, because escaping changes data. When the csv may contain attacker-controlled data (exported logs, form input, scraped content) and may be opened in a spreadsheet, call SanitizeFormulas first to prefix those cells with a single quote '; callers that do not want their bytes changed should not call it.

- writes are atomic and not world-readable: WriteCsvFile / WriteCsvFileE / WriteCsvFileComma write the csv to a temporary file in the same directory as the destination and then rename it over the destination, so a failed or interrupted write cannot truncate or corrupt the destination. The file (new or overwritten) ends up with mode 0600 (owner-readable only); callers that need different permissions can os.Chmod the result. Because the destination is replaced with os.Rename rather than opened, a symlink planted at the destination is replaced instead of followed.

* CSV FILE *

READ

Reads file and parses as a SimpleCsv object. `fileRead` is false if there's an error reading the file or parsing the CSV, if the file's header row contains duplicate names (headers must be unique), or if the file is ragged (rows with a different number of fields than the header row are rejected; uniform row width is an invariant).

var x simplecsv.SimpleCsv
x, fileRead = simplecsv.ReadCsvFile("my1file.csv")

All read functions (file-based and reader-based, limited or not) strip a UTF-8 byte order mark (BOM) from the first cell of the first row if present. Excel and many Windows tools write one (EF BB BF) at the start of the file; without stripping, the first header would become "\ufeffid" and every field lookup would silently fail. Only the first cell of the first row is touched.

CREATE

Create empty file and define csv headers. Header names must be unique; if they are not, the returned error is non-nil:

var u simplecsv.SimpleCsv
var err error
u, err = simplecsv.CreateEmptyCsv([]string{"Age", "Gender", "ID"})
if err != nil {
    log.Fatal(err)
}

`MustCreateEmptyCsv` is the same but panics when the headers are invalid (empty or duplicated). Handy in tests and small scripts where a valid header list is part of the program, not a runtime condition:

u = simplecsv.MustCreateEmptyCsv([]string{"Age", "Gender", "ID"})

WRITE

Write the SimpleCsv object to my2file.csv. If there's an error, `wasWritten` is false.

wasWritten := u.WriteCsvFile("my2file.csv")

The write is atomic (the csv is written to a temp file in the same directory and renamed over the destination, so a failed or interrupted write cannot corrupt the destination), the file ends up with mode 0600 (owner-readable only), and a symlink at the destination is replaced rather than followed.

SANITIZE (CSV / FORMULA INJECTION)

The Write* functions write values verbatim. A value that begins with `=`, `+`, `-`, `@`, a tab, a carriage return, a line feed, or the full-width variants `=`, `+`, `-`, `@` is interpreted as a formula by spreadsheet applications (Excel, LibreOffice Calc, Google Sheets), and can be used to execute commands or read other cells when the file is opened. This is a real risk when the csv contains data that may be attacker-controlled (exported logs, form input, scraped content) and is later opened in a spreadsheet.

`SanitizeFormulas` returns an independent copy of the csv in which every such cell is prefixed with a single quote `'` (the spreadsheet convention), so the value is treated as text. Escaping changes data (for example `-5` becomes `'-5`, though the spreadsheet hides the quote and displays `-5` as text), so sanitizing is opt-in. The original csv is not modified.

safe := u.SanitizeFormulas()
wasWritten := safe.WriteCsvFile("export.csv")
err := safe.WriteTo(os.Stdout, ',')

ERRORS

`ReadCsvFileE` and `WriteCsvFileE` work like `ReadCsvFile` and `WriteCsvFile` but return an error with the reason of the failure:

x, err = simplecsv.ReadCsvFileE("my1file.csv")
err := u.WriteCsvFileE("my2file.csv")

The joins, `GroupBy` and `Concat` have their own `E` variants; see ERROR VARIANTS below.

`ReadCsv` and the file-reading wrappers read the whole input into memory. For large or attacker-controlled input, `ReadCsvLimit` reads one record at a time and rejects input over the configured limits. `maxRecords` includes the header row, `maxBytes` limits input bytes, zero disables the corresponding limit, and negative limits are rejected:

file, err := os.Open("large.csv")
if err != nil {
    log.Fatal(err)
}
defer file.Close()
x, err := simplecsv.ReadCsvLimit(file, ',', 10000, 10*1024*1024)
if err != nil {
    log.Fatal(err)
}

DELIMITERS, READERS AND WRITERS

Simplecsv uses `,` as the default field separator, but it can read and write files with other separators, like `;` or tabs, and it can read from any io.Reader and write to any io.Writer:

x, err = simplecsv.ReadCsvFileComma("semicolonfile.csv", ';')
x, err = simplecsv.ReadCsv(os.Stdin, '\t')
err = x.WriteCsvFileComma("semicolonfile.csv", ';')
err = x.WriteTo(os.Stdout, ',')

* HEADERS *

The cells of the first row are considered headers.

GET

Get all headers:

headers := x.GetHeaders()

Get header at position one (second position as it starts from 0):

headerName, headerExists := x.GetHeader(1)

Get header position: (it returns `-1` if the header does not exist)

position := x.GetHeaderPosition("Gender")

RENAME

Rename header: (old header, new header)

x, headerExists := x.RenameHeader("ID", "IDnumber")

`headerExists` is false if the old header does not exist, or if `IDnumber` already exists as another column: header names must stay unique, so a rename that would produce a duplicate is rejected. Renaming a header to its own name is a no-op and is allowed.

* ROWS *

GET

Get number of rows:

numberOfRows := x.GetNumberRows()

Get second row:

row, rowExists := x.GetRow(1)

Get second row as a map:

row, rowExists := x.GetRowAsMap(1)

ADD

Add a slice to a row. The slice must have the same size as the CSV number of columns. If not wasSuccessful is false.

x, wasSuccessful = x.AddRow([]string{"24", "M", "2986732"})

Add row from map: (If the map keys don't exist as columns, the value will be discarded. If a key does not exist, it will create empty cells.)

mymap := make(map[string]string)
mymap["Age"] = "62"
mymap["Gender"] = "F"
mymap["ID"] = "6463246"
x, wasAdded = x.AddRowFromMap(mymap)

SET

Set second row (1) from a slice. The length of the slice must be the same as the number of columns and the row must already exist. If there’s an error `wasSet` is false.

x, wasSet = x.SetRow(1, []string{"45", "F", "8356138"})

Set second row from map: If the map keys don't exist as columns, the value will be discarded. If a key does not exist, it will create empty cells.)

mymap2 := make(map[string]string)
mymap2["Age"] = "62"
mymap2["Gender"] = "F"
mymap2["ID"] = "6463246"
x, wasAdded = x.SetRowFromMap(1, mymap2)

UPDATE

Unlike `SetRowFromMap`, `UpdateRowCellsFromMap` does not erase the cells value just because the column names are not keys in the map. It updates the cells that have the column name in the map and maintains the value of all the others.

To update the age in row 1:

mymap3 := make(map[string]string)
mymap3["Age"] = "63"
x, wasUpdated = x.UpdateRowCellsFromMap(1, mymap3)

DELETE

Delete second row: (If the row number is invalid, `wasDeleted` is false)

x, wasDeleted = x.DeleteRow(1)

DATA ROWS

Get the number of data rows, excluding the header row (unlike `GetNumberRows`, which includes it):

numberOfDataRows := x.GetNumberDataRows()

Get all the data rows as copies (the header row is not included; changes to the returned rows don't affect the csv):

dataRows := x.GetDataRows()

Iterate over the data rows without touching the header: `EachDataRow` calls `fn` once per data row, in csv order, with the row index (starting at 1), a copy of the row, and a map view of it (header name to cell value). Mutating the row or the map inside `fn` does not affect the csv. `fn` may return `false` to stop the iteration early:

x.EachDataRow(func(rowIndex int, row []string, asMap map[string]string) bool {
    if asMap["status"] == "stop" {
        return false // stop the iteration
    }
    fmt.Printf("row %d: %v\n", rowIndex, row)
    return true
})

SLICE

`Head` returns the header row plus the first `n` data rows (or all of them if there are fewer). `n` negative or zero means no data rows:

sample := x.Head(10)

`Tail` returns the header row plus the last `n` data rows:

recent := x.Tail(10)

`SliceRows` returns the header row plus the data rows whose csv row index is in `[start, end)` (half-open, data rows start at index 1). `end` beyond the last row is clamped. `wasSliced` is false if the csv is empty, `start < 1` or `end < start`:

x, wasSliced = x.SliceRows(1, 3) // data rows 1 and 2

APPEND

`AppendRows` appends the data rows of another csv with the same headers (same names, same order). `wasAppended` is false if either csv is empty or the headers differ:

x, wasAppended = x.AppendRows(anotherCsv)

CONCAT

`Concat` stacks several csvs vertically in one call: the first non-empty csv supplies the headers and every csv with a header row must have the same headers (same names, same order); empty csvs are skipped. If there is no non-empty csv or the headers differ, it returns nil and false:

combined, ok := simplecsv.Concat(day1, day2, day3)

CONCAT BY NAME

Monthly or vendor exports often add or reorder columns, and strict `Concat` rejects them. `ConcatByName` stacks csvs vertically aligning the columns by header name instead of by position: the result header row is the header row of the first non-empty csv, followed by the header names that only appear in later csvs, in order of first appearance. Each data row is copied with the value of each column placed under its header name, and an empty string where the csv has no column with that name. `ok` is false if there is no non-empty csv, or a csv has duplicate header names (the result would be ambiguous). When every csv has the same headers in the same order, `ConcatByName` is equivalent to `Concat`:

january, _ := simplecsv.ReadCsvFile("january.csv")   // columns: id, name
february, _ := simplecsv.ReadCsvFile("february.csv") // columns: name, bonus
combined, ok := simplecsv.ConcatByName(january, february)

DEDUPLICATE

`Unique` removes exact duplicate data rows: for each full row (all columns) only the first occurrence is kept, in csv order. The header row is kept:

unique := x.Unique()

`UniqueByFields` removes data rows that share the same key built from the named columns; the first row per key is kept. `wasUnique` is false if the csv is empty, no fields are given, or a field name does not exist:

x, wasUnique = x.UniqueByFields("email")
x, wasUnique = x.UniqueByFields("country", "city")

* DISTINCT AND COUNTS *

`Distinct` returns the distinct values of the data cells of a column, in order of first appearance. The header cell is never included and the returned slice is a copy:

values, wasRead := x.Distinct("Country")

`wasRead` is false if the csv is empty or the field name does not exist. A csv with only the header row returns an empty slice and true.

`ValueCounts` returns a new csv with the fixed headers `value` and `count`: one row per distinct value of the column, in order of first appearance, with the number of data rows that have that value, as a decimal string. The header cell is never counted and the original csv is not modified:

counts, wasRead := x.ValueCounts("Country")

`wasRead` is false if the csv is empty or the field name does not exist; in that case a copy of the csv is returned.

* GROUP BY *

`GroupBy` collapses the data rows into one output row per group, where a group is all the data rows whose key fields have the same values. The output header row is the key field names followed by the `As` name of each aggregation; each output data row is the key values followed by one cell per aggregation. Groups appear in order of first occurrence:

type Agg struct {
    Field string // source column; ignored for AggCount
    Op    AggOp
    As    string // output header; required; must be unique among keys+As
}

The aggregation operations are:

  • `AggCount` counts the data rows of the group. `Field` is ignored and may be empty.
  • `AggSum` adds the `Field` cells parsed with `strconv.ParseFloat`; cells that don't parse as numbers count as 0.
  • `AggMin` / `AggMax` use only finite numeric cells (NaN and infinities are skipped); a group without any finite numeric cell produces an empty cell.
  • `AggFirst` / `AggLast` take the `Field` value of the first and last data row of the group.
  • `AggJoin` joins the non-empty `Field` values of the group with ",".

Numeric results are formatted with `strconv.FormatFloat(f, 'f', -1, 64)`, so integers have no decimal noise:

summary, wasGrouped := sales.GroupBy(
    []string{"country"},
    []Agg{{Op: AggCount, As: "orders"}, {Field: "amount", Op: AggSum, As: "total"}},
)

`wasGrouped` is false if the csv is empty, no keys or no aggregations are given, a key or aggregation field does not exist, an `As` name is empty or duplicates another key or `As` name, or an aggregation operation is not one of the `AggOp` constants; in that case a copy of the csv is returned.

* CELLS *

GET

Get value of the cell in the second column, second row:

cellValue, cellExists := x.GetCell(1, 1)

Get the value of the cell in the column "Age", second row:

cellValue, cellExists := x.GetCellByField("Age", 1)

SET

Change the value of the cell in the first column (0) and the second row (1) to "27":

x, wasChanged = x.SetCell(0, 1, "27")

The same, using the column name instead of the column position:

x, wasChanged = x.SetCellByField("Age", 1, "27")

REPLACE IN FIELD

Replace the data cells of a column whose value is exactly equal to `old` (case-sensitive, whole cell) with `new`. The header cell is not modified. Useful to fix known bad values (`N/A` to empty) or rename codes:

x, wasReplaced = x.ReplaceInField("Status", "N/A", "")

TRIM

Remove the surrounding whitespace of every cell, including the header cells, with strings.TrimSpace:

x, wasTrimmed = x.TrimSpace()

`wasTrimmed` is false if trimming makes two header names equal: header names must stay unique.

* COLUMNS *

ADD

Add a column at the end of the CSV:

x, wasSuccessful = x.AddEmptyColumn("NewColumn")

REMOVE

Remove the column at position 1 (second column, because it's zero based):

x, wasRemoved = x.RemoveColumn(1)

Remove a column by name:

x, wasRemoved = x.RemoveColumnByName("Gender")

GET COLUMN VALUES

Get a copy of all the data cells of a column, skipping the header cell. The returned slice is a copy: changes to it don't affect the csv. `columnExists` is false if the csv is empty or the column position is not valid:

columnCells, columnExists := x.GetColumn(1)
columnCells, fieldExists := x.GetColumnByField("Age")

SET COLUMN VALUES

Replace the data cells of a column with a slice. The slice must have the same length as the number of data rows (the csv length minus the header row), because every data row must keep exactly one cell in the column. The header cell is not modified:

x, wasSet = x.SetColumn(1, []string{"24", "62", "45"})
x, wasSet = x.SetColumnByField("Age", []string{"24", "62", "45"})

MAP COLUMN VALUES

Transform the data cells of a column with a function. The function receives the cell value and the csv row index (1 for the first data row); the header cell is not transformed. The original csv is not modified:

x, wasMapped = x.MapColumnByField("Name", func(value string, row int) string {
    return strings.ToUpper(value)
})

FILL COLUMN VALUES

Set every data cell of a column to the same value. Useful for constants and defaults:

x, wasFilled = x.FillColumnByField("Source", "import")

* COLUMN SHAPE *

ADD AT POSITION

`AddEmptyColumnAt` inserts an empty column at a position: `index` is the position of the new column and must be between 0 and the number of columns (inclusive; inserting at the number of columns appends, like `AddEmptyColumn`). The header cell of the new column is `columnName` and every data cell is empty:

x, wasAdded = x.AddEmptyColumnAt("Source", 0)
x, wasAdded = x.AddEmptyColumnAt("Notes", 3)

`wasAdded` is false if the csv is empty, the column name already exists or `index` is out of range.

ADD WITH VALUES

`AddColumnByField` appends a column with the header name `columnName` and the given data cells. The values slice must have the same length as the number of data rows (the csv length minus the header row), because every data row must keep exactly one cell in the column; a csv with only the header row takes an empty values slice. The values are copied:

x, wasAdded = x.AddColumnByField("Total", []string{"24", "62", "45"})

`wasAdded` is false if the csv is empty, the column name already exists or the length of the values slice does not match the number of data rows.

COMPUTE FROM ROW MAP

`AddComputedColumn` appends a column whose data cells are computed by a function from the values of the other columns. The function is called once per data row, in csv order, with a map from header name to cell value: the map is built fresh for every row and is a copy, so the callback can read field names instead of column indexes, and mutating the map does not affect the csv. The string the function returns becomes the cell of the new column:

x, wasComputed = x.AddComputedColumn("full_name", func(row map[string]string) string {
    return row["first"] + " " + row["last"]
})

`wasComputed` is false if the csv is empty or the column name already exists; in that case a copy of the csv is returned. A csv with only the header row takes the new header cell only and the function is not called.

RENAME BATCH

`RenameHeaders` renames several headers in one call. Every key of the map must be an existing header, and the result must not contain duplicate header names: a rename fails if a key does not exist, two keys map to the same new name, or a renamed column collides with a header that is not renamed. The renames are applied simultaneously, so a swap ("a" -> "b", "b" -> "a") is applied correctly and map iteration order does not matter:

x, wasRenamed = x.RenameHeaders(map[string]string{"ID": "IdNumber", "Date": "Fecha"})

`wasRenamed` is false if the csv is empty or the renamed headers would contain duplicates. Renaming a header to its own name is a no-op for that column, and an empty map is a no-op that succeeds.

MOVE

`MoveColumn` moves the column with the header name `columnName` to position `newIndex` (0 to the number of columns minus 1), shifting the columns in between: the header and data cells of the moved column move together:

x, wasMoved = x.MoveColumn("Age", 0)

`wasMoved` is false if the csv is empty, the column name does not exist or `newIndex` is out of range. Moving a column to its own position is a no-op and succeeds.

SPLIT A FIELD

`SplitField` replaces the column with the header name `name` by `len(newNames)` new columns in its position. Every data cell is split on `sep` with `strings.Split`, and the parts fill the new columns in order: if a cell has fewer parts than `newNames`, the remaining new columns get empty cells; if it has more parts, the extra parts are dropped. The original column is removed, so a new name may reuse it but must not collide with any other header:

x, wasSplit = x.SplitField("coord", ",", []string{"lat", "lon"})
x, wasSplit = x.SplitField("last;first", ";", []string{"last", "first"})

`wasSplit` is false if the csv is empty, the field name does not exist, `newNames` is empty or contains duplicates, or a new name collides with another header.

COMBINE FIELDS

`CombineFields` appends a column with the header name `as` whose data cells join the values of the named columns with `sep` (via `strings.Join`). The source columns stay in place:

x, wasCombined = x.CombineFields([]string{"first", "last"}, " ", "full_name")
x, wasCombined = x.CombineFields([]string{"country", "sku"}, "-", "country_sku")

`wasCombined` is false if the csv is empty, `names` is empty, a name does not exist, `names` repeats a field, or `as` already exists as a header.

* FIND *

FIND IN COLUMN

Find the word "27" in the first column (column 0):

rowsWithWord, validColumn := x.FindInColumn(0, "27")

It returns a slice of row numbers (int) where you can find the word in the column position. Please note that in simplecsv all cells are strings.

If it doesn't find the value, it returns an empty slice.

In `FindInColumn` and in `FindInField` case does not matter. Foo = foo = FOO.

FIND IN FIELD

The same as `FindInColumn` but using a column/field name instead of position. Please note that `FindInField`, unlike `FindInColumn` never includes the header in the search result.

rowsWithWord, validFieldName := x.FindInField("Age", "27")

If the field name does not exist, the second value returned (`validFieldName`) is false.

MATCH IN COLUMN

Find where results match a regular expression in the third column (column 2):

rowsWithWord, areParamsOk := x.MatchInColumn(2, "p([a-z]+)ch$")

Use ^ and $ in the regular expression to match exact results.

Matching is case-insensitive by default: Foo = foo = FOO. (This is achieved by prefixing the (?i) flag to the expression; you can still re-enable case-sensitivity inside the pattern with (?-i), or use MatchInColumnCaseSensitive for the whole match.)

MATCH IN FIELD

Same as with MatchInColumn, but with a field (column name). Find where results match a regular expression in the column "ID":

rowsWithWord, areParamsOk := x.MatchInField("ID", "p([a-z]+)ch$")

Please note that `MatchInField`, unlike `MatchInColumn` never includes the header in the search result.

Matching is case-insensitive by default: Foo = foo = FOO.

CASE SENSITIVE FIND

`FindInColumn`, `FindInField`, `MatchInColumn` and `MatchInField` ignore case by default. Use the `*CaseSensitive` variants when case matters:

rowsWithWord, validFieldName = x.FindInFieldCaseSensitive("Name", "Ana")
rowsWithWord, areParamsOk = x.MatchInFieldCaseSensitive("ID", "p([a-z]+)ch$")

* SORT *

SORT BY FIELD

Sorts the csv by a column name and returns a new sorted csv. The header row stays at the top and the original csv is not modified. Finite numbers are sorted numerically among themselves and appear before non-numeric values (including NaN and Inf), which are sorted as strings. The sort is stable.

sortedCsv, fieldExists := x.SortByField("Age", true)

SORT BY COLUMN

The same as `SortByField` but using a column position. It sorts all the rows, including the first one:

sortedCsv, validColumn := x.SortByColumn(0, true)

SORT BY MULTIPLE FIELDS

Sorts the csv by several columns, one level per field in order, like `ORDER BY` in SQL: a later field is only compared when every earlier field compares equal. The header row stays at the top and the original csv is not modified. Each field uses the same ordering as `SortByField` (finite numbers numerically, other values as strings), and the sort is stable. An empty `ascending` sorts every field ascending; otherwise it has one bool per field (`true` ascending, `false` descending):

sortedCsv, wereFieldsFound := x.SortByFields([]string{"Country", "City"}, nil)
sortedCsv, wereFieldsFound = x.SortByFields([]string{"Country", "City"}, []bool{true, false})

`wereFieldsFound` is false if the csv is empty, no fields are given, a field name does not exist, or the length of `ascending` is neither 0 nor the number of fields; in that case a copy of the csv is returned.

SORT AN INDEX

`SortIndex` returns a sorted copy of an index:

sortedIndex := simplecsv.SortIndex([]int{5, 1, 3})

* FILTER ROWS *

`FilterRows` returns a new csv with the rows where the predicate function returns true. If `header` is true, the first row is kept as the header:

adults := x.FilterRows(func(row []string) bool {
    age, _ := strconv.Atoi(row[1])
    return age >= 18
}, true)

FIELD FILTERS

`FilterByField` keeps the data rows where the predicate returns true for the named column. The header cell is never passed to the predicate, and the field name is used instead of a column index:

adults, wasFiltered := x.FilterByField("age", func(value string) bool {
    n, _ := strconv.Atoi(value)
    return n >= 18
})

`Where` keeps the data rows where the named column is exactly equal to `value` (case-sensitive). `wasWhere` is false if the csv is empty or the field name does not exist:

active, wasWhere := x.Where("status", "active")

`WhereFold` is the same but case-insensitive: `Active`, `active` and `ACTIVE` all match:

active, wasWhereFold := x.WhereFold("status", "active")

* JOIN *

INNER JOIN

`JoinByField` joins two csvs by a common field and returns a new csv with the rows where the field has the same value in both csvs. The result has all the columns of the first csv, followed by the columns of the second csv except the join column. Non-join columns of the second csv must not share names with columns of the first; a collision is rejected so headers stay unique. If a join value shows up more than once, all combinations of rows are in the result. Values are compared exactly: case matters. The source csvs are not modified.

people, _ := simplecsv.ReadCsvFile("people.csv") // columns: ID, Name
ages, _ := simplecsv.ReadCsvFile("ages.csv")     // columns: ID, Age
joined, fieldExistsInBoth := people.JoinByField(ages, "ID")

`fieldExistsInBoth` is false if the field does not exist in one of the csvs.

LEFT JOIN

`LeftJoinByField` is like `JoinByField`, but the rows of the first csv without a match in the second one are also in the result, with empty cells in the columns of the second csv:

joined, fieldExistsInBoth := people.LeftJoinByField(ages, "ID")

JOIN ON DIFFERENT KEY NAMES

Real files rarely share the same key name (`id` vs `customer_id`). `Join` and `LeftJoin` join by one field name per csv, so no rename is needed: the result is the same as joining on a common name. The join column of the second csv is dropped from the result; the join column of the first csv is kept.

orders, _ := simplecsv.ReadCsvFile("orders.csv") // columns: id, amount
joined, bothFieldsExist := people.Join(orders, "id", "customer_id")

`bothFieldsExist` is false if one of the fields does not exist in its csv. `LeftJoin` is like `Join`, but the rows of the first csv without a match in the second one are also in the result, with empty cells in the columns of the second csv:

joined, bothFieldsExist := people.LeftJoin(orders, "id", "customer_id")

RIGHT JOIN

`RightJoin` is like `Join`, but every row of the second csv is in the result: rows without a match in the first csv are included with empty cells in the columns of the first csv, and rows of the first csv without a match are dropped. The rows of the second csv that had no match are appended at the end. Use it when the second file is the driver (e.g. a master product list):

joined, bothFieldsExist := people.RightJoin(orders, "id", "customer_id")

FULL JOIN

`FullJoin` is like `Join`, but every row of both csvs is in the result: rows without a match are included with empty cells in the columns of the other side. Use it to find unmatched keys on either side without two passes:

joined, bothFieldsExist := people.FullJoin(orders, "id", "customer_id")

SAME NAME JOIN

`RightJoinByField` and `FullJoinByField` are the same-name variants of `RightJoin` and `FullJoin`, like `JoinByField`:

joined, fieldExistsInBoth := people.RightJoinByField(ages, "ID")
joined, fieldExistsInBoth := people.FullJoinByField(ages, "ID")

JOIN ON MULTIPLE COLUMNS

`JoinOn`, `LeftJoinOn`, `RightJoinOn` and `FullJoinOn` join on several columns at once, for composite keys like `country` + `sku` or `date` + `store`. The i-th name of the first list must be a column of the first csv, the i-th name of the second list a column of the second csv, and the two lists must have the same length (at least one name). A row of the first csv is joined to a row of the second when the values of all its left key columns are exactly equal to the values of the corresponding right key columns (case matters). The join key is built with a length-prefixed encoding, so values containing separators cannot collide. All the right key columns are dropped from the result; the left key columns are kept. `wasJoined` is false if a csv is empty, the lists have different lengths or are empty, a field does not exist in its csv, or the result headers would collide:

orders, _ := simplecsv.ReadCsvFile("orders.csv")  // columns: country, sku, amount
prices, _ := simplecsv.ReadCsvFile("prices.csv")  // columns: country, sku, price
joined, wasJoined := orders.JoinOn(prices, []string{"country", "sku"}, []string{"country", "sku"})

`LeftJoinOn` is like `JoinOn`, but the rows of the first csv without a match are also in the result, with empty cells in the columns of the second csv. `RightJoinOn` keeps every row of the second csv, appending the unmatched ones at the end with empty cells in the columns of the first csv. `FullJoinOn` keeps every row of both csvs, with empty cells on the unmatched side. When both field lists have a single name, the `*On` functions are equivalent to their `Join` / `LeftJoin` / `RightJoin` / `FullJoin` counterparts.

MERGE COLUMNS

Sometimes two csvs are already row-aligned (same order, same length) and only need their columns glued side by side; a join would be wrong or wasteful. `MergeColumns` places the columns of other next to the columns of s, row by row: the result header row is the header row of s followed by the header row of other, and each result data row is the data row of s followed by the data row of other in the same position. Both csvs must have the same number of rows (header included); padding is not allowed. No header name of other may collide with a header name of s, so header names stay unique. `wasMerged` is false if a csv is empty, the row counts differ, or the result headers would collide:

customers, _ := simplecsv.ReadCsvFile("customers.csv") // columns: id, name
emails, _ := simplecsv.ReadCsvFile("emails.csv")       // columns: email, verified
merged, wasMerged := customers.MergeColumns(emails)

DIFF

"What changed between yesterday's export and today's?" `DiffByKey` compares the data rows of s (left) and other (right), keyed by the values of the column with the header name key. It returns three new csvs:

  • `added`: the data rows of other whose key is not present in s, with other's header row
  • `removed`: the data rows of s whose key is not present in other, with s's header row
  • `changed`: the data rows of s whose key is present in other but whose full row differs, with s's header row and s's values (join on the key if you need the right-side values)

Each result keeps the rows in order of appearance in its source csv and is an independent csv, so it can be written out or inspected on its own. The key values must be unique within each csv: a key repeated in s or in other makes the diff ambiguous, and it returns nil, nil, nil and false. It also returns nil, nil, nil and false if either csv is empty or the key is not a header name in either csv. A csv with only the header row is a valid input: all the other side's data rows are then `added` or `removed`:

yesterday, _ := simplecsv.ReadCsvFile("yesterday.csv")
today, _ := simplecsv.ReadCsvFile("today.csv")
added, removed, changed, ok := yesterday.DiffByKey(today, "id")

* ERROR VARIANTS *

A boolean `ok` tells you that something failed but not why: "did the join fail because a field is missing or because the result headers would collide?" The `E` variants of the functions where the reason matters most return an error instead, with a message prefixed with `simplecsv:` that describes the problem. The bool-returning functions are unchanged, and the `E` variants return the same csv the bool variants return on failure (an independent copy of the receiver for methods; nil for `ConcatE` and `ConcatByNameE`):

joined, err := people.JoinE(orders, "id", "customer_id")
if err != nil {
    log.Fatal(err) // e.g. simplecsv: cannot join: key field "customer_id" not found in the right csv
}
  • `JoinE` / `LeftJoinE` work like `Join` / `LeftJoin`. The error says which csv is empty, which key field is missing in which csv, or which right columns would collide with left columns:

    joined, err := people.LeftJoinE(orders, "id", "customer_id") if err != nil { log.Fatal(err) }

  • `GroupByE` works like `GroupBy`. The error says whether the csv is empty, no keys or aggregations are given, a key appears more than once, a key or aggregation field does not exist, an `As` name is empty or duplicates another key or `As` name, or an aggregation operation is invalid:

    summary, err := sales.GroupByE( []string{"country"}, []Agg{{Field: "amount", Op: AggSum, As: "total"}}, ) if err != nil { log.Fatal(err) }

  • `ConcatE` / `ConcatByNameE` work like `Concat` / `ConcatByName`. The error says whether no csv has a header row, a csv has different headers (`ConcatE`), or a csv has duplicate header names (`ConcatByNameE`):

    combined, err := simplecsv.ConcatE(day1, day2, day3) if err != nil { log.Fatal(err) }

* BOOLEAN FUNCTIONS *

Use boolean functions AND, OR and NOT to combine indexes and produce other complex indexes that point to rows in the csv. Very useful to produce search results.

Indexes are slices of row numbers (integers between 0 or 1 and the length of the csv -1). The indexes returned by OrIndex, AndIndex and NotIndex are sorted in ascending order.

OR

The `OrIndex` function accepts any number of operands. With no operands it returns an empty slice; with one operand it returns a sorted, de-duplicated copy. With 2 or more operands it returns their union:

var w []int
w = simplecsv.OrIndex(a, b)
w = simplecsv.OrIndex(a, b, c, d, e)

AND

The `AndIndex` function accepts any number of operands. With no operands it returns an empty slice; with one operand it returns a sorted, de-duplicated copy. With 2 or more operands it returns their intersection:

var p []int
p = simplecsv.AndIndex(a, b)
p = simplecsv.AndIndex(a, b, c, d, e)

NOT

The code below returns the negative of the index `g`, between row 1 and row 4. If `g` is an index with the values `{1, 2}` the negative of `g` is `{3, 4}`. Because 3 and 4 are the integers between 1 and 4 that are not in `g`.

var g []int
min := 1
max := 4
p = simplecsv.NotIndex(g, min, max)

Note: For csvs with headers the min value is usually 1 and for csvs without headers the min value is usually 0.

* ONLY *

Only are 2 functions to simplify and sort a csv.

ONLY THIS ROWS

It removes rows that are not in the index and reorders a CSV by the index order. If header is true, it starts by the csv header. Note: if header is true and the index contains 0, the header row appears twice in the result (once as the header, once as row 0).

newIndex := []int{1, 3}
header := true
x, _ = x.OnlyThisRows(newIndex, header)

ONLY THIS FIELDS

Removes fields that are not in the list of fields, reorders the CSV by the list of fields and adds fields that do not exist as blank fields.

fieldsList := []string{"Age", "ID"}
x, _ = x.OnlyThisFields(fieldsList)

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AndIndex

func AndIndex(indexes ...[]int) []int

AndIndex operates an AND operator in the indexes: it returns the values present in every index, treating each index as a set (duplicates inside one index do not count as presence in the others) With no operands it returns an empty slice; with one operand it returns a sorted, de-duplicated copy. The result is sorted in ascending order

func NotIndex

func NotIndex(index []int, min, max int) []int

NotIndex negates the index between a min value and a max value The min value tipically is either 0 or 1 (without and with headers) The max value tipically is the csv length - 1 (number of rows -1). A table with a header and 2 rows (length = 3) can have indexes with values of 1 and 2

func OrIndex

func OrIndex(indexes ...[]int) []int

OrIndex operates an OR operator in the indexes With no operands it returns an empty slice; with one operand it returns a sorted, de-duplicated copy. The result is sorted in ascending order.

func SortIndex

func SortIndex(index []int) []int

SortIndex returns a sorted copy of an index in ascending order

Types

type Agg

type Agg struct {
	Field string // source column; ignored for AggCount
	Op    AggOp
	As    string // output header; required; must be unique among keys+As
}

Agg describes one aggregation: it produces one output column for every group, aggregated from the Field column of the source csv with the operation Op. As is the output header: it must be non-empty and unique among the key field names and the other As names.

type AggOp

type AggOp int

AggOp describes the operation an aggregation applies to the Field column of the source csv.

const (
	// AggCount counts the data rows of the group. Field is ignored and may
	// be empty.
	AggCount AggOp = iota
	// AggSum adds the Field cells parsed with strconv.ParseFloat; cells
	// that don't parse as numbers count as 0.
	AggSum
	// AggMin is the smallest finite numeric Field cell of the group; a
	// group without finite numeric cells produces an empty cell.
	AggMin
	// AggMax is the largest finite numeric Field cell of the group; a
	// group without finite numeric cells produces an empty cell.
	AggMax
	// AggFirst is the Field value of the first data row of the group.
	AggFirst
	// AggLast is the Field value of the last data row of the group.
	AggLast
	// AggJoin joins the non-empty Field values of the group with ",".
	AggJoin
)

type SimpleCsv

type SimpleCsv [][]string

SimpleCsv is the type for simple csv

func Concat

func Concat(csvs ...SimpleCsv) (SimpleCsv, bool)

Concat returns a new csv with the data rows of all the csvs stacked vertically: the result header row is the header row of the first non-empty csv, followed by copies of the data rows of every csv in argument order. Every csv with a header row must have exactly the same header names in the same order; empty csvs (without a header row) are skipped. The original csvs are not modified and the result shares no data with them. If there is no non-empty csv, or two csvs have different headers, it returns nil and false.

func ConcatByName

func ConcatByName(csvs ...SimpleCsv) (SimpleCsv, bool)

ConcatByName returns a new csv with the data rows of all the csvs stacked vertically, aligning the columns by header name instead of by position: the result header row is the header row of the first non-empty csv, followed by the header names that only appear in later csvs, in order of first appearance. Each data row of every csv is copied into the result with the value of each column placed under its header name, and an empty string where the csv has no column with that name. Empty csvs (without a header row) are skipped. The original csvs are not modified and the result shares no data with them. If there is no non-empty csv, or any csv has duplicate header names (the result would be ambiguous and break the unique-header invariant), it returns nil and false.

func ConcatByNameE

func ConcatByNameE(csvs ...SimpleCsv) (SimpleCsv, error)

ConcatByNameE is like ConcatByName, but it returns an error describing the reason of the failure instead of false. On failure it returns nil.

func ConcatE

func ConcatE(csvs ...SimpleCsv) (SimpleCsv, error)

ConcatE is like Concat, but it returns an error describing the reason of the failure instead of false. On failure it returns nil.

func CreateEmptyCsv

func CreateEmptyCsv(columnNames []string) (SimpleCsv, error)

CreateEmptyCsv creates an empty CSV with the headers passed as a slice. The headers are copied, changes to the original slice don't affect the csv. At least one header is required. Header names must be unique, like database columns: if columnNames is empty or contains duplicates the returned error is non-nil and the returned csv is nil.

func CreateEmpyCsv deprecated

func CreateEmpyCsv(columnNames []string) (SimpleCsv, error)

CreateEmpyCsv creates an empty CSV with the headers passed as a slice

Deprecated: use CreateEmptyCsv, this function was kept for compatibility with the misspelled name

func MustCreateEmptyCsv

func MustCreateEmptyCsv(headers []string) SimpleCsv

MustCreateEmptyCsv is like CreateEmptyCsv but panics if the headers are invalid (empty or duplicated). It is handy in tests and small scripts where the header list is part of the program, not a runtime condition:

people := simplecsv.MustCreateEmptyCsv([]string{"id", "name"})

func ReadCsv

func ReadCsv(r io.Reader, comma rune) (SimpleCsv, error)

ReadCsv reads all the records from r using comma as the field separator and returns a SimpleCsv or an error. The entire input is read into memory; use ReadCsvLimit for bounded reads.

The first record is treated as the header row. Header names must be unique, like database columns: a file with duplicate headers in the first row is rejected with an error. All rows must have the same number of fields as the header row: a ragged file is rejected with an error. This makes uniform row width an invariant of a SimpleCsv, alongside the unique-headers invariant.

A UTF-8 byte order mark (BOM, written by Excel and many Windows tools at the start of the file) is stripped from the first cell of the first row, so the first header is "id" and not "\ufeffid".

func ReadCsvFile

func ReadCsvFile(filename string) (SimpleCsv, bool)

ReadCsvFile reads a file and returns a SimpleCsv. The second value is false if there's an error reading the file or parsing the CSV.

func ReadCsvFileComma

func ReadCsvFileComma(filename string, comma rune) (SimpleCsv, error)

ReadCsvFileComma reads a delimited file (for example a semicolon or tab separated file) and returns a SimpleCsv or an error.

func ReadCsvFileE

func ReadCsvFileE(filename string) (SimpleCsv, error)

ReadCsvFileE reads a file and returns a SimpleCsv or an error.

func ReadCsvLimit

func ReadCsvLimit(r io.Reader, comma rune, maxRecords, maxBytes int64) (SimpleCsv, error)

ReadCsvLimit reads records from r using comma as the field separator and returns a SimpleCsv or an error. maxRecords is the maximum number of CSV records, including the header row, and maxBytes is the maximum number of input bytes passed to the CSV parser. A zero limit disables that limit; negative limits are rejected. If a limit is exceeded, the returned csv is nil.

Unlike ReadCsv, this function does not use csv.Reader.ReadAll. Use positive limits when reading input that may be large or attacker-controlled.

Like ReadCsv, a UTF-8 byte order mark (BOM) is stripped from the first cell of the first row if present.

func (SimpleCsv) AddColumnByField

func (s SimpleCsv) AddColumnByField(columnName string, values []string) (SimpleCsv, bool)

AddColumnByField appends a column with the header name columnName and the data cells values, and returns a new csv. len(values) must equal the number of data rows (len(s)-1), because every data row must keep exactly one cell in the column; a csv with only the header row takes an empty values slice. The values are copied: changes to the original slice don't affect the csv. The original csv is not modified and the result shares no data with it. If the csv is empty, the column name already exists or the length of values does not match, it returns a copy of the csv and false.

func (SimpleCsv) AddComputedColumn

func (s SimpleCsv) AddComputedColumn(columnName string, fn func(row map[string]string) string) (SimpleCsv, bool)

AddComputedColumn appends a column with the header name columnName whose data cells are computed by fn from the values of the other columns, and returns a new csv. fn is called once per data row, in csv order, with a map from header name to cell value: the map is built fresh for every row and is a copy, so the callback can read field names instead of column indexes, and mutating the map does not affect the csv. The string fn returns becomes the cell of the new column. A csv with only the header row takes the new header cell only and fn is not called. The original csv is not modified and the result shares no data with it. If the csv is empty or the column name already exists, it returns a copy of the csv and false.

func (SimpleCsv) AddEmptyColumn

func (s SimpleCsv) AddEmptyColumn(columnName string) (SimpleCsv, bool)

AddEmptyColumn adds an empty column at the end of the csv and returns a new csv. The original csv is not modified. columnName must not already exist as a header.

func (SimpleCsv) AddEmptyColumnAt

func (s SimpleCsv) AddEmptyColumnAt(columnName string, index int) (SimpleCsv, bool)

AddEmptyColumnAt inserts an empty column at position index and returns a new csv. index must be between 0 and the number of columns (inclusive): inserting at the number of columns appends, like AddEmptyColumn. The header cell of the new column is columnName and every data cell is empty. The original csv is not modified and the result shares no data with it. If the csv is empty, the column name already exists or index is out of range, it returns a copy of the csv and false.

func (SimpleCsv) AddRow

func (s SimpleCsv) AddRow(rowValue []string) (SimpleCsv, bool)

AddRow adds a row at the end of the csv and returns a new csv The row is copied, changes to the original slice don't affect the csv The original csv is not modified

func (SimpleCsv) AddRowFromMap

func (s SimpleCsv) AddRowFromMap(rowValue map[string]string) (SimpleCsv, bool)

AddRowFromMap adds map values to a row. Ignores keys with unexisting headers Fills blank where the key does not exists

func (SimpleCsv) AppendRows

func (s SimpleCsv) AppendRows(other SimpleCsv) (SimpleCsv, bool)

AppendRows returns a new csv with the data rows of other appended after the data rows of s. other must have exactly the same header names in the same order as s (other's header row itself is not appended). The original csvs are not modified and the result shares no data with them. If s or other is empty, or their headers differ, it returns a copy of s and false.

func (SimpleCsv) CombineFields

func (s SimpleCsv) CombineFields(names []string, sep, as string) (SimpleCsv, bool)

CombineFields appends a column with the header name as whose data cells join the values of the named columns with sep, and returns a new csv. The source columns stay in place; the new column is appended at the end. Every name in names must exist as a header, and names must not repeat a field. The header cells are never joined. The original csv is not modified and the result shares no data with it. If the csv is empty, names is empty, a name does not exist, names repeats a field, or as already exists as a header, it returns a copy of the csv and false.

func (SimpleCsv) DeleteRow

func (s SimpleCsv) DeleteRow(rowNumber int) (SimpleCsv, bool)

DeleteRow deletes the row and returns a new csv The original csv is not modified. Deleting row 0 is allowed only when the promoted row (if any) has unique header names.

func (SimpleCsv) DiffByKey

func (s SimpleCsv) DiffByKey(other SimpleCsv, key string) (added, removed, changed SimpleCsv, ok bool)

DiffByKey compares the data rows of s (left) and other (right), keyed by the values of the column with the header name key. It returns three new csvs:

  • added: the data rows of other whose key is not present in s, with other's header row
  • removed: the data rows of s whose key is not present in other, with s's header row
  • changed: the data rows of s whose key is present in other but whose full row differs, with s's header row and s's values (callers that want the right-side values can join on the key)

The rows keep their order of appearance in the source csv. The key values must be unique within each csv: a key repeated in s or in other makes the diff ambiguous, and it returns nil, nil, nil and false. It also returns nil, nil, nil and false if either csv is empty or the key is not a header name in either csv. A csv with only the header row is a valid input (all the other side's rows are then added or removed). The original csvs are not modified and the results share no data with them.

func (SimpleCsv) Distinct

func (s SimpleCsv) Distinct(field string) ([]string, bool)

Distinct returns the distinct values of the data cells of the column with the header name field, in order of first appearance. The header cell in row 0 is never included, and the returned slice is a copy: changes to it don't affect the csv. If the csv is empty or the field name does not exist, it returns an empty slice and false. A csv with only the header row returns an empty slice and true.

func (SimpleCsv) EachDataRow

func (s SimpleCsv) EachDataRow(fn func(rowIndex int, row []string, asMap map[string]string) bool)

EachDataRow calls fn for each data row (rowIndex >= 1), in csv order, with a copy of the row and a map view of it (header name to cell value). The row is a copy, so mutating it inside fn does not affect the csv; the map is rebuilt per row and is independent too. fn may return false to stop the iteration early. The header row is never passed to fn: an empty csv or a csv with only the header row calls fn zero times.

func (SimpleCsv) FillColumnByField

func (s SimpleCsv) FillColumnByField(columnName string, value string) (SimpleCsv, bool)

FillColumnByField returns a new csv where every data cell of the column with the header name columnName is set to value. The header cell in row 0 is not modified and the original csv is not modified. If the csv is empty or the field name does not exist, it returns a copy of the csv and false.

func (SimpleCsv) FilterByField

func (s SimpleCsv) FilterByField(name string, pred func(value string) bool) (SimpleCsv, bool)

FilterByField returns a new csv with the header row and the data rows where pred(value) returns true for the value of the named column. The predicate is called once per data row, in csv order, and never with the header cell. The original csv is not modified and the result shares no data with it. If the csv is empty or the field name does not exist, it returns a copy of the csv and false.

func (SimpleCsv) FilterRows

func (s SimpleCsv) FilterRows(predicate func(row []string) bool, header bool) SimpleCsv

FilterRows returns a new csv with copies of the rows where predicate returns true. If header is true, the first row is kept as the header and is not filtered. The original csv is not modified. The predicate receives a copy of each row and cannot mutate the source.

func (SimpleCsv) FindInColumn

func (s SimpleCsv) FindInColumn(columnPosition int, word string) ([]int, bool)

FindInColumn returns a slice with the rownumbers where the "word" is in the columnPosition If the column is not valid it returns an empty slice and a second false value The search includes row 0 (the header row), unlike FindInField. Case does not matter: Foo = foo = FOO.

func (SimpleCsv) FindInColumnCaseSensitive

func (s SimpleCsv) FindInColumnCaseSensitive(columnPosition int, word string) ([]int, bool)

FindInColumnCaseSensitive is like FindInColumn but case matters: Foo != foo. The search includes row 0 (the header row), unlike FindInFieldCaseSensitive.

func (SimpleCsv) FindInField

func (s SimpleCsv) FindInField(columnName string, word string) ([]int, bool)

FindInField returns a slice with the rownumbers where the "word" is in the column name If the column is not valid it returns an empty slice and a second false value It never includes the header row in the search result. Case does not matter: Foo = foo = FOO.

func (SimpleCsv) FindInFieldCaseSensitive

func (s SimpleCsv) FindInFieldCaseSensitive(columnName string, word string) ([]int, bool)

FindInFieldCaseSensitive is like FindInField but case matters: Foo != foo.

func (SimpleCsv) FullJoin

func (s SimpleCsv) FullJoin(other SimpleCsv, leftField, rightField string) (SimpleCsv, bool)

FullJoin returns a new csv with the full (outer) join of s and other by the column leftField of s and the column rightField of other, which may have different names. Every row of s and every row of other is in the result: rows with a matching join value are joined to all their matches, and rows without a match are included with empty strings in the columns of the other side.

Join values are compared exactly: case matters ("Ana" != "ana"). Rows shorter than the join column position are treated as having an empty string in that cell, and cells beyond the header width are not in the result.

The result headers are the headers of s followed by the headers of other excluding the join column rightField (the left join column is kept). Non-join columns of other must not share names with columns of s; a collision is rejected so header names stay unique.

The result rows are in the order of s, with the rows of other that had no match appended at the end.

If leftField or rightField does not exist in its csv, or if the result would have duplicate headers, it returns a copy of s and false. The source csvs are not modified and the result shares no rows with them. When leftField and rightField have the same name, FullJoin is equivalent to FullJoinByField.

func (SimpleCsv) FullJoinByField

func (s SimpleCsv) FullJoinByField(other SimpleCsv, fieldName string) (SimpleCsv, bool)

FullJoinByField is like FullJoin, joining by the column fieldName, which must exist in both csvs.

func (SimpleCsv) FullJoinOn

func (s SimpleCsv) FullJoinOn(other SimpleCsv, leftFields, rightFields []string) (SimpleCsv, bool)

FullJoinOn is like JoinOn, but every row of s and every row of other is in the result: rows with a matching join key are joined to all their matches, and rows without a match are included with empty strings in the columns of the other side.

func (SimpleCsv) GetCell

func (s SimpleCsv) GetCell(column int, row int) (string, bool)

GetCell Returns the value of a cell by position. If the cell does not exist, it returns the second value as false.

func (SimpleCsv) GetCellByField

func (s SimpleCsv) GetCellByField(columnName string, row int) (string, bool)

GetCellByField returns the value of a cell by column name. If the cell does not exist, it returns the second value as false.

func (SimpleCsv) GetColumn

func (s SimpleCsv) GetColumn(column int) ([]string, bool)

GetColumn returns a copy of the data cells of the column in position column, skipping the header cell in row 0. The caller can mutate the returned slice without affecting the csv. If the csv is empty or the column position is not valid, it returns an empty slice and false.

func (SimpleCsv) GetColumnByField

func (s SimpleCsv) GetColumnByField(columnName string) ([]string, bool)

GetColumnByField returns a copy of the data cells of the column with the header name columnName, skipping the header cell in row 0. The caller can mutate the returned slice without affecting the csv. If the csv is empty or the field name does not exist, it returns an empty slice and false.

func (SimpleCsv) GetDataRows

func (s SimpleCsv) GetDataRows() [][]string

GetDataRows returns copies of all the data rows (rows 1 to len(s)-1), excluding the header row. The caller can mutate the returned rows without affecting the csv. If the csv is empty or has only the header row, it returns an empty slice.

func (SimpleCsv) GetHeader

func (s SimpleCsv) GetHeader(columnPosition int) (string, bool)

GetHeader returns the header name in a position Returns false as the second value if it does not exist

func (SimpleCsv) GetHeaderPosition

func (s SimpleCsv) GetHeaderPosition(columnName string) int

GetHeaderPosition returns the header position, returns -1 if it does not exist

func (SimpleCsv) GetHeaders

func (s SimpleCsv) GetHeaders() []string

GetHeaders returns a copy of the headers as a slice If the csv is empty it returns an empty slice

func (SimpleCsv) GetNumberDataRows

func (s SimpleCsv) GetNumberDataRows() int

GetNumberDataRows returns the number of data rows, excluding the header row. For an empty csv or a csv with only the header row it returns 0. Use this instead of GetNumberRows when counting data: the header row is not data.

func (SimpleCsv) GetNumberRows

func (s SimpleCsv) GetNumberRows() int

GetNumberRows returns the number of rows, including the header row

func (SimpleCsv) GetRow

func (s SimpleCsv) GetRow(rowNumber int) ([]string, bool)

GetRow returns a copy of the row rowNumber If rowNumber does not exist, it returns an empty slice and false

func (SimpleCsv) GetRowAsMap

func (s SimpleCsv) GetRowAsMap(rowNumber int) (map[string]string, bool)

GetRowAsMap returns the row as a map If the row does not exist, returns nil and false If the row is shorter than the headers, missing cells are empty strings

func (SimpleCsv) GroupBy

func (s SimpleCsv) GroupBy(keys []string, aggs []Agg) (SimpleCsv, bool)

GroupBy collapses the data rows of s into one output row per group, where a group is all the data rows whose key fields have the same values. The output header row is the key field names followed by the aggregation As names, and each output data row is the key values followed by one cell per aggregation, in the order of aggs. Groups appear in order of first occurrence (the csv order of their first row) and aggregations process the rows of a group in csv order.

AggCount counts the data rows of the group. AggSum adds the Field cells parsed with strconv.ParseFloat: cells that don't parse as numbers count as 0. AggMin and AggMax use only finite numeric cells (NaN and infinities are skipped); a group without any finite numeric cell produces an empty cell. AggFirst and AggLast take the Field value of the first and last data row of the group. AggJoin joins the non-empty Field values with ",". Numeric results are formatted with strconv.FormatFloat(f, 'f', -1, 64), so integers have no decimal noise.

The original csv is not modified and the result shares no data with it. ok is false if the csv is empty, no keys or no aggregations are given, a key or aggregation field does not exist, an As name is empty or duplicates another key or As name, or an aggregation operation is not one of the AggOp constants. On failure a copy of the csv is returned.

func (SimpleCsv) GroupByE

func (s SimpleCsv) GroupByE(keys []string, aggs []Agg) (SimpleCsv, error)

GroupByE is like GroupBy, but it returns an error describing the reason of the failure instead of false. On failure it returns an independent copy of s.

func (SimpleCsv) Head

func (s SimpleCsv) Head(n int) SimpleCsv

Head returns a new csv with the header row and the first n data rows, or all the data rows if there are fewer than n. If n is negative or zero, the result has no data rows (the header row only, if present). The original csv is not modified and the result shares no data with it.

func (SimpleCsv) Join

func (s SimpleCsv) Join(other SimpleCsv, leftField, rightField string) (SimpleCsv, bool)

Join returns a new csv with the inner join of s and other by the column leftField of s and the column rightField of other, which may have different names. Only the rows whose join value exists in both csvs are in the result. If the same join value shows up more than once, all combinations of rows are in the result.

Join values are compared exactly: case matters ("Ana" != "ana"). Rows shorter than the join column position are treated as having an empty string in that cell, and cells beyond the header width are not in the result.

The result headers are the headers of s followed by the headers of other excluding the join column rightField (the left join column is kept). Non-join columns of other must not share names with columns of s; a collision is rejected so header names stay unique.

If leftField or rightField does not exist in its csv, or if the result would have duplicate headers, it returns a copy of s and false. The source csvs are not modified and the result shares no rows with them. When leftField and rightField have the same name, Join is equivalent to JoinByField.

func (SimpleCsv) JoinByField

func (s SimpleCsv) JoinByField(other SimpleCsv, fieldName string) (SimpleCsv, bool)

JoinByField returns a new csv with the inner join of s and other by the column fieldName, which must exist in both csvs. Only the rows whose join value exists in both csvs are in the result. If the same join value shows up more than once, all combinations of rows are in the result.

Join values are compared exactly: case matters ("Ana" != "ana"). Rows shorter than the join column position are treated as having an empty string in that cell, and cells beyond the header width are not in the result.

The result headers are the headers of s followed by the headers of other excluding the join column. Non-join columns of other must not share names with columns of s; a collision is rejected so header names stay unique.

If fieldName does not exist in one of the csvs, or if the result would have duplicate headers, it returns a copy of s and false. The source csvs are not modified and the result shares no rows with them.

func (SimpleCsv) JoinE

func (s SimpleCsv) JoinE(other SimpleCsv, leftField, rightField string) (SimpleCsv, error)

JoinE is like Join, but it returns an error describing the reason of the failure instead of false. On failure it returns an independent copy of s.

func (SimpleCsv) JoinOn

func (s SimpleCsv) JoinOn(other SimpleCsv, leftFields, rightFields []string) (SimpleCsv, bool)

JoinOn returns a new csv with the inner join of s and other by several columns at once. The i-th name of leftFields must be a column of s and the i-th name of rightFields a column of other: a row of s is joined to a row of other when the values of all its left key columns are exactly equal to the values of the corresponding right key columns. Only the rows whose join key exists in both csvs are in the result. If the same join key shows up more than once, all combinations of rows are in the result.

Join values are compared exactly: case matters ("Ana" != "ana"). Rows shorter than a join column position are treated as having an empty string in that cell, and cells beyond the header width are not in the result. The join key is built with a length-prefixed encoding, so values containing separators cannot collide.

The result headers are the headers of s followed by the headers of other excluding all the right key columns (the left key columns are kept). Non-key columns of other must not share names with columns of s; a collision is rejected so header names stay unique.

If s or other is empty, leftFields and rightFields have different lengths or are empty, one of the fields does not exist in its csv, or the result would have duplicate headers, it returns a copy of s and false. The source csvs are not modified and the result shares no rows with them. When both field lists have a single name, JoinOn is equivalent to Join.

func (SimpleCsv) LeftJoin

func (s SimpleCsv) LeftJoin(other SimpleCsv, leftField, rightField string) (SimpleCsv, bool)

LeftJoin is like Join, but the rows of s without a match in other are also in the result, with empty strings in the columns of other.

func (SimpleCsv) LeftJoinByField

func (s SimpleCsv) LeftJoinByField(other SimpleCsv, fieldName string) (SimpleCsv, bool)

LeftJoinByField is like JoinByField, but the rows of s without a match in other are also in the result, with empty strings in the columns of other.

func (SimpleCsv) LeftJoinE

func (s SimpleCsv) LeftJoinE(other SimpleCsv, leftField, rightField string) (SimpleCsv, error)

LeftJoinE is like LeftJoin, but it returns an error describing the reason of the failure instead of false. On failure it returns an independent copy of s.

func (SimpleCsv) LeftJoinOn

func (s SimpleCsv) LeftJoinOn(other SimpleCsv, leftFields, rightFields []string) (SimpleCsv, bool)

LeftJoinOn is like JoinOn, but the rows of s without a match in other are also in the result, with empty strings in the columns of other.

func (SimpleCsv) MapColumnByField

func (s SimpleCsv) MapColumnByField(columnName string, fn func(value string, row int) string) (SimpleCsv, bool)

MapColumnByField returns a new csv where every data cell of the column with the header name columnName is replaced by fn(value, row), called once per data row in csv order. row is the csv row index (1 for the first data row). The header cell in row 0 is not mapped and the original csv is not modified. If the csv is empty or the field name does not exist, it returns a copy of the csv and false.

func (SimpleCsv) MatchInColumn

func (s SimpleCsv) MatchInColumn(columnPosition int, regularexpression string) ([]int, bool)

MatchInColumn returns a slice with the rownumbers where the regular expression applies in the columnPosition. Matching is case-insensitive by default: Foo = foo = FOO. Use MatchInColumnCaseSensitive when case matters. If the column or regular expression are not valid it returns an empty slice and a second false value. The search includes row 0 (the header row), unlike MatchInField.

func (SimpleCsv) MatchInColumnCaseSensitive

func (s SimpleCsv) MatchInColumnCaseSensitive(columnPosition int, regularexpression string) ([]int, bool)

MatchInColumnCaseSensitive is like MatchInColumn but case matters: Foo != foo. The search includes row 0 (the header row), unlike MatchInFieldCaseSensitive. If the column or regular expression are not valid it returns an empty slice and a second false value.

func (SimpleCsv) MatchInField

func (s SimpleCsv) MatchInField(columnName string, regularexpression string) ([]int, bool)

MatchInField returns a slice with the rownumbers where the regular expression applies in the column name. Matching is case-insensitive by default: Foo = foo = FOO. Use MatchInFieldCaseSensitive when case matters. It never includes the header row in the search result. If the field name or regular expression are not valid it returns an empty slice and a second false value.

func (SimpleCsv) MatchInFieldCaseSensitive

func (s SimpleCsv) MatchInFieldCaseSensitive(columnName string, regularexpression string) ([]int, bool)

MatchInFieldCaseSensitive is like MatchInField but case matters: Foo != foo. It never includes the header row in the search result. If the field name or regular expression are not valid it returns an empty slice and a second false value.

func (SimpleCsv) MergeColumns

func (s SimpleCsv) MergeColumns(other SimpleCsv) (SimpleCsv, bool)

MergeColumns returns a new csv with the columns of other placed side by side with the columns of s: the result header row is the header row of s followed by the header row of other, and each result data row is the data row of s followed by the data row of other in the same position. Both csvs must have the same number of rows (header included) and no header name of other may collide with a header name of s, so header names stay unique. Padding is not allowed: row counts must match exactly. The original csvs are not modified and the result shares no data with them. If s or other is empty, their row counts differ, or the result would have duplicate headers, it returns a copy of s and false.

func (SimpleCsv) MoveColumn

func (s SimpleCsv) MoveColumn(columnName string, newIndex int) (SimpleCsv, bool)

MoveColumn moves the column with the header name columnName to position newIndex and returns a new csv. The columns between the old and the new position shift by one, and every row keeps its cells: the header and data cells of the moved column move together. newIndex must be a valid column position (0 to len(headers)-1); moving a column to its own position is a no-op and is allowed. The original csv is not modified and the result shares no data with it. If the csv is empty, the column name does not exist or newIndex is out of range, it returns a copy of the csv and false.

func (SimpleCsv) OnlyThisFields

func (s SimpleCsv) OnlyThisFields(fields []string) (SimpleCsv, bool)

OnlyThisFields returns a simplecsv with the fields. At least one field name is required (empty or nil fields is rejected).

func (SimpleCsv) OnlyThisRows

func (s SimpleCsv) OnlyThisRows(rowsIndex []int, header bool) (SimpleCsv, bool)

OnlyThisRows removes all rows that are not in the index and sorts the csv by the index order All rows must exist or it fails If header is true, the header row is always the first row of the result, so including 0 in rowsIndex duplicates the header row in the result

func (SimpleCsv) RemoveColumn

func (s SimpleCsv) RemoveColumn(columnPosition int) (SimpleCsv, bool)

RemoveColumn removes the column in position X and returns a new csv. The original csv is not modified

func (SimpleCsv) RemoveColumnByName

func (s SimpleCsv) RemoveColumnByName(columnName string) (SimpleCsv, bool)

RemoveColumnByName removes column by name

func (SimpleCsv) RenameHeader

func (s SimpleCsv) RenameHeader(oldHeader string, newHeader string) (SimpleCsv, bool)

RenameHeader renames the header and returns a new csv Returns the second value as false if it did't found the oldHeader or if newHeader already exists as a different column: the library requires header names to be unique, so a rename that would produce a duplicate is rejected. Renaming a header to its own name is a no-op and is allowed. The original csv is not modified

func (SimpleCsv) RenameHeaders

func (s SimpleCsv) RenameHeaders(pairs map[string]string) (SimpleCsv, bool)

RenameHeaders renames several headers in one call and returns a new csv. Every key of pairs must be an existing header, and the result must not contain duplicate header names: a rename fails if a key does not exist, two keys map to the same new name, or a renamed column collides with a header that is not renamed. The renames are applied simultaneously: the new name of every column is looked up from the original header row, so a swap ("a" -> "b", "b" -> "a") is applied correctly and map iteration order does not matter. Renaming a header to its own name is a no-op for that column. An empty pairs map is a no-op that returns a copy of the csv and true. The original csv is not modified and the result shares no data with it. If the csv is empty, it returns a copy of the csv and false.

func (SimpleCsv) ReplaceInField

func (s SimpleCsv) ReplaceInField(columnName string, old string, new string) (SimpleCsv, bool)

ReplaceInField returns a new csv where every data cell of the column with the header name columnName whose value is exactly equal to old (case-sensitive, whole cell) is replaced by new. The header cell in row 0 is not modified and the original csv is not modified. If the csv is empty or the field name does not exist, it returns a copy of the csv and false.

func (SimpleCsv) RightJoin

func (s SimpleCsv) RightJoin(other SimpleCsv, leftField, rightField string) (SimpleCsv, bool)

RightJoin returns a new csv with the right join of s and other by the column leftField of s and the column rightField of other, which may have different names. Every row of other is in the result: rows with a matching join value in s are joined to all their matches, and rows of other without a match are included with empty strings in the columns of s. The rows of s without a match in other are not in the result.

Join values are compared exactly: case matters ("Ana" != "ana"). Rows shorter than the join column position are treated as having an empty string in that cell, and cells beyond the header width are not in the result.

The result headers are the headers of s followed by the headers of other excluding the join column rightField (the left join column is kept). Non-join columns of other must not share names with columns of s; a collision is rejected so header names stay unique.

The result rows are in the order of s, with the rows of other that had no match appended at the end.

If leftField or rightField does not exist in its csv, or if the result would have duplicate headers, it returns a copy of s and false. The source csvs are not modified and the result shares no rows with them. When leftField and rightField have the same name, RightJoin is equivalent to RightJoinByField.

func (SimpleCsv) RightJoinByField

func (s SimpleCsv) RightJoinByField(other SimpleCsv, fieldName string) (SimpleCsv, bool)

RightJoinByField is like RightJoin, joining by the column fieldName, which must exist in both csvs.

func (SimpleCsv) RightJoinOn

func (s SimpleCsv) RightJoinOn(other SimpleCsv, leftFields, rightFields []string) (SimpleCsv, bool)

RightJoinOn is like JoinOn, but every row of other is in the result: rows with a matching join key in s are joined to all their matches, and rows of other without a match are included with empty strings in the columns of s. The rows of s without a match in other are not in the result. The rows of other that had no match are appended at the end.

func (SimpleCsv) SanitizeFormulas

func (s SimpleCsv) SanitizeFormulas() SimpleCsv

SanitizeFormulas returns a copy of the csv where every cell whose first rune is one of the formula-injection trigger characters =, +, -, @, tab, carriage return, line feed, or the full-width variants =, +, -, @ is prefixed with a single quote ' (the Excel and LibreOffice Calc convention), so that spreadsheet applications treat the value as text instead of interpreting it as a formula.

This mitigates CSV / formula injection: a cell whose value is, for example, "=cmd|'/c calc'!A1" triggers command execution when the file is opened in a spreadsheet, because encoding/csv does not neutralize such values on write (it only quotes fields that contain the delimiter, the quote character or a newline). Sanitizing is recommended whenever the file may contain attacker-controlled data (exported logs, form input, scraped content, etc.) and may later be opened in a spreadsheet.

Escaping changes data: "=cmd|'/c calc'!A1" becomes "'=cmd|'/c calc'!A1" and a negative number like "-5" becomes "'-5" (the leading ' is hidden by the spreadsheet, which displays -5 as text). It is therefore opt-in: callers that do not want their bytes changed should not call it, and the unsanitized WriteTo / WriteCsvFile* functions write values verbatim by design. The written bytes are only changed when SanitizeFormulas is applied first, for example:

err = s.SanitizeFormulas().WriteCsvFileE("export.csv")
err = s.SanitizeFormulas().WriteTo(os.Stdout, ',')

SanitizeFormulas returns an independent copy: the original csv is not modified and shares no data with the returned csv, like the other methods that return a SimpleCsv.

func (SimpleCsv) SetCell

func (s SimpleCsv) SetCell(column int, row int, value string) (SimpleCsv, bool)

SetCell changes the value of a cell and returns a new csv The original csv is not modified. Setting a header cell (row 0) that would produce duplicate header names is rejected.

func (SimpleCsv) SetCellByField

func (s SimpleCsv) SetCellByField(columnName string, row int, value string) (SimpleCsv, bool)

SetCellByField changes the value of a cell with a specific column

func (SimpleCsv) SetColumn

func (s SimpleCsv) SetColumn(column int, values []string) (SimpleCsv, bool)

SetColumn replaces the data cells of the column in position column with the values slice and returns a new csv. The header cell in row 0 is not modified and the original csv is not modified. len(values) must equal the number of data rows (len(s)-1), because every data row must keep exactly one cell in the column. The values are copied, changes to the original slice don't affect the csv. If the csv is empty, the column position is not valid or the length of values does not match, it returns a copy of the csv and false.

func (SimpleCsv) SetColumnByField

func (s SimpleCsv) SetColumnByField(columnName string, values []string) (SimpleCsv, bool)

SetColumnByField is SetColumn for the column with the header name columnName. If the csv is empty or the field name does not exist, it returns a copy of the csv and false.

func (SimpleCsv) SetRow

func (s SimpleCsv) SetRow(rowNumber int, rowValue []string) (SimpleCsv, bool)

SetRow updates a row in the csv and returns a new csv The row is copied, changes to the original slice don't affect the csv The original csv is not modified. Setting row 0 to a value with duplicate header names is rejected.

func (SimpleCsv) SetRowFromMap

func (s SimpleCsv) SetRowFromMap(rowNumber int, rowValue map[string]string) (SimpleCsv, bool)

SetRowFromMap replaces a row by the maps value Ignores keys with unexisting headers Fills blank where the key does not exists

func (SimpleCsv) SliceRows

func (s SimpleCsv) SliceRows(start, end int) (SimpleCsv, bool)

SliceRows returns a new csv with the header row and the data rows whose csv row index is in the half-open range [start, end) (end is not included; data rows have csv index >= 1). If end is beyond the last row, all the available data rows are included. An empty range (end == start) is valid and returns the header row only. The original csv is not modified and the result shares no data with it. If the csv is empty, start is less than 1 or end is less than start, it returns a copy of the csv and false.

func (SimpleCsv) SortByColumn

func (s SimpleCsv) SortByColumn(columnPosition int, ascending bool) (SimpleCsv, bool)

SortByColumn sorts all the rows of the csv by the column in columnPosition and returns a new sorted csv. The original csv is not modified, and later changes to the sorted csv do not affect it. All rows are sorted, including the header row (row 0), unlike SortByField. Finite numbers are sorted numerically among themselves and appear before non-numeric values (including NaN and Inf), which are sorted as strings. The sort is stable. If the column position is not valid it returns a copy of the csv and false.

func (SimpleCsv) SortByField

func (s SimpleCsv) SortByField(columnName string, ascending bool) (SimpleCsv, bool)

SortByField sorts the csv by the column with the name columnName and returns a new sorted csv. The header row stays at the top and the original csv is not modified, and later changes to the sorted csv do not affect it. Finite numbers are sorted numerically among themselves and appear before non-numeric values (including NaN and Inf), which are sorted as strings. The sort is stable. If the column name does not exist it returns a copy of the csv and false.

func (SimpleCsv) SortByFields

func (s SimpleCsv) SortByFields(fields []string, ascending []bool) (SimpleCsv, bool)

SortByFields sorts the csv by several columns, one level per field in order, and returns a new sorted csv. The header row stays at the top and the original csv is not modified, and later changes to the sorted csv do not affect it. A later field is only compared when every earlier field compares equal, like ORDER BY in SQL. Each field is sorted according to the corresponding bool in ascending: true for ascending, false for descending. If ascending is empty, every field is sorted ascending; otherwise its length must equal len(fields). Each field uses the same ordering as SortByField: finite numbers are sorted numerically among themselves and appear before non-numeric values (including NaN and Inf), which are sorted as strings. The sort is stable: data rows that compare equal on every field keep their csv order. ok is false if the csv is empty, no fields are given, a field name does not exist, or len(ascending) is neither 0 nor len(fields). On failure a copy of the csv is returned.

func (SimpleCsv) SplitField

func (s SimpleCsv) SplitField(name, sep string, newNames []string) (SimpleCsv, bool)

SplitField replaces the column with the header name name by len(newNames) new columns in its position, and returns a new csv. Every data cell of the original column is split on sep with strings.Split, and the parts fill the new columns in order: if a cell has fewer parts than newNames, the remaining new columns get empty cells; if it has more parts, the extra parts are dropped. The original column is removed, so a new name may be the same as it but must not collide with any other header and the names in newNames must be unique. The header cell is replaced, never split. The original csv is not modified and the result shares no data with it. If the csv is empty, the field name does not exist, newNames is empty or contains duplicates, or a new name collides with another header, it returns a copy of the csv and false.

func (SimpleCsv) Tail

func (s SimpleCsv) Tail(n int) SimpleCsv

Tail returns a new csv with the header row and the last n data rows, or all the data rows if there are fewer than n. If n is negative or zero, the result has no data rows (the header row only, if present). The original csv is not modified and the result shares no data with it.

func (SimpleCsv) TrimSpace

func (s SimpleCsv) TrimSpace() (SimpleCsv, bool)

TrimSpace returns a new csv where the surrounding whitespace of every cell, including the header cells in row 0, has been removed with strings.TrimSpace. The original csv is not modified. Trimming can merge header names, so if the trimmed header row contains duplicate names the operation is rejected (the library requires unique headers): it returns a copy of the csv and false.

func (SimpleCsv) Unique

func (s SimpleCsv) Unique() SimpleCsv

Unique returns a new csv with the same header row and only the first occurrence of each full data row (all columns), in csv order. The original csv is not modified and the result shares no data with it. An empty csv or a csv with only the header row is returned as an independent copy.

func (SimpleCsv) UniqueByFields

func (s SimpleCsv) UniqueByFields(fields ...string) (SimpleCsv, bool)

UniqueByFields returns a new csv with the same header row and, for each key built from the values of the named columns, only the first data row with that key is kept, in csv order. The original csv is not modified and the result shares no data with it. If the csv is empty, no fields are given, or a field name does not exist as a header, it returns a copy of the csv and false.

func (SimpleCsv) UpdateRowCellsFromMap

func (s SimpleCsv) UpdateRowCellsFromMap(rowNumber int, rowValue map[string]string) (SimpleCsv, bool)

UpdateRowCellsFromMap updates the cells whose column name is a key in the map and maintains the value of all the others. Ignores keys with unexisting headers. A key with an empty string value sets the cell to an empty string.

func (SimpleCsv) ValueCounts

func (s SimpleCsv) ValueCounts(field string) (SimpleCsv, bool)

ValueCounts returns a new csv with the fixed headers value and count: one row per distinct value of the data cells of the column with the header name field, in order of first appearance, with the number of data rows that have that value as a decimal string. The header cell in row 0 is never counted. The original csv is not modified and the result shares no data with it. If the csv is empty or the field name does not exist, it returns a copy of the csv and false. A csv with only the header row yields a csv with just the value,count headers and true.

func (SimpleCsv) Where

func (s SimpleCsv) Where(name, value string) (SimpleCsv, bool)

Where returns a new csv with the header row and the data rows where the named column is exactly equal to value. The comparison is case-sensitive: "Active" does not match "active". The header cell is never matched. The original csv is not modified and the result shares no data with it. If the csv is empty or the field name does not exist, it returns a copy of the csv and false.

func (SimpleCsv) WhereFold

func (s SimpleCsv) WhereFold(name, value string) (SimpleCsv, bool)

WhereFold is like Where but the comparison is case-insensitive (strings.EqualFold): "Active", "active" and "ACTIVE" all match. The header cell is never matched. The original csv is not modified and the result shares no data with it. If the csv is empty or the field name does not exist, it returns a copy of the csv and false.

func (SimpleCsv) WriteCsvFile

func (s SimpleCsv) WriteCsvFile(filename string) bool

WriteCsvFile writes the SimpleCsv to a file. Returns false if there's an error creating or writing the file.

func (SimpleCsv) WriteCsvFileComma

func (s SimpleCsv) WriteCsvFileComma(filename string, comma rune) error

WriteCsvFileComma writes the SimpleCsv to a file using comma as the field separator and returns an error on failure.

The write is atomic: the csv is first written to a temporary file in the same directory as filename and then renamed over it, so a failed or interrupted write cannot truncate or corrupt the destination. The temporary file (and therefore the destination after the rename) is created with mode 0600 (owner-readable only), so written files are not world-readable by default; callers that need different permissions can os.Chmod the result. Because the destination is replaced with os.Rename rather than opened, a symlink planted at filename is replaced instead of followed: the write does not follow a symlink at the destination.

func (SimpleCsv) WriteCsvFileE

func (s SimpleCsv) WriteCsvFileE(filename string) error

WriteCsvFileE writes the SimpleCsv to a file and returns an error on failure.

func (SimpleCsv) WriteTo

func (s SimpleCsv) WriteTo(w io.Writer, comma rune) error

WriteTo writes the SimpleCsv to w using comma as the field separator and returns an error on failure.

Jump to

Keyboard shortcuts

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