import { FileDescriptor } from '../descriptors/FileDescriptor.js';
/**
* Sequential reader and iterator for Fixed Width Files.
* Parses raw file content line-by-line according to a FileDescriptor specification.
*
* @example
* const reader = new Reader(fwfContent, fileDescriptor);
* for (const row of reader) {
* console.log(row);
* }
*/
export class Reader {
/**
* Creates an instance of Reader.
*
* @param {string | string[] | Iterable<string>} iterable - Content as a string, line array, or string iterable.
* @param {FileDescriptor} fileDescriptor - Layout specification for the file.
* @param {string} [newline="\n"] - Line separator sequence.
* @throws {TypeError} If fileDescriptor is not a FileDescriptor instance or iterable is invalid.
*/
constructor(iterable, fileDescriptor, newline = '\n') {
if (!(fileDescriptor instanceof FileDescriptor)) {
throw new TypeError('fileDescriptor deve ser uma instância de FileDescriptor');
}
/**
* Layout descriptor for the file.
* @type {FileDescriptor}
*/
this.fileDescriptor = fileDescriptor;
/**
* Line separator character.
* @type {string}
*/
this.newline = newline;
/**
* Current line number index during iteration (1-based).
* @type {number}
*/
this.lineNum = 0;
/**
* Array of extracted and normalized raw line strings.
* @type {string[]}
*/
this.lines = this.extractLines(iterable);
this.validateAndNormalizeLines();
/**
* Total number of lines in the file.
* @type {number}
*/
this.linesCount = this.lines.length;
}
/**
* Normalizes and splits various input types into an array of string lines.
*
* @param {string | string[] | Iterable<string>} iterable - Input content.
* @returns {string[]} Array of lines.
* @throws {TypeError} If input type is not supported.
*/
extractLines(iterable) {
if (typeof iterable === 'string') {
const normalized = iterable.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
const rawLines = normalized.split('\n');
if (rawLines.length > 0 && rawLines[rawLines.length - 1] === '') {
rawLines.pop();
}
return rawLines;
}
if (Array.isArray(iterable)) {
return iterable.map((l) => (l.endsWith('\n') || l.endsWith('\r') ? l.replace(/[\r\n]+$/, '') : l));
}
if (iterable && typeof iterable[Symbol.iterator] === 'function') {
return Array.from(iterable).map((l) => (typeof l === 'string' ? l.replace(/[\r\n]+$/, '') : l));
}
throw new TypeError('Unsupported Iterable');
}
/**
* Validates line lengths against expected file line size and pads trailing spaces if needed.
*
* @throws {Error} If line length exceeds expected file line size.
*/
validateAndNormalizeLines() {
const expectedSize = this.fileDescriptor.lineSize;
for (let i = 0; i < this.lines.length; i++) {
let line = this.lines[i];
if (line.length > expectedSize) {
throw new Error(`A linha ${i + 1} possui tamanho ${line.length} diferente do esperado ${expectedSize}`);
}
if (line.length < expectedSize) {
this.lines[i] = line.padEnd(expectedSize, ' ');
}
}
}
/**
* Iterator generator producing parsed row objects sequentially.
*
* @yields {Record<string, *>} Parsed row values object for each line.
*/
*[Symbol.iterator]() {
this.lineNum = 0;
const totalLines = this.lines.length;
for (let i = 0; i < totalLines; i++) {
this.lineNum = i + 1;
const line = this.lines[i];
if (i === 0 && this.fileDescriptor.header) {
yield this.fileDescriptor.header.getValues(line);
} else if (i === totalLines - 1 && this.fileDescriptor.footer) {
yield this.fileDescriptor.footer.getValues(line);
} else {
yield this.fileDescriptor.details[0].getValues(line);
}
}
}
}