Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/custom separator #22

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ Takes a string of CSV data and converts it to a 2 dimensional array of `[entries
- csv - the CSV string to parse
- options
- typed - infer types (default `false`)
- separator - the used separator, one character (default `,`)
- delimiter - the used field delimiter, one character (default `"`)
- reviver<sup>1</sup> - a custom function to modify the output (default `(value) => value`)

*<sup>1</sup> Values for `row` and `col` are 1-based.*
Expand Down Expand Up @@ -98,6 +100,8 @@ Takes a 2 dimensional array of `[entries][values]` and converts them to CSV
- array - the input array to stringify
- options
- eof - add a trailing newline at the end of file (default `true`)
- separator - the used separator, one character (default `,`)
- delimiter - the used field delimiter, one character (default `"`)
- replacer<sup>1</sup> - a custom function to modify the values (default `(value) => value`)

*<sup>1</sup> Values for `row` and `col` are 1-based.*
Expand Down
4 changes: 4 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
*
* options
* - typed - infer types [false]
* - separator - use custom separator [,]
* - delimiter - use custom delimiter ["]
*
* @static
* @param {string} csv the CSV string to parse
Expand All @@ -16,6 +18,8 @@ export function parse(csv: string, options?: any, reviver?: Function): any[];
*
* options
* - eof - add a trailing newline at the end of file [true]
* - separator - use custom separator [,]
* - delimiter - use custom delimiter ["]
*
* @static
* @param {Array} array the input array to stringify
Expand Down
47 changes: 36 additions & 11 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
*
* options
* - typed - infer types [false]
* - separator - use custom separator [,]
* - delimiter - use custom delimiter ["]
*
* @static
* @param {string} csv the CSV string to parse
Expand All @@ -20,7 +22,15 @@ export function parse (csv, options, reviver = v => v) {
ctx.col = 1
ctx.row = 1

const lexer = /"|,|\r\n|\n|\r|[^",\r\n]+/y
ctx.options.delimiter = ctx.options.delimiter === undefined ? '"' : options.delimiter;
if(ctx.options.delimiter.length > 1 || ctx.options.delimiter.length === 0)
throw Error(`CSVError: delimiter must be one character [${ctx.options.separator}]`)

ctx.options.separator = ctx.options.separator === undefined ? ',' : options.separator;
if(ctx.options.separator.length > 1 || ctx.options.separator.length === 0)
throw Error(`CSVError: separator must be one character [${ctx.options.separator}]`)

const lexer = new RegExp(`${escapeRegExp(ctx.options.delimiter)}|${escapeRegExp(ctx.options.separator)}|\r\n|\n|\r|[^${escapeRegExp(ctx.options.delimiter)}${escapeRegExp(ctx.options.separator)}\r\n]+`, 'y')
const isNewline = /^(\r\n|\n|\r)$/

let matches = []
Expand All @@ -33,10 +43,10 @@ export function parse (csv, options, reviver = v => v) {
switch (state) {
case 0: // start of entry
switch (true) {
case match === '"':
case match === ctx.options.delimiter:
state = 3
break
case match === ',':
case match === ctx.options.separator:
state = 0
valueEnd(ctx)
break
Expand All @@ -53,7 +63,7 @@ export function parse (csv, options, reviver = v => v) {
break
case 2: // un-delimited input
switch (true) {
case match === ',':
case match === ctx.options.separator:
state = 0
valueEnd(ctx)
break
Expand All @@ -69,7 +79,7 @@ export function parse (csv, options, reviver = v => v) {
break
case 3: // delimited input
switch (true) {
case match === '"':
case match === ctx.options.delimiter:
state = 4
break
default:
Expand All @@ -80,11 +90,11 @@ export function parse (csv, options, reviver = v => v) {
break
case 4: // escaped or closing delimiter
switch (true) {
case match === '"':
case match === ctx.options.delimiter:
state = 3
ctx.value += match
break
case match === ',':
case match === ctx.options.separator:
state = 0
valueEnd(ctx)
break
Expand Down Expand Up @@ -114,6 +124,8 @@ export function parse (csv, options, reviver = v => v) {
*
* options
* - eof - add a trailing newline at the end of file [true]
* - separator - use custom separator [,]
* - delimiter - use custom delimiter ["]
*
* @static
* @param {Array} array the input array to stringify
Expand All @@ -129,19 +141,27 @@ export function stringify (array, options = {}, replacer = v => v) {
ctx.col = 1
ctx.output = ''

const needsDelimiters = /"|,|\r\n|\n|\r/
ctx.options.delimiter = ctx.options.delimiter === undefined ? '"' : options.delimiter;
if(ctx.options.delimiter.length > 1 || ctx.options.delimiter.length === 0)
throw Error(`CSVError: delimiter must be one character [${ctx.options.separator}]`)

ctx.options.separator = ctx.options.separator === undefined ? ',' : options.separator;
if(ctx.options.separator.length > 1 || ctx.options.separator.length === 0)
throw Error(`CSVError: separator must be one character [${ctx.options.separator}]`)

const needsDelimiters = new RegExp(`${escapeRegExp(ctx.options.delimiter)}|${escapeRegExp(ctx.options.separator)}|\r\n|\n|\r`)

array.forEach((row, rIdx) => {
let entry = ''
ctx.col = 1
row.forEach((col, cIdx) => {
if (typeof col === 'string') {
col = col.replace(/"/g, '""')
col = needsDelimiters.test(col) ? `"${col}"` : col
col = col.replace(ctx.options.delimiter, `${ctx.options.delimiter}${ctx.options.delimiter}`)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only replace the first instance of the delimiter in a string

col = needsDelimiters.test(col) ? `${ctx.options.delimiter}${col}${ctx.options.delimiter}` : col
}
entry += replacer(col, ctx.row, ctx.col)
if (cIdx !== row.length - 1) {
entry += ','
entry += ctx.options.separator
}
ctx.col++
})
Expand Down Expand Up @@ -192,3 +212,8 @@ function inferType (value) {
return value
}
}

/** @private */
function escapeRegExp(str) {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
}
2 changes: 0 additions & 2 deletions index.min.js

This file was deleted.

22 changes: 22 additions & 0 deletions test/__test__/delimiter1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"description": [
"When options.delimiter is set the parser uses this character as delimiter"
],
"csv": [
"'123','456','789'",
"false,true,Test",
""
],
"json": [
[
"123",
"456",
"789"
],
[
"false",
"true",
"Test"
]
]
}
22 changes: 22 additions & 0 deletions test/__test__/delimiter2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"description": [
"When options.delimiter is set the parser uses this character as delimiter (RegExp meta char)"
],
"csv": [
"|123|,|456|,|789|",
"false,true,Test",
""
],
"json": [
[
"123",
"456",
"789"
],
[
"false",
"true",
"Test"
]
]
}
22 changes: 22 additions & 0 deletions test/__test__/delimiter3.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"description": [
"When options.delimiter is set stringify uses this character as delimiter"
],
"csv": [
"123,456,789",
"false,true,'Test,with separator'",
""
],
"json": [
[
"123",
"456",
"789"
],
[
"false",
"true",
"Test,with separator"
]
]
}
22 changes: 22 additions & 0 deletions test/__test__/separator1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"description": [
"When options.separator is set the parser uses this character as separator"
],
"csv": [
"123;456;789",
"false;true;\"Test;with separator\"",
""
],
"json": [
[
"123",
"456",
"789"
],
[
"false",
"true",
"Test;with separator"
]
]
}
22 changes: 22 additions & 0 deletions test/__test__/separator2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"description": [
"When options.separator is set the parser uses this character as separator (RegExp meta char)"
],
"csv": [
"123|456|789",
"false|true|Test",
""
],
"json": [
[
"123",
"456",
"789"
],
[
"false",
"true",
"Test"
]
]
}
88 changes: 87 additions & 1 deletion test/options.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ const replacer1 = require('./__test__/replacer1.json')
const replacer2 = require('./__test__/replacer2.json')
const eof1 = require('./__test__/eof1.json')
const eof2 = require('./__test__/eof2.json')
const separator1 = require('./__test__/separator1.json')
const separator2 = require('./__test__/separator2.json')
const delimiter1 = require('./__test__/delimiter1.json')
const delimiter2 = require('./__test__/delimiter2.json')
const delimiter3 = require('./__test__/delimiter3.json')

test('Reviver #1 - The reviver should append 1 to each value', (t) => {
const expect = reviver1.json
Expand Down Expand Up @@ -41,7 +46,7 @@ test('Typed #1 - When set to true the parser should infer the value types', (t)
test('Typed #2- When set to false the parser should not infer the value types', (t) => {
const expect = typed2.json
const actual = CSV.parse(typed2.csv.join('\n'), { typed: false })

t.deepEqual(actual, expect)

t.end()
Expand Down Expand Up @@ -82,3 +87,84 @@ test('EOF #2- When set to false the formatter should not include a newline at th

t.end()
})

test('Separator #1 - When set the parser should use this character as separator', (t) => {
const expect = separator1.json
const actual = CSV.parse(separator1.csv.join('\n'), { separator: ';' })

t.deepEqual(actual, expect)

t.end()
})

test('Separator #2 - The parser accepts regular expression meta characters as separator', (t) => {
const expect = separator2.json
const actual = CSV.parse(separator2.csv.join('\n'), { separator: '|' })

t.deepEqual(actual, expect)

t.end()
})

test('Separator #3 - The separator must be one character', (t) => {
t.throws(
() => CSV.parse(separator1.csv.join('\n'), { separator: '' }),
/separator must be one character/
)
t.throws(
() => CSV.parse(separator1.csv.join('\n'), { separator: '==' }),
/separator must be one character/
)

t.end()
})

test('Separator #4 - When set stringify should use this character as separator', (t) => {
const expect = separator1.csv.join('\n')
const actual = CSV.stringify(separator1.json, { separator: ';' })

t.deepEqual(actual, expect)

t.end()
})

test('Delimiter #1 - When set the parser should use this character as delimiter', (t) => {
const expect = delimiter1.json
const actual = CSV.parse(delimiter1.csv.join('\n'), { delimiter: '\'' })

t.deepEqual(actual, expect)

t.end()
})

test('Delimiter #2 - The parser accepts regular expression meta characters as delimiter', (t) => {
const expect = delimiter2.json
const actual = CSV.parse(delimiter2.csv.join('\n'), { delimiter: '|' })

t.deepEqual(actual, expect)

t.end()
})

test('Delimiter #3 - The delimiter must be one character', (t) => {
t.throws(
() => CSV.parse(delimiter1.csv.join('\n'), { delimiter: '' }),
/delimiter must be one character/
)
t.throws(
() => CSV.parse(delimiter1.csv.join('\n'), { delimiter: '==' }),
/delimiter must be one character/
)

t.end()
})


test('Delimiter #4 - When set stringify should use this character as delimiter', (t) => {
const expect = delimiter3.csv.join('\n')
const actual = CSV.stringify(delimiter3.json, { delimiter: '\'' })

t.deepEqual(actual, expect)

t.end()
})