Skip to content

Commit

Permalink
generate() add renaming. README - add more examples.
Browse files Browse the repository at this point in the history
  • Loading branch information
koresar committed Mar 18, 2021
1 parent 5a12541 commit 9fa0f08
Show file tree
Hide file tree
Showing 3 changed files with 392 additions and 29 deletions.
214 changes: 203 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,204 @@

Mini 1k module for CSV file manipulations

- Parse CSV text to deep JSON objects.
- Customise each column parsing with your code.
- Serialise deep JSON objects to CSV.
- Rename CSV headers and object keys on the fly.
- Simply generate CSV from arrays of strings.
- Parse CSV to simple arrays of strings.

## Usage

```shell
npm i lil-csv
```

### Parse CSV

Every row and column as text:
Import

```js
import { parse } from "lil-csv";
import { parse, generate } from "lil-csv";
// or
const { parse, generate } = require("lil-csv");
```

### Examples

const text = `Column 1,"Some,other",Boolean
text data,"data, with, commas",false`;
#### Objects

Parse to object

```js
const text = `name,address.street,address.country
John Noa,"7 Blue Bay, Berala",AU`;

const rows = parse(text);

console.log(rows);
// [
// [ 'Column 1', 'Some,other', 'Boolean' ],
// [ 'text data', 'data, with, commas', 'false' ]
// ]
assert.deepStrictEqual(rows, [
{
name: "John Noa",
address: {
street: "7 Blue Bay, Berala",
country: "AU",
},
},
]);
```

Generate CSV from objects

```js
const rows = [
{
name: "John Noa",
address: {
street: "7 Blue Bay, Berala",
country: "AU",
},
},
];

const text = generate(rows);

assert.deepStrictEqual(
text,
`name,address.street,address.country
John Noa,"7 Blue Bay, Berala",AU`
);
```

#### Arrays

Parse to arrays

```js
const text = `name,address.street,address.country
John Noa,"7 Blue Bay, Berala",AU`;

const rows = parse(text, { header: false });

assert.deepStrictEqual(rows, [
["name", "address.street", "address.country"],
["John Noa", "7 Blue Bay, Berala", "AU"],
]);
```

Generate CSV from arrays

```js
const rows = [
["name", "address.street", "address.country"],
["John Noa", "7 Blue Bay, Berala", "AU"],
];

const text = generate(rows, { header: false });

assert.deepStrictEqual(
text,
`name,address.street,address.country
John Noa,"7 Blue Bay, Berala",AU`
);
```

#### Customise parsed objects

Rename columns, custom parse data:

```js
const countryLookup = { PH: "Philippines", AU: "Australia" };

const text = `name,date of birth,address.street,address.country,address.postcode
John Noa,N/A,"7 Blue Bay, Berala",AU,XXXX
Lily Noa,1992-12-26,"7 Blue Bay, Berala",AU,2222`;

const rows = parse(text, {
header: {
name: "fullName",
"date of birth": {
jsonName: "dob",
parse: (v) => (isNaN(new Date(v).valueOf()) ? null : v),
},
"address.street": String,
"address.country": {
jsonName: "country",
parse: (v) => countryLookup[v.toUpperCase()] || null,
},
"address.postcode": (v) => (v && v.match && v.match(/^\d{4}$/) ? v : null),
},
});

assert.deepStrictEqual(rows, [
{
fullName: "John Noa",
dob: null,
address: {
street: "7 Blue Bay, Berala",
postcode: null,
},
country: "Australia",
},
{
fullName: "Lily Noa",
dob: "1992-12-26",
address: {
street: "7 Blue Bay, Berala",
postcode: "2222",
},
country: "Australia",
},
]);
```

#### Customise CSV generation

Rename columns, custom stringify data:

```js
const countryReverseLookup = { PHILIPPINES: "PH", AUSTRALIA: "AU" };

const rows = [
{
fullName: "John Noa",
dob: null,
address: {
street: "7 Blue Bay, Berala",
postcode: null,
},
country: "Australia",
},
{
fullName: "Lily Noa",
dob: "1992-12-26",
address: {
street: "7 Blue Bay, Berala",
postcode: "2222",
},
country: "Australia",
},
];

const text = generate(rows, {
header: {
fullName: "name",
dob: {
jsonName: "date of birth",
stringify: (v) => (!v || isNaN(new Date(v).valueOf()) ? "N/A" : v),
},
"address.street": String,
country: {
jsonName: "address.country",
stringify: (v) => countryReverseLookup[v.toUpperCase()] || "N/A",
},
"address.postcode": (v) => (v && v.match && v.match(/^\d{4}$/) ? v : "N/A"),
},
});

assert.deepStrictEqual(
text,
`name,date of birth,address.street,address.country,address.postcode
John Noa,N/A,"7 Blue Bay, Berala",AU,N/A
Lily Noa,1992-12-26,"7 Blue Bay, Berala",AU,2222`
);
```

