jtc-utils
Version:
Utilities for Japanese Traditional Companies
306 lines (305 loc) • 10.7 kB
JavaScript
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;
}
}