@typecad/jlcpcb-parts
Version:
Intelligent fuzzy search for JLCPCB electrical components with CLI interface
148 lines • 5.5 kB
JavaScript
import { createReadStream } from 'fs';
import { createInterface } from 'readline';
/**
* Handles parsing CSV files into ComponentRecord objects
*/
export class CsvParser {
/**
* Parses a CSV file into an array of ComponentRecord objects
* @param filePath Path to the CSV file
* @returns Promise that resolves to an array of ComponentRecord objects
* @throws ParsingError if parsing fails
*/
async parseFile(filePath) {
try {
const components = [];
let headers = [];
let lineNumber = 0;
// Create a read stream for the file
const fileStream = createReadStream(filePath, { encoding: 'utf-8' });
const rl = createInterface({
input: fileStream,
crlfDelay: Infinity
});
// Process each line
for await (const line of rl) {
lineNumber++;
// Skip empty lines
if (!line.trim()) {
continue;
}
// Parse the CSV line
const values = this.parseCsvLine(line);
if (lineNumber === 1) {
// First line contains headers
headers = values;
// Validate headers
this.validateHeaders(headers);
}
else {
// Create component record
try {
const component = this.createComponentRecord(headers, values);
components.push(component);
}
catch (error) {
console.warn(`Warning: Skipping malformed row at line ${lineNumber}: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
return components;
}
catch (error) {
const parsingError = new Error(`Failed to parse CSV file: ${error instanceof Error ? error.message : String(error)}`);
throw parsingError;
}
}
/**
* Parses a single CSV line into an array of values
* @param line CSV line to parse
* @returns Array of values
* @private
*/
parseCsvLine(line) {
const values = [];
let currentValue = '';
let inQuotes = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
if (char === '"') {
// Handle quotes
if (inQuotes && i + 1 < line.length && line[i + 1] === '"') {
// Escaped quote inside quotes
currentValue += '"';
i++; // Skip the next quote
}
else {
// Toggle quote state
inQuotes = !inQuotes;
}
}
else if (char === ',' && !inQuotes) {
// End of value
values.push(currentValue);
currentValue = '';
}
else {
// Add character to current value
currentValue += char;
}
}
// Add the last value
values.push(currentValue);
return values;
}
/**
* Validates that the CSV headers contain the required fields
* @param headers Array of header names
* @throws Error if required headers are missing
* @private
*/
validateHeaders(headers) {
const requiredHeaders = [
'lcsc', 'category', 'subcategory', 'mfr', 'package',
'manufacturer', 'description', 'datasheet'
];
const missingHeaders = requiredHeaders.filter(header => !headers.includes(header));
if (missingHeaders.length > 0) {
throw new Error(`CSV file is missing required headers: ${missingHeaders.join(', ')}`);
}
}
/**
* Creates a ComponentRecord from headers and values
* @param headers Array of header names
* @param values Array of values
* @returns ComponentRecord object
* @throws Error if values array doesn't match headers array
* @private
*/
createComponentRecord(headers, values) {
if (headers.length !== values.length) {
throw new Error(`Value count (${values.length}) doesn't match header count (${headers.length})`);
}
// Create an object with all headers as keys
const record = {};
for (let i = 0; i < headers.length; i++) {
record[headers[i]] = values[i];
}
// Extract description from extra JSON field if description column is empty
if (!record.description || record.description.trim() === '') {
try {
const extraIndex = headers.indexOf('extra');
if (extraIndex !== -1 && values[extraIndex]) {
const extraData = JSON.parse(values[extraIndex]);
if (extraData.description && typeof extraData.description === 'string') {
record.description = extraData.description;
}
}
}
catch (error) {
// If JSON parsing fails, keep the empty description
// This is not a critical error, so we just continue
}
}
// Cast to ComponentRecord
return record;
}
}
//# sourceMappingURL=CsvParser.js.map