CSV

This module provides functions for working with CSV (Comma Separated Values) data in TypeScript. It includes functions for parsing CSV data into an array of rows (each of which is an array of cell values), and for converting an array of rows into CSV data as a string.

Introduction to CSV

CSV is a simple file format used to store tabular data, such as a spreadsheet or database. It stores data in plain text, with each line representing a row and each field (column) within that row separated by a comma or other delimiter. The first line usually contains the names of the fields, also known as the header row.

For example, a CSV file might look like this:

name,email,age
John Smith,john.smith@example.com,30
Jane Doe,jane.doe@example.com,25

This CSV file has three fields (name, email, and age) and two rows of data. The first row contains the field names, and the second and third rows contain the data for each person.

To escape special characters within cell values in a CSV file, you can use double quotes to surround the cell value. For example, if a cell value contains a comma, you can escape it by surrounding the value with double quotes:

"John,Smith","john.smith@example.com","30"

If a cell value contains a double quote character, you can escape it by using two double quotes in a row:

"John Smith","john.smith@example.com","He said ""Hello"" to me."

CSV files are commonly used to transfer data between programs, such as from a database to a spreadsheet application or vice versa. They are also used to import and export data for websites and other applications.

The MIME type for CSV files is text/csv. This is the correct MIME type to use when serving CSV files over the internet, or when specifying the type of CSV file in an HTML <input type="file"> element.

Importing the core module

Once the fi.hg.core module is installed, you can import it in your TypeScript code using the following syntax:

import { Csv, parseCsvRow, stringifyCsvRow } from './fi/hg/core/csv';

Parsing CSV Data

To parse a CSV string into an array of rows, you can use the parseCsv function. This function takes a string as input and returns an array of rows, where each row is an array of cells.

Here is an example of how to use the parseCsv function:

import { Csv, parseCsv } from './fi/hg/core/csv';

const csvString = 'id,name,age\n1,Alice,30\n2,Bob,40\n';
const csv: Csv = parseCsv(csvString);

console.log(csv); // [['id', 'name', 'age'], ['1', 'Alice', '30'], ['2', 'Bob', '40']]

You can also use the parseCsvRow function to parse a single row of CSV data. This function takes a string as input and returns an array of cells.

Here is an example of how to use the parseCsvRow function:

import { parseCsvRow } from './fi/hg/core/csv';

const csvRowString = '1,Alice,30';
const csvRow: string[] = parseCsvRow(csvRowString);

console.log(csvRow); // ['1', 'Alice', '30']

Generating CSV data

The HG CSV module provides several functions for generating CSV data as a string from an array of rows or from a list of objects.

Converting cell values to strings

Before we can generate CSV data, we need to be able to convert individual cell values into strings suitable for inclusion in a CSV file. The stringifyCsvCellValue function can be used for this purpose. It takes a single cell value as a parameter, and returns a string representation of the value.

If the input value is an array, the elements of the array are joined with commas. If the input value is undefined, an empty string is returned. Otherwise, the value is simply converted to a string using the toString method.

Here's an example of using the stringifyCsvCellValue function:

import { stringifyCsvCellValue } from './fi/hg/core/csv';

console.log(stringifyCsvCellValue([1, 2, 3]));  // Outputs "1,2,3"
console.log(stringifyCsvCellValue(undefined));  // Outputs ""
console.log(stringifyCsvCellValue(123));        // Outputs "123"
console.log(stringifyCsvCellValue("abc"));      // Outputs "abc"

Generating rows from objects

The getCsvRowFromJsonObject function can be used to generate a single row of CSV data from an object.

It takes an object, an array of property names to include in the row, and an optional map of property transformers as parameters.

The property transformers are functions that take an object and a property name, and return the value to be included in the row for that property.

If no property transformer is provided for a given property, the value of the property is retrieved using the get function from the Lodash library and passed to the stringifyCsvCellValue function.

Here's an example of using the getCsvRowFromJsonObject function:

import { getCsvRowFromJsonObject } from './fi/hg/core/csv';

const obj = {
    id: 123,
    name: "John Smith",
    email: "john.smith@example.com"
};

const row = getCsvRowFromJsonObject(obj, ['id', 'name', 'email']);
console.log(row);  // Outputs ["123", "John Smith", "john.smith@example.com"]

You can also provide a property transformer map to customize the values included in the row for certain properties. For example:

import { getCsvRowFromJsonObject } from './fi/hg/core/csv';

const obj = {
    id: 123,
    name: "John Smith",
    email: "john.smith@example.com"
};

const propertyTransformer: CsvPropertyTransformerMap<typeof obj> = {
    name: (item: typeof obj) => `"${item.name}"`,
    email: (item: typeof obj) => item.email.toLowerCase()
};

