UNPKG

jtc-utils

Version:
362 lines (334 loc) 11.1 kB
import type { FileHandle } from "node:fs/promises"; import type { Writable } from "node:stream"; import type { Charset, CharsetEncoder } from "./charset/charset.cjs"; import { utf8 } from "./charset/utf8.cjs"; export declare type FixlenWriterColumn = { length: number; shift?: boolean; fill?: "left" | "right"; filler?: string; type?: | "zerofill" | "int-le" | "int-be" | "uint-le" | "uint-be" | "zoned" | "packed"; }; export class FixlenWriter { private writer: Promise<WritableStreamDefaultWriter<Uint8Array>>; private encoder: CharsetEncoder; private ebcdic: boolean; private shift: boolean; private filler: Uint8Array; private lineSeparator?: Uint8Array; private columns: (FixlenWriterColumn & { fillerBytes: Uint8Array })[]; private lineLength: number; private bom: boolean; private fatal: boolean; private index = 0; constructor( dest: WritableStream<Uint8Array> | FileHandle | Writable, config: { columns: FixlenWriterColumn[]; charset?: Charset; bom?: boolean; shift?: boolean; filler?: string; lineSeparator?: string; fatal?: boolean; }, ) { const charset = config.charset ?? utf8; this.encoder = charset.createEncoder(); this.ebcdic = charset.isEbcdic(); this.bom = charset.isUnicode() ? (config.bom ?? false) : false; this.shift = config.shift ?? false; this.filler = this.encoder.encode(config.filler || " ", { shift: this.shift, }); if (config.lineSeparator) { this.lineSeparator = this.encoder.encode(config.lineSeparator, { shift: this.shift, }); } this.fatal = config.fatal ?? true; this.lineLength = 0; this.columns = []; for (let pos = 0; pos < config.columns.length; pos++) { const col = config.columns[pos]; if (col.length <= 0) { throw new RangeError(`column length must be positive: ${col.length}`); } this.lineLength += col.length; const colShift = col.shift ?? this.shift; this.columns.push({ length: col.length, shift: colShift, fill: col.fill, fillerBytes: col.filler ? this.encoder.encode(col.filler, { shift: colShift }) : this.filler, type: col.type, }); } if (this.lineSeparator) { this.lineLength += this.lineSeparator.length; } let stream: Promise<WritableStream<Uint8Array>>; if (dest instanceof WritableStream) { stream = Promise.resolve(dest); } else { const writable = "createWriteStream" in dest ? dest.createWriteStream() : dest; if ( "constructor" in writable && "toWeb" in writable.constructor && typeof writable.constructor.toWeb === "function" ) { stream = Promise.resolve(writable.constructor.toWeb(writable)); } else { throw new TypeError(`Unsuppoted destination: ${dest}`); } } this.writer = stream.then((value) => value.getWriter()); } async write( record: unknown[], options?: { columns: FixlenWriterColumn[]; shift?: boolean; filler?: string; lineSeparator?: string; }, ) { let shift = this.shift; let filler = this.filler; let lineSeparator = this.lineSeparator; let columns = this.columns; let lineLength = this.lineLength; if (options?.shift != null) { shift = options.shift; } if (options?.filler != null) { filler = this.encoder.encode(options.filler, { shift }); } if (options?.lineSeparator != null) { lineSeparator = this.encoder.encode(options.lineSeparator); } if (options?.columns) { columns = []; lineLength = 0; for (let pos = 0; pos < options.columns.length; pos++) { const col = options.columns[pos]; if (col.length <= 0) { throw new RangeError(`column length must be positive: ${col.length}`); } lineLength += col.length; const colShift = col.shift ?? this.shift; columns.push({ length: col.length, shift: col.shift ?? false, fill: col.fill, fillerBytes: col.filler ? this.encoder.encode(col.filler, { shift: colShift }) : filler, type: col.type, }); } if (lineSeparator) { lineLength += lineSeparator.length; } } let buf: Uint8Array; let start = 0; if (this.bom) { const bomBytes = this.encoder.encode("\uFEFF"); buf = new Uint8Array(bomBytes.length + lineLength); buf.set(bomBytes, 0); start += bomBytes.length; this.bom = false; } else { buf = new Uint8Array(lineLength); } for (let pos = 0; pos < columns.length; pos++) { const col = columns[pos]; const value = record[pos]; const isNumber = typeof value === "number" && Number.isFinite(value); const type = isNumber ? col.type : undefined; const fill = col.fill ?? (isNumber ? "right" : "left"); if (!type) { const text = value != null ? value.toString() : ""; let encoded = this.encoder.encode(text, { shift: col.shift }); if (encoded.length > col.length) { if (this.fatal) { throw new RangeError("overflow error"); } encoded = this.encoder.encode(text, { shift: col.shift, limit: col.length, }); } if (encoded.length === col.length) { buf.set(encoded, start); } else { if ((col.length - encoded.length) % col.fillerBytes.length !== 0) { throw new RangeError( `filler length is mismatched: ${col.fillerBytes.length}`, ); } if (fill === "right") { for ( let i = 0; i < col.length - encoded.length; i += col.fillerBytes.length ) { buf.set(col.fillerBytes, start + i); } buf.set(encoded, col.length - encoded.length); } else { for ( let i = 0; i < col.length - encoded.length; i += col.fillerBytes.length ) { buf.set(col.fillerBytes, start + encoded.length + i); } buf.set(encoded, start); } } } else if (!Number.isInteger(value)) { if (this.fatal) { throw new RangeError(`value must be integer: ${value}`); } else { buf.fill(0, start, start + col.length); } } else if (type === "zerofill") { const num = value as number; const encoded = new Uint8Array(col.length); let pos = 0; const sign = Math.sign(num) < 0; const text = Math.abs(num).toFixed(); if (sign) { const minus = this.encoder.encode("-", { shift: col.shift }); encoded.set(minus, pos); pos += minus.length; } const content = this.encoder.encode(text, { shift: col.shift }); if (pos + content.length < col.length) { const zero = this.encoder.encode("0", { shift: col.shift }); for (; pos < col.length - content.length; pos += zero.length) { encoded.set(zero, pos); } } if (pos + content.length === col.length) { encoded.set(content, col.length - content.length); buf.set(encoded, start); } else if (this.fatal) { throw new RangeError(`overflow error: ${value}`); } else { buf.fill(0, start, start + col.length); } } else if (type.startsWith("int-")) { const num = value as number; const view = new DataView(buf.buffer); const littleEndien = type === "int-le"; if (col.length === 4) { view.setInt32(start, num, littleEndien); } else if (col.length === 2) { view.setInt16(start, num, littleEndien); } else if (col.length === 1) { view.setInt8(start, num); } else if (this.fatal) { throw new RangeError("byte length must be 1, 2 or 4."); } else { buf.fill(0, start, start + col.length); } } else if (type.startsWith("uint-")) { const num = value as number; const view = new DataView(buf.buffer); const littleEndien = type === "uint-le"; if (col.length === 4) { view.setUint32(start, num, littleEndien); } else if (col.length === 2) { view.setUint16(start, num, littleEndien); } else if (col.length === 1) { view.setUint8(start, num); } else if (this.fatal) { throw new RangeError("length must be 1, 2 or 4."); } else { buf.fill(0, start, start + col.length); } } else if (type === "zoned") { const num = value as number; const sign = Math.sign(num) < 0; const text = Math.abs(num).toFixed(); if (this.fatal && col.length < text.length) { throw new RangeError(`length is too short: ${col.length}`); } for (let i = 0; i < col.length; i++) { let n = i < col.length - text.length ? 0 : text.charCodeAt(i - (col.length - text.length)) - 0x30; if (n > 0xa) { if (this.fatal) { throw new RangeError(`Invalid value: ${n}`); } else { n = 0; } } if (i === col.length - 1) { buf[start + i] = (sign ? 0xd0 : 0xc0) | n; } else { buf[start + i] = (this.ebcdic ? 0xf0 : 0x30) | n; } } } else if (type === "packed") { const num = value as number; const sign = Math.sign(num) < 0; const text = Math.abs(num).toFixed(); if (this.fatal && col.length < Math.ceil((text.length + 1) / 2)) { throw new RangeError(`length is too short: ${col.length}`); } buf[start + col.length - 1] = sign ? 0x0d : 0x0c; for (let i = 0; i < text.length; i++) { let n = text.charCodeAt(text.length - 1 - i) - 0x30; if (n > 0xa) { if (this.fatal) { throw new RangeError(`Invalid value: ${n}`); } else { n = 0; } } if (i % 2 === 1) { buf[start + col.length - 1 - ((i + 1) >>> 1)] = n; } else { buf[start + col.length - 1 - (i >>> 1)] |= n << 4; } } } else { throw new RangeError(`unknown column type: ${type}`); } start += col.length; } if (lineSeparator) { buf.set(lineSeparator, buf.length - lineSeparator.length); } this.index++; const writer = await this.writer; await writer.write(buf); } get count() { return this.index; } async close() { const writer = await this.writer; if (this.bom) { await writer.write(this.encoder.encode("\uFEFF")); this.bom = false; } await writer.close(); } }