Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions .github/workflows/test-and-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,27 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
luaVersion: [ "5.4", "5.3", "5.2", "5.1", "luajit", "luajit-openresty" ]
lua: [lua=5.1, lua=5.2, lua=5.3, lua=5.4, luajit=@v2.0, luajit=@v2.1]
steps:
- uses: actions/checkout@master

- name: Setup ‘lua’
uses: leafo/gh-actions-lua@v9
with:
luaVersion: ${{ matrix.luaVersion }}
- name: Setup ‘luarocks’
uses: leafo/gh-actions-luarocks@v4
- name: Install libreadline
run: sudo apt-get install -y libreadline-dev

- name: Install Lua (${{ matrix.lua }})
run: |
pip install git+https://github.com/luarocks/hererocks
hererocks -r^ --${{ matrix.lua }} lua_install
echo lua_install/bin >> $GITHUB_PATH

- name: install depedencies
- name: Install depedencies
run: |
luarocks install busted
luarocks install lua-cjson
luarocks install luacov
luarocks install luacov-coveralls


- name: run unit tests with coverage
- name: Run unit tests with coverage
run: busted --verbose --coverage

- name: Report test coverage
Expand Down
140 changes: 75 additions & 65 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,80 +48,80 @@ end
The options are the same for `parseLine` and `parse`, with the exception of `loadFromString` and `bufferSize`. `loadFromString` only works with `parse` and `bufferSize` can only be specified for `parseLine`.

The following are optional parameters passed in via the third argument as a table.
- `delimiter`
- `delimiter`

If your file doesn't use the comma character as the delimiter, you can specify your own. It is limited to one character and defaults to `,`
```lua
ftcsv.parse("a>b>c\r\n1,2,3", {loadFromString=true, delimiter=">"})
```
If your file doesn't use the comma character as the delimiter, you can specify your own. It is limited to one character and defaults to `,`
```lua
ftcsv.parse("a>b>c\r\n1,2,3", {loadFromString=true, delimiter=">"})
```

- `loadFromString`
- `loadFromString`

If you want to load a csv from a string instead of a file, set `loadFromString` to `true` (default: `false`)
```lua
ftcsv.parse("a,b,c\r\n1,2,3", {loadFromString=true})
```
If you want to load a csv from a string instead of a file, set `loadFromString` to `true` (default: `false`)
```lua
ftcsv.parse("a,b,c\r\n1,2,3", {loadFromString=true})
```

- `rename`
- `rename`

If you want to rename a field, you can set `rename` to change the field names. The below example will change the headers from `a,b,c` to `d,e,f`
If you want to rename a field, you can set `rename` to change the field names. The below example will change the headers from `a,b,c` to `d,e,f`

Note: You can rename two fields to the same value, ftcsv will keep the field that appears latest in the line.
Note: You can rename two fields to the same value, ftcsv will keep the field that appears latest in the line.

```lua
local options = {loadFromString=true, rename={["a"] = "d", ["b"] = "e", ["c"] = "f"}}
local actual = ftcsv.parse("a,b,c\r\napple,banana,carrot", options)
```
```lua
local options = {loadFromString=true, rename={["a"] = "d", ["b"] = "e", ["c"] = "f"}}
local actual = ftcsv.parse("a,b,c\r\napple,banana,carrot", options)
```

- `fieldsToKeep`
- `fieldsToKeep`

If you only want to keep certain fields from the CSV, send them in as a table-list and it should parse a little faster and use less memory.
If you only want to keep certain fields from the CSV, send them in as a table-list and it should parse a little faster and use less memory.

Note: If you want to keep a renamed field, put the new name of the field in `fieldsToKeep`:
Note: If you want to keep a renamed field, put the new name of the field in `fieldsToKeep`:

```lua
local options = {loadFromString=true, fieldsToKeep={"a","f"}, rename={["c"] = "f"}}
local actual = ftcsv.parse("a,b,c\r\napple,banana,carrot\r\n", options)
```
```lua
local options = {loadFromString=true, fieldsToKeep={"a","f"}, rename={["c"] = "f"}}
local actual = ftcsv.parse("a,b,c\r\napple,banana,carrot\r\n", options)
```