const row = getCsvRowFromJsonObject(obj, ['id', 'name', 'email'], propertyTransformer);
console.log(row);  // Outputs ["123", "\"John Smith\"", "john.smith@example.com"]

In this example, the name property is wrapped in double quotes, and the email property is converted to lower case before being included in the row.

Generating an entire CSV file from a list of objects

The getCsvFromJsonObjectList function can be used to generate an entire CSV file from a list of objects.

It takes a list of objects, an optional array of property names to include in the rows, an optional flag to include a header row with the property names, and an optional map of property
transformers as parameters.

If no property names are provided, the keys of the first object in the list are used.

If the includeHeader flag is true, the first row of the CSV file will be a header row with the property names.

The property transformers work the same way as in the
getCsvRowFromJsonObject function, allowing you to customize the values
included in the rows for certain properties.

Here's an example of using the getCsvFromJsonObjectList function:

import { getCsvFromJsonObjectList } from './fi/hg/core/csv';

const list = [
    {
        id: 123,
        name: "John Smith",
        email: "john.smith@example.com"
    },
    {
        id: 456,
        name: "Jane Doe",
        email: "jane.doe@example.com"
    }
];

const csv = getCsvFromJsonObjectList(list, ['id', 'name', 'email'], true);
console.log(csv);
// Outputs:
// [
//   ["id", "name", "email"],
//   ["123", "John Smith", "john.smith@example.com"],
//   ["456", "Jane Doe", "jane.doe@example.com"]
// ]

You can also omit the header row or use different property names by setting the includeHeader flag to false or providing a different array of property names, respectively.

You can also use a property transformer map to customize the values included in the rows for certain properties. For example:

import { getCsvFromJsonObjectList } from './fi/hg/core/csv';

const list = [
    {
        id: 123,
        name: "John Smith",
        email: "john.smith@example.com"
    },
    {
        id: 456,
        name: "Jane Doe",
        email: "jane.doe@example.com"
    }
];

const propertyTransformer: CsvPropertyTransformerMap<typeof list[0]> = {
    name: (item: typeof list[0]) => `"${item.name}"`,
    email: (item: typeof list[0]) => item.email.toLowerCase()
};

const csv = getCsvFromJsonObjectList(list, ['id', 'name', 'email'], true, propertyTransformer);
console.log(csv);
// Outputs:
// [
//   ["id", "name", "email"],
//   ["123", "\"John Smith\"", "john.smith@example.com"],
//   ["456", "\"Jane Doe\"", "jane.doe@example.com"]
// ]

Customizing the output

All the CSV generation functions in the HG CSV module accept optional parameters to customize the output of the generated CSV data.

In this section, we'll discuss how to use the separator, quote, and propertyTransformer parameters to customize the output of the CSV generation functions.

Changing the separator and quote characters

By default, the CSV generation functions use a comma (,) as the separator between cells and a double quote (") as the quote character around cell values that contain the separator or newline characters. You can change these characters by passing different values for the separator and quote parameters.

For example, here's how you could use the pipe (|) as the separator and single quotes (') as the quote character:

import { parseCsv } from './fi/hg/core/csv';

const csvString = "'name'|'email'|'age'\n'John Smith'|'john.smith@example.com'|'30'\n'Jane Doe'|'jane.doe@example.com'|'25'";

// Use the pipe character (|) as the separator and single quotes (') as the quote character
const csv = parseCsv(csvString, '|', "'");

console.log(csv);

This code parses the following CSV data:

'name'|'email'|'age'
'John Smith'|'john.smith@example.com'|'30'
'Jane Doe'|'jane.doe@example.com'|'25'

And produces the following output:

[
  ['name', 'email', 'age'],
  ['John Smith', 'john.smith@example.com', '30'],
  ['Jane Doe', 'jane.doe@example.com', '25']
]

Alternatively, you can use the stringifyCsv function to generate CSV data using different separator and quote characters.

Here is an example of how you can use the separator and quote parameters with the stringifyCsv function:

import { stringifyCsv } from './csv';

const csv = [
  ['name', 'email', 'age'],
  ['John Smith', 'john.smith@example.com', '30'],
  ['Jane Doe', 'jane.doe@example.com', '25'],
];

// Use the pipe character (|) as the separator and single quotes (') as the quote character
const csvString = stringifyCsv(csv, '|', "'");

console.log(csvString);

This code generates the following CSV data:

'name'|'email'|'age'
'John Smith'|'john.smith@example.com'|'30'
'Jane Doe'|'jane.doe@example.com'|'25'

The stringifyCsv function generates a string representation of the CSV data, using the specified separator and quote characters to separate cells and quote cell values that contain the separator or newline characters.

You can also use the default separator and quote characters by omitting the separator and quote parameters:

import { stringifyCsv } from './csv';