Process rows to JSON objects:
Expand Down Expand Up @@ -107,3 +282,20 @@ console.log(text);
// my str,123.123,false,2020-12-12T00:00:00.000Z,1999-09-09,
// -1,not number,False,,bad DOB,
```

## API

### `parse(text, [options = { header: true, escapeChar: "\\" }])`

- `text` - String, the string to parse.
- `options` - Object, optional parsing options.
- `options.escapeChar` - String character, the escape character used within that CSV.
- `options.header` - Boolean, or Array of string, or Object. Default is `true`.
- Boolean
- `true` - create JSON objects from CSV rows. Assume first row of the text is a header, would be used as object keys.
- `false` - create string arrays from CSV rows.
- Array - create JSON objects from CSV rows. The array would be used as object keys.
- Object - create JSON objects from CSV rows. Object keys - CSV header name, Object values - either string or Object.
- value is String - rename CSV header. E.g. `"User First Name": "user.firstName"`
- `header[].parse` - use this function to deserialize a CSV cell to a value. E.g. convert "2020-12-12" string to a Date.
- `header[].jsonName` - rename CSV header. E.g. `jsonName: "user.firstName"`
45 changes: 27 additions & 18 deletions src/lil-csv.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@ const isBoolean = (v) => typeof v === "boolean";
const isDate = (v) => v instanceof Date && !isNaN(v.valueOf());
const isObject = (v) => v && typeof v === "object" && !isArray(v);
const isFunction = (v) => typeof v === "function";
const getDeep = (obj, path) => path.split(".").reduce((result, curr) => (result == null ? result : result[curr]), obj);
const setDeep = (obj, path, value) => {

function getDeep(obj, path) {
return path.split(".").reduce((result, curr) => (result == null ? result : result[curr]), obj);
}

function setDeep(obj, path, value) {
path.split(".").reduce((result, curr, index, paths) => {
const newVal = index + 1 === paths.length ? value : {};
return isObject(result[curr]) ? result[curr] : (result[curr] = newVal);
}, obj);
};
}

const mergeDeep = (target, ...sources) => {
function mergeDeep(target, ...sources) {
if (!sources.length) return target;
const source = sources.shift();

Expand All @@ -28,13 +32,14 @@ const mergeDeep = (target, ...sources) => {
}

return mergeDeep(target, ...sources);
};
}

const deepKeys = (obj, prefix) =>
Object.entries(obj).reduce((keys, [k, v]) => {
function keysDeep(obj, prefix) {
return Object.entries(obj).reduce((keys, [k, v]) => {
const newKey = prefix ? prefix + "." + k : k;
return keys.concat(isObject(v) ? deepKeys(v, newKey) : newKey);
return keys.concat(isObject(v) ? keysDeep(v, newKey) : newKey);
}, []);
}

export function parse(str, { header = true, escapeChar = "\\" } = {}) {
const entries = [];
Expand Down Expand Up @@ -119,24 +124,24 @@ export function parse(str, { header = true, escapeChar = "\\" } = {}) {
}

export function generate(rows, { header = true, lineTerminator = "\n", escapeChar = "\\" } = {}) {
const serialise = (v) => {
function serialiseString(v) {
v = v.replace(/"/g, escapeChar + '"'); // Escape quote character
return v.includes(",") ? '"' + v + '"' : v; // Add quotes if value has commas
};
}

const valueToString = (v) => {
function valueToString(v) {
if (v == null || v === "" || !((isNumber(v) && !isNaN(v)) || isString(v) || isDate(v) || isBoolean(v)))
return ""; // ignore bad data

v = isDate(v) ? v.toISOString() : String(v); // convert any kind of value to string
return serialise(v);
};
return v;
}

let detectedHeaders = null;
if (header) {
if (isBoolean(header)) {
if (isObject(rows[0])) {
detectedHeaders = deepKeys(mergeDeep({}, ...rows.filter(isObject)));
detectedHeaders = keysDeep(mergeDeep({}, ...rows.filter(isObject)));
if (detectedHeaders.length === 0) throw new Error("Bad header and rows");
} else {
if (!isArray(rows[0])) throw new Error("Can't auto detect header from rows");
Expand All @@ -157,9 +162,9 @@ export function generate(rows, { header = true, lineTerminator = "\n", escapeCha
const textHeader = detectedHeaders
? detectedHeaders
.map((h) => {
const dataHeader = isObject(header) ? header[h] : h;
const dataHeader = header[h] || h;
const newHeader = dataHeader.jsonName || (isString(dataHeader) ? dataHeader : h);
return serialise(newHeader);
return serialiseString(newHeader);
})
.join() + lineTerminator
: "";
Expand All @@ -170,13 +175,17 @@ export function generate(rows, { header = true, lineTerminator = "\n", escapeCha
if (isArray(row)) {
if (detectedHeaders && row.length !== detectedHeaders.length)
throw new Error(`Each row array must have exactly ${detectedHeaders.length} items`);
return row.map(valueToString).join();
return row.map((v) => serialiseString(valueToString(v))).join();
}
if (isObject(row)) {
if (!detectedHeaders) throw new Error("Unexpected row object");

return detectedHeaders
.map((h) => {
return valueToString(getDeep(row, h));
const dataHeader = header[h] || h;
let stringify = dataHeader.stringify || dataHeader;
if (!isFunction(stringify)) stringify = valueToString;
return serialiseString(valueToString(stringify(getDeep(row, h))));
})
.join();
}
Expand Down
Loading

0 comments on commit 9fa0f08

Please sign in to comment.