billboard.js
Version:
Re-usable easy interface JavaScript chart library, based on D3 v4+
162 lines (131 loc) • 3.7 kB
text/typescript
/**
* Copyright (c) 2017 ~ present NAVER Corp.
* billboard.js project is licensed under the MIT license
* @see https://datatracker.ietf.org/doc/html/rfc4180
*/
const QUOTE = 34; // "
const NEWLINE = 10; // \n
const RETURN = 13; // \r
type RowCallback<T> = (row: string[], index: number) => T | undefined | null;
type ObjectCallback<T> = (d: Record<string, string>, index: number, columns: string[]) => T;
/**
* Create a DSV parser for the specified delimiter.
* @param {string} delimiter The delimiter character
* @returns {object} DSV parser object with parse and parseRows methods
* @private
*/
function dsv(delimiter: string) {
const delimiterCode = delimiter.charCodeAt(0);
/**
* Parse DSV string into rows of string arrays.
* @param {string} text DSV string to parse
* @param {function} [callback] Optional row callback
* @returns {Array} Parsed rows
* @private
*/
function parseRows<T = string[]>(text: string, callback?: RowCallback<T>): T[] {
const rows: T[] = [];
let len = text.length;
let pos = 0;
let rowNum = 0;
let eof = len <= 0;
let eol = false;
// Strip BOM
text.charCodeAt(0) === 0xfeff && pos++;
// Strip trailing newline
text.charCodeAt(len - 1) === NEWLINE && --len;
text.charCodeAt(len - 1) === RETURN && --len;
/**
* Get next token from DSV string.
* @returns {string|null} Next field value, or null for EOL/EOF
*/
function token(): string | null {
if (eof) {
return null;
}
if (eol) {
eol = false;
return null;
}
const start = pos;
let c: number, end: number;
// Quoted value
if (text.charCodeAt(start) === QUOTE) {
while (++pos < len) {
if (text.charCodeAt(pos) === QUOTE) {
if (text.charCodeAt(pos + 1) !== QUOTE) {
break;
}
pos++;
}
}
if ((end = pos++) >= len) {
eof = true;
} else if ((c = text.charCodeAt(pos++)) === NEWLINE) {
eol = true;
} else if (c === RETURN) {
eol = true;
text.charCodeAt(pos) === NEWLINE && ++pos;
}
return text.slice(start + 1, end).replace(/""/g, "\"");
}
// Unquoted value
while (pos < len) {
if ((c = text.charCodeAt(end = pos++)) === NEWLINE) {
eol = true;
} else if (c === RETURN) {
eol = true;
text.charCodeAt(pos) === NEWLINE && ++pos;
} else if (c !== delimiterCode) {
continue;
}
return text.slice(start, end);
}
eof = true;
return text.slice(start, len);
}
let t: string | null;
while ((t = token()) !== null) {
const row: string[] = [];
while (t !== null) {
row.push(t);
t = token();
}
if (callback) {
const result = callback(row, rowNum++);
result != null && rows.push(result);
} else {
rows.push(row as T);
}
}
return rows;
}
/**
* Parse DSV string into array of objects using first row as headers.
* @param {string} text DSV string to parse
* @param {function} [callback] Optional row callback
* @returns {Array} Array of objects
* @private
*/
function parse<T = Record<string, string>>(text: string, callback?: ObjectCallback<T>): T[] {
let columns: string[] = [];
return parseRows(text, (row, i) => {
if (i === 0) {
columns = row;
return null;
}
const obj: Record<string, string> = {};
for (let j = 0; j < columns.length; j++) {
obj[columns[j]] = row[j] ?? "";
}
return callback ? callback(obj, i - 1, columns) : obj as T;
});
}
return {parse, parseRows};
}
const csv = dsv(",");
const tsv = dsv("\t");
export const csvParse = csv.parse;
export const csvParseRows = csv.parseRows;
export const tsvParse = tsv.parse;
export const tsvParseRows = tsv.parseRows;