const csv = [
  ['name', 'email', 'age'],
  ['John Smith', 'john.smith@example.com', '30'],
  ['Jane Doe', 'jane.doe@example.com', '25'],
];

// Use the default separator (,) and quote (") characters
const csvString = stringifyCsv(csv);

console.log(csvString);

This code generates the following CSV data:

"name","email","age"
"John Smith","john.smith@example.com","30"
"Jane Doe","jane.doe@example.com","25"

Using property transformers to customize cell values

The propertyTransformer parameter allows you to provide custom functions that transform the values of certain properties before they are included in the CSV rows. The propertyTransformer parameter should be an object that maps property names to transformer functions. The transformer functions should take an object and a property name as arguments, and return the
transformed value for the property.

Here's an example of using the propertyTransformer parameter to customize the values of the name and email properties:

import { getCsvFromJsonObjectList } from './fi/hg/core/csv';

const list = [
    {
        id: 123,
        name: "John Smith",
        email: "john.smith@example.com"
    },
    {
        id: 456,
        name: "Jane Doe",
        email: "jane.doe@example.com"
    }
];

const propertyTransformer: CsvPropertyTransformerMap<typeof list[0]> = {
    name: (item: typeof list[0]) => `"${item.name}"`,
    email: (item: typeof list[0]) => item.email.toLowerCase()
};

const csv = getCsvFromJsonObjectList(list, ['id', 'name', 'email'], true, propertyTransformer);
console.log(csv);
// Outputs:
// [
//   ["id", "name", "email"],
//   ["123", "\"John Smith\"", "john.smith@example.com"],
//   ["456", "\"Jane Doe\"", "jane.doe@example.com"]
// ]

In this example, the name property values are surrounded by double quotes, and the email property values are converted to lower case.

Handling line breaks in cell values

Line breaks within cell values can cause issues when generating and parsing CSV data, as they can disrupt the expected structure of the CSV data.

To handle line breaks in cell values, the HG CSV module provides the replaceLineBreak option, which allows you to specify a character to replace line breaks in cell values with. By default, this constant is set to a single space ().

You can use the stringifyCsv function to replace line breaks in cell values with the character specified by the replaceLineBreak option when stringifying CSV data.

Working with quoted line breaks

Sometimes, CSV cell values may contain line breaks that are intended to be
included as part of the cell value, rather than being used to indicate the end of a row. In such cases, the cell value may be quoted to indicate that the line breaks within it are part of the cell value, rather than being used to split the rows.

TODO

Examples

Here are some examples of using the various functions in the HG CSV module to parse and generate CSV data:

Example 1: Generating CSV data from a list of objects

import { getCsvFromJsonObjectLis, CsvPropertyTransformerMapt } from './fi/hg/core/csv';

const list = [
    {
        id: 123,
        name: "John Smith",
        email: "john.smith@example.com",
        description: "Lorem ipsum dolor sit amet,\nconsectetur adipiscing elit."
    },
    {
        id: 456,
        name: "Jane Doe",
        email: "jane.doe@example.com",
        description: "Ut enim ad minim veniam,\nquis nostrud exercitation ullamco laboris."
    }
];

const propertyTransformer: CsvPropertyTransformerMap<typeof list[0]> = {
    description: (item: typeof list[0]) => item.description.toLowerCase()
};

const csv = getCsvFromJsonObjectList(list, ['id', 'name', 'email', 'description'], true, propertyTransformer);
console.log(csv);
// Outputs:
// [
//   ["123", "John Smith", "john.smith@example.com", "lorem ipsum dolor sit amet, consectetur adipiscing elit."],
//   ["456", "Jane Doe", "jane.doe@example.com", "ut enim ad minim veniam, quis nostrud exercitation ullamco laboris."]
// ]

In this example, we use the getCsvFromJsonObjectList function to generate CSV data from a list of objects. We specify the properties to include in the CSV rows using the properties parameter, and we set the includeHeader parameter to true to include a header row with the property names.

We also use the propertyTransformer parameter to provide a custom function that replaces uppercase characters in the description property with the lowercase character.

Example 2: Parsing CSV data

import { parseCsvRow } from './fi/hg/core/csv';

const csv = [
    'id,name,email,description',
    '123,John Smith,john.smith@example.com,"Lorem ipsum dolor sit amet,\nconsectetur adipiscing elit."',
    '456,Jane Doe,jane.doe@example.com,"Ut enim ad minim veniam,\nquis nostrud exercitation ullamco laboris."'
];

for (const row of csv) {
    let cells = parseCsvRow(row, ',', '"');
    console.log(cells);
}
// Outputs:
// ["id", "name", "email", "description"]
// ["123", "John Smith", "john.smith@example.com", "Lorem ipsum dolor sit amet,\nconsectetur adipiscing elit."]
// ["456", "Jane Doe", "jane.doe@example.com", "Ut enim ad minim veniam,\nquis nostrud exercitation ullamco laboris."]

