UNPKG

jtc-utils

Version:
377 lines (353 loc) 11 kB
import type { FileHandle } from "node:fs/promises"; import type { Readable } from "node:stream"; import type { Charset, CharsetDecoder } from "./charset/charset.ts"; import { utf8 } from "./charset/utf8.ts"; export declare type FixlenReaderColumn = { start: number; length?: number; shift?: boolean; trim?: "left" | "right" | "both"; type?: | "decimal" | "int-le" | "int-be" | "uint-le" | "uint-be" | "zoned" | "uzoned" | "packed" | "upacked"; }; export interface FixlenLineDecoder { decode(column: FixlenReaderColumn): string | number; } export class FixlenReader { private reader: Promise<ReadableStreamDefaultReader<Uint8Array>>; private decoder: CharsetDecoder; private lineLength: number; private columns: | (FixlenReaderColumn & { end: number })[] | ((line: FixlenLineDecoder) => FixlenReaderColumn[]); private shift: boolean; private fatal: boolean; private buf = new Uint8Array(); private index = 0; constructor( src: | string | Uint8Array | Blob | ReadableStream<Uint8Array> | FileHandle | Readable, config: { lineLength: number; columns: | FixlenReaderColumn[] | ((line: FixlenLineDecoder) => FixlenReaderColumn[]); charset?: Charset; bom?: boolean; shift?: boolean; fatal?: boolean; }, ) { const charset = config.charset ?? utf8; this.fatal = config.fatal ?? true; this.shift = config.shift ?? false; this.lineLength = config.lineLength; this.columns = Array.isArray(config.columns) ? this.normalizeColumns(this.lineLength, config.columns, this.shift) : config.columns; let stream: Promise<ReadableStream<Uint8Array>>; if (typeof src === "string") { stream = Promise.resolve( new ReadableStream<Uint8Array>({ start(controller) { const encoder = charset.createEncoder(); controller.enqueue(encoder.encode(src)); controller.close(); }, }), ); } else if (src instanceof Uint8Array) { stream = Promise.resolve( new ReadableStream<Uint8Array>({ start(controller) { controller.enqueue(src); controller.close(); }, }), ); } else if (src instanceof Blob) { stream = Promise.resolve(src.stream()); } else if (src instanceof ReadableStream) { stream = Promise.resolve(src); } else { const readable = "createReadStream" in src ? src.createReadStream() : src; if ( "constructor" in readable && "toWeb" in readable.constructor && typeof readable.constructor.toWeb === "function" ) { stream = Promise.resolve(readable.constructor.toWeb(readable)); } else { throw new TypeError(`Unsuppoted source: ${src}`); } } this.decoder = charset.createDecoder({ fatal: this.fatal, ignoreBOM: config.bom != null ? !config.bom : false, }); this.reader = stream.then((value) => value.getReader()); } async read(options?: { lineLength: number; columns: | FixlenReaderColumn[] | ((line: FixlenLineDecoder) => FixlenReaderColumn[]); shift?: boolean; }): Promise<(string | number)[] | undefined> { const items = new Array<string | number>(); const lineLength = options?.lineLength ?? this.lineLength; const shift = options?.shift ?? this.shift; let done = false; let buf = this.buf; const reader = await this.reader; do { const readed = await reader.read(); done = readed.done; if (readed.value) { buf = buf.length > 0 ? this.concat(buf, readed.value) : readed.value; } } while (!done && buf.length < lineLength); let line: Uint8Array; if (buf.length === 0) { this.buf = buf; return; } else if (buf.length < lineLength) { if (this.fatal) { throw new TypeError("Insufficient data."); } line = buf; this.buf = buf.subarray(buf.length); } else { line = buf.subarray(0, lineLength); this.buf = buf.subarray(lineLength); } let columns = !options?.columns ? this.columns : Array.isArray(options.columns) ? this.normalizeColumns(lineLength, options.columns, shift) : options.columns; if (!Array.isArray(columns)) { columns = this.normalizeColumns( lineLength, columns({ decode: (column: FixlenReaderColumn) => { const col: FixlenReaderColumn & { end: number } = { ...column, end: column.length != null ? column.start + column.length : lineLength, }; return this.decode(col, line); }, }), shift, ); } for (let i = 0; i < columns.length; i++) { items.push(this.decode(columns[i], line)); } this.index++; return items; } async *[Symbol.asyncIterator](): AsyncGenerator<(string | number)[]> { let record: (string | number)[] | undefined; while ((record = await this.read()) != null) { yield record; } } get count() { return this.index; } async close() { const reader = await this.reader; await reader.cancel(); } private normalizeColumns( lineLength: number, columns: FixlenReaderColumn[], shift: boolean, ): (FixlenReaderColumn & { end: number })[] { return columns.map((col, index, array) => { if (col.start < 0) { throw new RangeError(`columns[${index}].start must be positive.`); } if (col.start >= lineLength) { throw new RangeError(`columns[${index}].start is too large.`); } if (col.length != null && col.length <= 0) { throw new RangeError(`columns[${index}].length must be positive.`); } let end: number; if (col.length != null) { end = col.start + col.length; if (end > lineLength) { throw new RangeError(`columns[${index}].length is too large.`); } } else if (index + 1 === array.length) { end = lineLength; } else if (col.start < array[index + 1].start) { end = array[index + 1].start; } else { throw new RangeError(`columns[${index}].length is required.`); } return { start: col.start, end, length: col.length, shift: col.shift ?? shift, trim: col.trim, type: col.type, }; }); } private decode(col: FixlenReaderColumn & { end: number }, line: Uint8Array) { if (!col.type || col.type === "decimal") { let text = ""; const minEnd = Math.min(col.end, line.length); if (col.start < minEnd) { text = this.decoder.decode(line.subarray(col.start, minEnd), { shift: col.shift, }); switch (col.trim) { case "left": text = text.trimStart(); break; case "right": text = text.trimEnd(); break; case "both": text = text.trim(); break; } } if (col.type === "decimal") { const num = Number.parseFloat(text); if (this.fatal && Number.isNaN(num)) { throw new RangeError(`Invalid number: ${text}`); } return num; } return text; } else if (col.type.startsWith("int-")) { const view = new DataView(line.buffer, line.byteOffset, line.byteLength); const littleEndien = col.type === "int-le"; const len = col.end - col.start; if (len === 4) { return view.getInt32(col.start, littleEndien); } else if (len === 2) { return view.getInt16(col.start, littleEndien); } else if (len === 1) { return view.getInt8(col.start); } else { if (this.fatal) { throw new RangeError( `byte length of ${col.type} type must be 1, 2 or 4.`, ); } return Number.NaN; } } else if (col.type.startsWith("uint-")) { const view = new DataView(line.buffer, line.byteOffset, line.byteLength); const littleEndien = col.type === "uint-le"; const len = col.end - col.start; if (len === 4) { return view.getUint32(col.start, littleEndien); } else if (len === 2) { return view.getUint16(col.start, littleEndien); } else if (len === 1) { return view.getUint8(col.start); } else { if (this.fatal) { throw new RangeError( `byte length of ${col.type} type must be 1, 2 or 4.`, ); } return Number.NaN; } } else if (col.type === "zoned" || col.type === "uzoned") { try { let num = 0; for (let i = col.start; i < col.end; i++) { const l4 = line[i] & 0xf; if (l4 > 0x9) { throw new RangeError( `low 4 bits of ${col.type} type must be 0-9: 0x${l4.toString(16).padStart(2, "0")}`, ); } num = num * 10 + l4; if (col.type === "zoned" && i + 1 === col.end) { const h4 = (line[i] >>> 4) & 0xf; if (h4 === 0xb || h4 === 0xd || h4 === 0x7) { num *= -1; } } } return num; } catch (err) { if (this.fatal) { throw err; } return Number.NaN; } } else if (col.type === "packed" || col.type === "upacked") { try { let num = 0; for (let i = col.start; i < col.end; i++) { const h4 = (line[i] >> 4) & 0xf; if (h4 > 0x9) { throw new RangeError( `high 4 bits of ${col.type} type must be 0-9: 0x${h4.toString(16).padStart(2, "0")}`, ); } num = num * 10 + h4; const l4 = line[i] & 0xf; if (col.type === "packed" && i + 1 === col.end) { if (l4 <= 0x9) { throw new RangeError( `last low 4 bits of ${col.type} type must be A-F: 0x${l4.toString(16).padStart(2, "0")}`, ); } if (l4 === 0xb || l4 === 0xd) { num *= -1; } } else { if (l4 > 0x9) { throw new RangeError( `low 4 bits of ${col.type} type must be 0-9: 0x${l4.toString(16).padStart(2, "0")}`, ); } num = num * 10 + l4; } } return num; } catch (err) { if (this.fatal) { throw err; } return Number.NaN; } } else { throw new RangeError(`unknown column type: ${col.type}`); } } private concat(a1: Uint8Array, a2: Uint8Array) { const result = new Uint8Array(a1.length + a2.length); result.set(a1, 0); result.set(a2, a1.length); return result; } }