Also Note: If you apply a function to the headers via headerFunc, and want to select fields from fieldsToKeep, you need to have what the post-modified header would be in fieldsToKeep.
Also Note: If you apply a function to the headers via headerFunc, and want to select fields from fieldsToKeep, you need to have what the post-modified header would be in fieldsToKeep.

- `ignoreQuotes`
- `ignoreQuotes`

If `ignoreQuotes` is `true`, it will leave all quotes in the final parsed output. This is useful in situations where the fields aren't quoted, but contain quotes, or if the CSV didn't handle quotes correctly and you're trying to parse it.
If `ignoreQuotes` is `true`, it will leave all quotes in the final parsed output. This is useful in situations where the fields aren't quoted, but contain quotes, or if the CSV didn't handle quotes correctly and you're trying to parse it.

```lua
local options = {loadFromString=true, ignoreQuotes=true}
local actual = ftcsv.parse('a,b,c\n"apple,banana,carrot', options)
```
```lua
local options = {loadFromString=true, ignoreQuotes=true}
local actual = ftcsv.parse('a,b,c\n"apple,banana,carrot', options)
```

- `headerFunc`
- `headerFunc`

Applies a function to every field in the header. If you are using `rename`, the function is applied after the rename.
Applies a function to every field in the header. If you are using `rename`, the function is applied after the rename.

Ex: making all fields uppercase
```lua
local options = {loadFromString=true, headerFunc=string.upper}
local actual = ftcsv.parse("a,b,c\napple,banana,carrot", options)
```
Ex: making all fields uppercase
```lua
local options = {loadFromString=true, headerFunc=string.upper}
local actual = ftcsv.parse("a,b,c\napple,banana,carrot", options)
```

- `headers`
- `headers`

Set `headers` to `false` if the file you are reading doesn't have any headers. This will cause ftcsv to create indexed tables rather than a key-value tables for the output.
Set `headers` to `false` if the file you are reading doesn't have any headers. This will cause ftcsv to create indexed tables rather than a key-value tables for the output.

```lua
local options = {loadFromString=true, headers=false, delimiter=">"}
local actual = ftcsv.parse("apple>banana>carrot\ndiamond>emerald>pearl", options)
```
```lua
local options = {loadFromString=true, headers=false, delimiter=">"}
local actual = ftcsv.parse("apple>banana>carrot\ndiamond>emerald>pearl", options)
```

Note: Header-less files can still use the `rename` option and after a field has been renamed, it can specified as a field to keep. The `rename` syntax changes a little bit:
Note: Header-less files can still use the `rename` option and after a field has been renamed, it can specified as a field to keep. The `rename` syntax changes a little bit:

```lua
local options = {loadFromString=true, headers=false, rename={"a","b","c"}, fieldsToKeep={"a","b"}, delimiter=">"}
local actual = ftcsv.parse("apple>banana>carrot\ndiamond>emerald>pearl", options)
```
```lua
local options = {loadFromString=true, headers=false, rename={"a","b","c"}, fieldsToKeep={"a","b"}, delimiter=">"}
local actual = ftcsv.parse("apple>banana>carrot\ndiamond>emerald>pearl", options)
```

In the above example, the first field becomes 'a', the second field becomes 'b' and so on.
In the above example, the first field becomes 'a', the second field becomes 'b' and so on.

For all tested examples, take a look in /spec/feature_spec.lua