Example 3: Generating CSV data

import { getCsvFromJsonObjectList } from './fi/hg/core/csv';

const list = [
    { id: 123, name: 'John Smith', email: 'john.smith@example.com', description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' },
    { id: 456, name: 'Jane Doe', email: 'jane.doe@example.com', description: 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.' }
];

const csv = getCsvFromJsonObjectList(list, ['id', 'name', 'email', 'description'], true, undefined);
console.log(csv);
// Outputs:
// [
//  ["id", "name", "email", "description"],
//  ["123", "John Smith", "john.smith@example.com", "Lorem ipsum dolor sit amet, consectetur adipiscing elit."],
//  ["456", "Jane Doe", "jane.doe@example.com", "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris."]
// ]

In this example, we use the getCsvFromJsonObjectList function to generate CSV data from a list of objects. We specify the properties to include in the CSV rows using the properties parameter, and we set the includeHeader parameter to true to include a header row with the property names.

Example 4: Using property transformers to customize cell values

import { getCsvFromJsonObjectList } from './fi/hg/core/csv';

const list = [
    { id: 123, name: 'John Smith', email: 'john.smith@example.com', description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' },
    { id: 456, name: 'Jane Doe', email: 'jane.doe@example.com', description: 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.' }
];

const propertyTransformer : CsvPropertyTransformerMap = {
    email: (item: ReadonlyJsonObject, key: string): string => {
        return `${key}: ${item[key]}`;
    }
};

const csv = getCsvFromJsonObjectList(list, ['id', 'name', 'email', 'description'], true, propertyTransformer);
console.log(csv);
// Outputs:
// [
//  ["id", "name", "email", "description"],
//  ["123", "John Smith", "email: john.smith@example.com", "Lorem ipsum dolor sit amet, consectetur adipiscing elit."],
//  ["456", "Jane Doe", "email: jane.doe@example.com", "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris."]
// ]

In this example, we use the propertyTransformer parameter to provide custom functions that transform the values of certain properties before they are included in the CSV rows.

We define a propertyTransformer object with a email property, which is a function that takes an object and a property key as its parameters and returns a string with the property key and value.

We pass the propertyTransformer object to the getCsvFromJsonObjectList function as the propertyTransformer parameter. This causes the email property values in the CSV rows to be transformed by the custom function before being included in the CSV data.

As a result, the email cell values in the CSV rows are modified to include the property key, resulting in cell values like "email: john.smith@example.com" instead of just "john.smith@example.com".

Example 5: Handling line breaks in cell values

import { getCsvFromJsonObjectList, parseCsvRow } from './fi/hg/core/csv';

const list = [
    { id: 123, name: 'John Smith', email: 'john.smith@example.com', description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.\nSuspendisse in ipsum quis nisl ultricies rhoncus at sit amet dolor.' },
    { id: 456, name: 'Jane Doe', email: 'jane.doe@example.com', description: 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.\nConsectetur adipiscing elit.' }
];

const csv = getCsvFromJsonObjectList(list, ['id', 'name', 'email', 'description'], true);
console.log(csv);
// Outputs:
// [
//  ["id", "name", "email", "description"],
//  ["123", "John Smith", "john.smith@example.com", "Lorem ipsum dolor sit amet, consectetur adipiscing elit.\nSuspendisse in ipsum quis nisl ultricies rhoncus at sit amet dolor."],
//  ["456", "Jane Doe", "jane.doe@example.com", "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.\nConsectetur adipiscing elit."]
// ]

const csvString = stringifyCsv(csv, undefined, undefined, "\n");
console.log(csvString);
// Outputs:
// id,name,email,description
// 123,John Smith,john.smith@example.com,"Lorem ipsum dolor sit amet, consectetur adipiscing elit.\nSuspendisse in ipsum quis nisl ultricies rhoncus at sit amet dolor."
// 456,Jane Doe,jane.doe@example.com,"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.\nConsectetur adipiscing elit."

const parsedCsv = parseCsv(csvString);
console.log(parsedCsv);
// Outputs:
// [
//  ["id", "name", "email", "description"],
//  ["123", "John Smith", "john.smith@example.com", "Lorem ipsum dolor sit amet, consectetur adipiscing elit.\nSuspendisse in ipsum quis nisl ultricies rhoncus at sit amet dolor."],
//  ["456", "Jane Doe", "jane.doe@example.com", "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.\nConsectetur adipiscing elit."]
// ]

console.log(parsedCsv[1][3]);
// Outputs: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.\nSuspendisse in ipsum quis nisl ultricies rhoncus at sit amet dolor."