UNPKG

jtc-utils

Version:
306 lines (305 loc) 10.7 kB
import { utf8 } from "./charset/utf8.mjs"; export class FixlenReader { reader; decoder; lineLength; columns; shift; fatal; buf = new Uint8Array(); index = 0; constructor(src, config) { 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; if (typeof src === "string") { stream = Promise.resolve(new ReadableStream({ start(controller) { const encoder = charset.createEncoder(); controller.enqueue(encoder.encode(src)); controller.close(); }, })); } else if (src instanceof Uint8Array) { stream = Promise.resolve(new ReadableStream({ 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) { const items = new Array(); 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; 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) => { const col = { ...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]() { let record; while ((record = await this.read()) != null) { yield record; } } get count() { return this.index; } async close() { const reader = await this.reader; await reader.cancel(); } normalizeColumns(lineLength, columns, shift) { 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; 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, }; }); } decode(col, line) { 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 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 must be 1, 2 or 4."); } return Number.NaN; } } else if (col.type === "zoned") { try { let num = 0; for (let i = col.start; i < col.end; i++) { if (i + 1 === col.end) { const h4 = (line[i] >>> 4) & 0xf; if (h4 > 0x9) { throw new RangeError("high 4 bit at last must be A-F."); } if (h4 === 0xb || h4 === 0xd) { num = -1 * num; } } const l4 = line[i] & 0xf; if (l4 > 0x9) { throw new RangeError("low 4 bit must be decimal."); } num = num * 10 + l4; } return num; } catch (err) { if (this.fatal) { throw err; } return Number.NaN; } } else if (col.type === "packed") { try { let num = 0; for (let i = col.start; i < col.end; i++) { const h4 = (line[i] >> 8) & 0xf; if (h4 > 0x9) { throw new RangeError("high 4 bit must be decimal."); } num = num * 10 + h4; const l4 = line[i] & 0xf; if (i + 1 === col.end) { if (l4 > 0x9) { throw new RangeError("low 4 bit at last must be A-F."); } if (l4 === 0xb || l4 === 0xd) { num = -1 * num; } } else { if (l4 > 0x9) { throw new RangeError("low 4 bit must be decimal."); } 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}`); } } concat(a1, a2) { const result = new Uint8Array(a1.length + a2.length); result.set(a1, 0); result.set(a2, a1.length); return result; } }