Expand All @@ -147,29 +147,39 @@ file:close()
```

### Options
- `delimiter`
- `delimiter`

by default the encoder uses a `,` as a delimiter. The delimiter can be changed by setting a value for `delimiter`
by default the encoder uses a `,` as a delimiter. The delimiter can be changed by setting a value for `delimiter`

```lua
local output = ftcsv.encode(everyUser, {delimiter="\t"})
```
```lua
local output = ftcsv.encode(everyUser, {delimiter="\t"})
```

- `fieldsToKeep`

if `fieldsToKeep` is set in the encode process, only the fields specified will be written out to a file. The `fieldsToKeep` will be written out in the order that is specified.

```lua
local output = ftcsv.encode(everyUser, {fieldsToKeep={"Name", "Phone", "City"}})
```

- `onlyRequiredQuotes`

- `fieldsToKeep`
if `onlyRequiredQuotes` is set to `true`, the output will only include quotes around fields that are quotes, have newlines, or contain the delimter.

if `fieldsToKeep` is set in the encode process, only the fields specified will be written out to a file. The `fieldsToKeep` will be written out in the order that is specified.
```lua
local output = ftcsv.encode(everyUser, {onlyRequiredQuotes=true})
```

```lua
local output = ftcsv.encode(everyUser, {fieldsToKeep={"Name", "Phone", "City"}})
```
- `encodeNilAs`

- `onlyRequiredQuotes`
by default a `nil` value in a table will be encoded as the string `"nil"`. The value a `nil` value in the a table can be set with `encodeNilAs`.

if `onlyRequiredQuotes` is set to `true`, the output will only include quotes around fields that are quotes, have newlines, or contain the delimter.
```lua
local output = ftcsv.encode(everyUser, {encodeNilAs=""}) -- for setting nil to the empty string
local output = ftcsv.encode(everyUser, {encodeNilAs=0}) -- for setting it to 0
```

```lua
local output = ftcsv.encode(everyUser, {onlyRequiredQuotes=true})
```


## Error Handling
Expand Down
4 changes: 2 additions & 2 deletions ftcsv-1.4.0-1.rockspec → ftcsv-1.5.0-1.rockspec
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package = "ftcsv"
version = "1.4.0-1"
version = "1.5.0-1"

source = {
url = "git://github.com/FourierTransformer/ftcsv.git",
tag = "1.4.0"
tag = "1.5.0"
}

description = {
Expand Down
47 changes: 33 additions & 14 deletions ftcsv.lua
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
local ftcsv = {
_VERSION = 'ftcsv 1.4.0',
_VERSION = 'ftcsv 1.5.0',
_DESCRIPTION = 'CSV library for Lua',
_URL = 'https://github.com/FourierTransformer/ftcsv',
_LICENSE = [[
The MIT License (MIT)

Copyright (c) 2016-2023 Shakil Thakur
Copyright (c) 2016-2025 Fourier Transformer

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down Expand Up @@ -658,19 +658,33 @@ end
-- The ENCODER code is below here
-- This could be broken out, but is kept here for portability

local function generateCustomToString(valueToConvertNilTo)
local newReturnValue = tostring(valueToConvertNilTo)
local generatedFunction = function(field)
if type(field) == "nil" then
return newReturnValue
else
return tostring(field)
end
end
return generatedFunction
end

local function delimitField(field)
field = tostring(field)
if field:find('"') then
return field:gsub('"', '""')
else
return field
local function generateDelimitField(customToString)
local delimitField = function(field)
field = customToString(field)
if field:find('"') then
return field:gsub('"', '""')
else
return field
end
end
return delimitField
end

local function generateDelimitAndQuoteField(delimiter)
local function generateDelimitAndQuoteField(delimiter, customToString)
local generatedFunction = function(field)
field = tostring(field)
field = customToString(field)
if field:find('"') then
return '"' .. field:gsub('"', '""') .. '"'
elseif field:find('[\n' .. delimiter .. ']') then
Expand Down Expand Up @@ -722,10 +736,15 @@ local function csvLineGenerator(inputTable, delimiter, headers, options)
arguments.t = inputTable
-- we want to use the same delimitField throughout,
-- so we're just going to pass it in

local toStringToUse = tostring
if options and options.encodeNilAs ~= nil then
toStringToUse = generateCustomToString(options.encodeNilAs)
end
if options and options.onlyRequiredQuotes == true then
arguments.delimitField = generateDelimitAndQuoteField(delimiter)
arguments.delimitField = generateDelimitAndQuoteField(delimiter, toStringToUse)
else
arguments.delimitField = delimitField
arguments.delimitField = generateDelimitField(toStringToUse)
end

return luaCompatibility.load(outputFunc), arguments, 0
Expand All @@ -752,9 +771,9 @@ end

local function escapeHeadersForOutput(headers, delimiter, options)
local escapedHeaders = {}
local delimitField = delimitField
local delimitField = generateDelimitField(tostring)
if options and options.onlyRequiredQuotes == true then
delimitField = generateDelimitAndQuoteField(delimiter)
delimitField = generateDelimitAndQuoteField(delimiter, tostring)
end
for i = 1, #headers do
escapedHeaders[i] = delimitField(headers[i])
Expand Down
Loading