@bitbybit-dev/core
Version:
Bit By Bit Developers Core CAD API to Program Geometry
342 lines (341 loc) • 13.3 kB
JavaScript
/**
* Contains various CSV parsing and generation methods.
*/
export class CSVBitByBit {
/**
* Parses CSV text to a 2D array of strings (rows and columns).
* Example: csv='a,b,c\n1,2,3' → [['a','b','c'], ['1','2','3']]
* @param inputs CSV text and parsing options
* @returns 2D array of strings
* @group parse
* @shortname parse to array
* @drawable false
*/
parseToArray(inputs) {
// Convert literal escape sequences to actual characters
const rowSeparator = this.convertEscapeSequences(inputs.rowSeparator || "\n");
const columnSeparator = this.convertEscapeSequences(inputs.columnSeparator || ",");
// Also convert escape sequences in the CSV data itself (e.g., literal "\n" to actual newline)
const csvData = this.convertEscapeSequences(inputs.csv);
const lines = csvData.split(rowSeparator);
const result = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (!line)
continue;
const columns = this.parseCsvLine(line, columnSeparator);
result.push(columns);
}
return result;
}
/**
* Parses CSV text to an array of JSON objects using headers.
* Example: csv='name,age\nJohn,30\nJane,25', headerRow=0, dataStartRow=1
* → [{'name':'John','age':'30'}, {'name':'Jane','age':'25'}]
* @param inputs CSV text and parsing options
* @returns Array of JSON objects
* @group parse
* @shortname parse to json
* @drawable false
*/
parseToJson(inputs) {
var _a, _b;
const array = this.parseToArray({
csv: inputs.csv,
rowSeparator: inputs.rowSeparator,
columnSeparator: inputs.columnSeparator
});
if (array.length === 0)
return [];
const headerRow = (_a = inputs.headerRow) !== null && _a !== void 0 ? _a : 0;
const dataStartRow = (_b = inputs.dataStartRow) !== null && _b !== void 0 ? _b : 1;
if (headerRow >= array.length) {
throw new Error(`Header row ${headerRow} is out of bounds (total rows: ${array.length})`);
}
const headers = array[headerRow];
const numberColumnsSet = new Set(inputs.numberColumns || []);
const result = [];
for (let i = dataStartRow; i < array.length; i++) {
const row = array[i];
const obj = {};
headers.forEach((header, index) => {
const value = row[index] || "";
// Convert to number if this column is in the numberColumns list
if (numberColumnsSet.has(header)) {
const num = parseFloat(value);
// Normalize -0 to 0
obj[header] = num === 0 ? 0 : num;
}
else {
obj[header] = value;
}
});
result.push(obj);
}
return result;
}
/**
* Parses CSV text to JSON using custom headers (ignores CSV headers if present).
* Example: csv='John,30\nJane,25', headers=['name','age']
* → [{'name':'John','age':'30'}, {'name':'Jane','age':'25'}]
* @param inputs CSV text, custom headers, and parsing options
* @returns Array of JSON objects
* @group parse
* @shortname parse to json with headers
* @drawable false
*/
parseToJsonWithHeaders(inputs) {
var _a;
const array = this.parseToArray({
csv: inputs.csv,
rowSeparator: inputs.rowSeparator,
columnSeparator: inputs.columnSeparator
});
if (array.length === 0)
return [];
const dataStartRow = (_a = inputs.dataStartRow) !== null && _a !== void 0 ? _a : 0;
const numberColumnsSet = new Set(inputs.numberColumns || []);
const result = [];
for (let i = dataStartRow; i < array.length; i++) {
const row = array[i];
const obj = {};
inputs.headers.forEach((header, index) => {
const value = row[index] || "";
// Convert to number if this column is in the numberColumns list
if (numberColumnsSet.has(header)) {
const num = parseFloat(value);
// Normalize -0 to 0
obj[header] = num === 0 ? 0 : num;
}
else {
obj[header] = value;
}
});
result.push(obj);
}
return result;
}
/**
* Queries CSV data by column/header name and returns all values in that column.
* Example: csv='name,age\nJohn,30\nJane,25', column='name' → ['John', 'Jane']
* @param inputs CSV text, column name, and parsing options
* @returns Array of values from the specified column
* @group query
* @shortname query column
* @drawable false
*/
queryColumn(inputs) {
const numberColumns = inputs.asNumber ? [inputs.column] : undefined;
const jsonData = this.parseToJson({
csv: inputs.csv,
headerRow: inputs.headerRow,
dataStartRow: inputs.dataStartRow,
rowSeparator: inputs.rowSeparator,
columnSeparator: inputs.columnSeparator,
numberColumns: numberColumns
});
return jsonData.map(row => row[inputs.column] !== undefined ? row[inputs.column] : "");
}
/**
* Queries CSV data and filters rows where a column matches a value.
* Example: csv='name,age\nJohn,30\nJane,25', column='age', value='30' → [{'name':'John','age':'30'}]
* @param inputs CSV text, column name, value, and parsing options
* @returns Array of matching rows as JSON objects
* @group query
* @shortname query rows by value
* @drawable false
*/
queryRowsByValue(inputs) {
var _a;
const jsonData = this.parseToJson({
csv: inputs.csv,
headerRow: inputs.headerRow,
dataStartRow: inputs.dataStartRow,
rowSeparator: inputs.rowSeparator,
columnSeparator: inputs.columnSeparator,
numberColumns: inputs.numberColumns
});
// If the column is a number column, compare as numbers
const isNumberColumn = (_a = inputs.numberColumns) === null || _a === void 0 ? void 0 : _a.includes(inputs.column);
const compareValue = isNumberColumn ? parseFloat(inputs.value) : inputs.value;
return jsonData.filter(row => {
const rowValue = row[inputs.column];
if (isNumberColumn) {
return rowValue === compareValue;
}
return rowValue === inputs.value;
});
}
/**
* Converts a 2D array to CSV text.
* Example: array=[['name','age'], ['John','30']] → 'name,age\nJohn,30'
* @param inputs 2D array and formatting options
* @returns CSV text
* @group generate
* @shortname array to csv
* @drawable false
*/
arrayToCsv(inputs) {
const columnSeparator = this.convertEscapeSequences(inputs.columnSeparator || ",");
const rowSeparator = this.convertEscapeSequences(inputs.rowSeparator || "\n");
return inputs.array.map(row => row.map(cell => this.escapeCsvCell(cell, columnSeparator)).join(columnSeparator)).join(rowSeparator);
}
/**
* Converts an array of JSON objects to CSV text.
* Example: json=[{'name':'John','age':'30'}], headers=['name','age'] → 'name,age\nJohn,30'
* @param inputs JSON array, headers, and formatting options
* @returns CSV text
* @group generate
* @shortname json to csv
* @drawable false
*/
jsonToCsv(inputs) {
const columnSeparator = this.convertEscapeSequences(inputs.columnSeparator || ",");
const rowSeparator = this.convertEscapeSequences(inputs.rowSeparator || "\n");
if (!inputs.json || inputs.json.length === 0) {
return inputs.includeHeaders ? inputs.headers.join(columnSeparator) : "";
}
const lines = [];
// Add headers if requested
if (inputs.includeHeaders) {
lines.push(inputs.headers.map(h => this.escapeCsvCell(h, columnSeparator)).join(columnSeparator));
}
// Add data rows
inputs.json.forEach(obj => {
const row = inputs.headers.map(header => {
const value = obj[header];
return this.escapeCsvCell(value !== undefined && value !== null ? String(value) : "", columnSeparator);
});
lines.push(row.join(columnSeparator));
});
return lines.join(rowSeparator);
}
/**
* Converts an array of JSON objects to CSV text using object keys as headers.
* Example: json=[{'name':'John','age':'30'}] → 'name,age\nJohn,30'
* @param inputs JSON array and formatting options
* @returns CSV text
* @group generate
* @shortname json to csv auto
* @drawable false
*/
jsonToCsvAuto(inputs) {
var _a;
if (!inputs.json || inputs.json.length === 0)
return "";
// Get headers from first object
const headers = Object.keys(inputs.json[0]);
return this.jsonToCsv({
json: inputs.json,
headers: headers,
includeHeaders: (_a = inputs.includeHeaders) !== null && _a !== void 0 ? _a : true,
columnSeparator: inputs.columnSeparator,
rowSeparator: inputs.rowSeparator
});
}
/**
* Gets the headers from a CSV file.
* Example: csv='name,age\nJohn,30', headerRow=0 → ['name', 'age']
* @param inputs CSV text and options
* @returns Array of header names
* @group query
* @shortname get headers
* @drawable false
*/
getHeaders(inputs) {
var _a;
const array = this.parseToArray({
csv: inputs.csv,
rowSeparator: inputs.rowSeparator,
columnSeparator: inputs.columnSeparator
});
const headerRow = (_a = inputs.headerRow) !== null && _a !== void 0 ? _a : 0;
if (headerRow >= array.length) {
throw new Error(`Header row ${headerRow} is out of bounds (total rows: ${array.length})`);
}
return array[headerRow];
}
/**
* Gets the number of rows in a CSV file (excluding headers if specified).
* Example: csv='name,age\nJohn,30\nJane,25', headerRow=0 → 2
* @param inputs CSV text and options
* @returns Number of data rows
* @group query
* @shortname row count
* @drawable false
*/
getRowCount(inputs) {
var _a;
const array = this.parseToArray({
csv: inputs.csv,
rowSeparator: inputs.rowSeparator,
columnSeparator: inputs.columnSeparator
});
const dataStartRow = (_a = inputs.dataStartRow) !== null && _a !== void 0 ? _a : (inputs.hasHeaders ? 1 : 0);
return Math.max(0, array.length - dataStartRow);
}
/**
* Gets the number of columns in a CSV file.
* Example: csv='name,age,city\nJohn,30,NYC' → 3
* @param inputs CSV text and options
* @returns Number of columns
* @group query
* @shortname column count
* @drawable false
*/
getColumnCount(inputs) {
const array = this.parseToArray(inputs);
return array.length > 0 ? array[0].length : 0;
}
parseCsvLine(line, separator) {
const result = [];
let current = "";
let inQuotes = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
const nextChar = line[i + 1];
if (char === "\"") {
if (inQuotes && nextChar === "\"") {
// Escaped quote
current += "\"";
i++; // Skip next quote
}
else {
// Toggle quote mode
inQuotes = !inQuotes;
}
}
else if (char === separator && !inQuotes) {
// End of field
result.push(current);
current = "";
}
else {
current += char;
}
}
// Add last field
result.push(current);
return result;
}
escapeCsvCell(cell, separator) {
// Convert to string first
const cellStr = cell !== undefined && cell !== null ? String(cell) : "";
// If cell contains separator, quotes, or newlines, wrap in quotes
if (cellStr.includes(separator) || cellStr.includes("\"") || cellStr.includes("\n") || cellStr.includes("\r")) {
// Escape existing quotes by doubling them
return "\"" + cellStr.replace(/"/g, "\"\"") + "\"";
}
return cellStr;
}
/**
* Converts literal escape sequence strings to their actual characters.
* For example, converts "\\n" (two characters) to "\n" (newline character).
*/
convertEscapeSequences(str) {
return str
.replace(/\\n/g, "\n")
.replace(/\\t/g, "\t")
.replace(/\\r/g, "\r");
}
}