// // stata/dta-write.ts — DTA writer (port of readstat_dta_write.c) // import { ReadStatError } from "../errors.js"; import { ReadStatType, ReadStatAlignment, readstatTypeClass, ReadStatTypeClass, ReadStatCompress } from "../types.js"; import { Writer, StringRef, latin1Bytes } from "../writer.js"; import { Variable } from "../variable.js"; import { ValueLabel } from "../labelset.js"; import { BinaryWriter } from "../binary.js"; import { DtaConfig, dtaTypeInfo, DTA_LOHI, DTA_GSO_TYPE_ASCII, DTA_OLD_MAX_INT8, DTA_OLD_MAX_INT16, DTA_OLD_MAX_INT32, DTA_OLD_MISSING_INT8, DTA_OLD_MISSING_INT16, DTA_OLD_MISSING_INT32, DTA_OLD_MISSING_FLOAT, DTA_OLD_MISSING_DOUBLE, DTA_113_MAX_INT8, DTA_113_MAX_INT16, DTA_113_MAX_INT32, DTA_113_MAX_FLOAT, DTA_113_MAX_DOUBLE, DTA_113_MISSING_INT8, DTA_113_MISSING_INT16, DTA_113_MISSING_INT32, DTA_113_MISSING_FLOAT, DTA_113_MISSING_DOUBLE, DTA_113_MISSING_INT8_A, DTA_113_MISSING_INT16_A, DTA_113_MISSING_INT32_A, DTA_113_MISSING_FLOAT_A, DTA_113_MISSING_DOUBLE_A, DTA_117_TYPE_CODE_INT8, DTA_117_TYPE_CODE_INT16, DTA_117_TYPE_CODE_INT32, DTA_117_TYPE_CODE_FLOAT, DTA_117_TYPE_CODE_DOUBLE, DTA_117_TYPE_CODE_STRL, DTA_111_TYPE_CODE_INT8, DTA_111_TYPE_CODE_INT16, DTA_111_TYPE_CODE_INT32, DTA_111_TYPE_CODE_FLOAT, DTA_111_TYPE_CODE_DOUBLE, DTA_OLD_TYPE_CODE_INT8, DTA_OLD_TYPE_CODE_INT16, DTA_OLD_TYPE_CODE_INT32, DTA_OLD_TYPE_CODE_FLOAT, DTA_OLD_TYPE_CODE_DOUBLE, } from "./dta.js"; import { dtaFormatTimestamp } from "./dta-parse-timestamp.js"; import { epochToTm } from "../spss/sav-parse-timestamp.js"; const DTA_FILE_VERSION_DEFAULT = 118; const DTA_OLD_MAX_WIDTH = 128; const DTA_111_MAX_WIDTH = 244; const DTA_117_MAX_WIDTH = 2045; const DTA_OLD_MAX_NAME_LEN = 9; const DTA_110_MAX_NAME_LEN = 33; const DTA_118_MAX_NAME_LEN = 129; const DTA_DEFAULT_DISPLAY_WIDTH_BYTE = 8; const DTA_DEFAULT_DISPLAY_WIDTH_INT16 = 8; const DTA_DEFAULT_DISPLAY_WIDTH_INT32 = 12; const DTA_DEFAULT_DISPLAY_WIDTH_FLOAT = 9; const DTA_DEFAULT_DISPLAY_WIDTH_DOUBLE = 10; const DTA_DEFAULT_DISPLAY_WIDTH_STRING = 9; const utf8 = new TextEncoder(); function enc(s: string): Uint8Array { return utf8.encode(s); } /** strncpy(dest+off, str, entryLen) semantics: copy min(bytes, entryLen), rest stays 0. */ function fillField(dest: Uint8Array, off: number, entryLen: number, str: string): void { const b = enc(str); const n = Math.min(b.length, entryLen); dest.set(b.subarray(0, n), off); } class DtaWriteCtx { cfg: DtaConfig; nvar: number; nobs: number; typlist: number[]; varlist: Uint8Array; fmtlist: Uint8Array; lbllist: Uint8Array; variableLabels: Uint8Array; srtlist: Uint8Array; recordLen = 0; varlistLen: number; fmtlistLen: number; lbllistLen: number; variableLabelsLen: number; constructor(version: number, nvar: number, nobs: number) { this.cfg = new DtaConfig(version, DTA_LOHI, nvar); this.nvar = nvar; this.nobs = nobs; this.typlist = new Array(nvar).fill(0); this.varlistLen = this.cfg.variableNameLen * nvar; this.fmtlistLen = this.cfg.fmtlistEntryLen * nvar; this.lbllistLen = this.cfg.lbllistEntryLen * nvar; this.variableLabelsLen = this.cfg.variableLabelsEntryLen * nvar; this.varlist = new Uint8Array(this.varlistLen); this.fmtlist = new Uint8Array(this.fmtlistLen); this.lbllist = new Uint8Array(this.lbllistLen); this.variableLabels = new Uint8Array(this.variableLabelsLen); this.srtlist = new Uint8Array(this.cfg.srtlistLen); } } function writeTag(writer: Writer, ctx: DtaWriteCtx, tag: string): ReadStatError { if (!ctx.cfg.fileIsXmlish) return ReadStatError.OK; return writer.writeBytes(latin1Bytes(tag)); } function writeChunk(writer: Writer, ctx: DtaWriteCtx, start: string, bytes: Uint8Array, end: string): ReadStatError { let e = writeTag(writer, ctx, start); if (e !== ReadStatError.OK) return e; e = writer.writeBytes(bytes); if (e !== ReadStatError.OK) return e; return writeTag(writer, ctx, end); } function tagLen(ctx: DtaWriteCtx, tag: string): number { return ctx.cfg.fileIsXmlish ? tag.length : 0; } // ---- type codes ---- function typecodeForVariable(v: Variable, ctx: DtaWriteCtx): { code: number; error: ReadStatError } { const maxLen = v.storageWidth; const ver = ctx.cfg.typlistVersion; if (ver === 111) { switch (v.type) { case ReadStatType.INT8: return { code: DTA_111_TYPE_CODE_INT8, error: ReadStatError.OK }; case ReadStatType.INT16: return { code: DTA_111_TYPE_CODE_INT16, error: ReadStatError.OK }; case ReadStatType.INT32: return { code: DTA_111_TYPE_CODE_INT32, error: ReadStatError.OK }; case ReadStatType.FLOAT: return { code: DTA_111_TYPE_CODE_FLOAT, error: ReadStatError.OK }; case ReadStatType.DOUBLE: return { code: DTA_111_TYPE_CODE_DOUBLE, error: ReadStatError.OK }; case ReadStatType.STRING: return { code: maxLen, error: ReadStatError.OK }; case ReadStatType.STRING_REF: return { code: 0, error: ReadStatError.ERROR_STRING_REFS_NOT_SUPPORTED }; } } else if (ver === 117) { switch (v.type) { case ReadStatType.INT8: return { code: DTA_117_TYPE_CODE_INT8, error: ReadStatError.OK }; case ReadStatType.INT16: return { code: DTA_117_TYPE_CODE_INT16, error: ReadStatError.OK }; case ReadStatType.INT32: return { code: DTA_117_TYPE_CODE_INT32, error: ReadStatError.OK }; case ReadStatType.FLOAT: return { code: DTA_117_TYPE_CODE_FLOAT, error: ReadStatError.OK }; case ReadStatType.DOUBLE: return { code: DTA_117_TYPE_CODE_DOUBLE, error: ReadStatError.OK }; case ReadStatType.STRING: return { code: maxLen, error: ReadStatError.OK }; case ReadStatType.STRING_REF: return { code: DTA_117_TYPE_CODE_STRL, error: ReadStatError.OK }; } } else { switch (v.type) { case ReadStatType.INT8: return { code: DTA_OLD_TYPE_CODE_INT8, error: ReadStatError.OK }; case ReadStatType.INT16: return { code: DTA_OLD_TYPE_CODE_INT16, error: ReadStatError.OK }; case ReadStatType.INT32: return { code: DTA_OLD_TYPE_CODE_INT32, error: ReadStatError.OK }; case ReadStatType.FLOAT: return { code: DTA_OLD_TYPE_CODE_FLOAT, error: ReadStatError.OK }; case ReadStatType.DOUBLE: return { code: DTA_OLD_TYPE_CODE_DOUBLE, error: ReadStatError.OK }; case ReadStatType.STRING: return { code: maxLen + 0x7f, error: ReadStatError.OK }; case ReadStatType.STRING_REF: return { code: 0, error: ReadStatError.ERROR_STRING_REFS_NOT_SUPPORTED }; } } return { code: 0, error: ReadStatError.ERROR_PARSE }; } // ---- variable width ---- function numericVariableWidth(type: ReadStatType): number { switch (type) { case ReadStatType.DOUBLE: return 8; case ReadStatType.FLOAT: return 4; case ReadStatType.INT32: return 4; case ReadStatType.INT16: return 2; case ReadStatType.INT8: return 1; default: return 0; } } function variableWidth(version: number, type: ReadStatType, userWidth: number): number { if (type === ReadStatType.STRING) { const max = version >= 117 ? DTA_117_MAX_WIDTH : version >= 111 ? DTA_111_MAX_WIDTH : DTA_OLD_MAX_WIDTH; if (userWidth > max || userWidth === 0) userWidth = max; return userWidth; } if (type === ReadStatType.STRING_REF && version >= 117) return 8; return numericVariableWidth(type); } // ---- name validation ---- const RESERVED = new Set([ "_all", "_b", "byte", "_coef", "_cons", "double", "float", "if", "in", "int", "long", "_n", "_N", "_pi", "_pred", "_rc", "_skip", "strL", "using", "with", ]); function validateNameChars(name: string, unicode: boolean): ReadStatError { for (let j = 0; j < name.length; j++) { const c = name[j]; const code = name.charCodeAt(j); if ((code < 0x80 || !unicode) && c !== "_" && !(c >= "a" && c <= "z") && !(c >= "A" && c <= "Z") && !(c >= "0" && c <= "9")) { return ReadStatError.ERROR_NAME_CONTAINS_ILLEGAL_CHARACTER; } } const first = name[0]; const firstCode = name.charCodeAt(0); if ((firstCode < 0x80 || !unicode) && first !== "_" && !(first >= "a" && first <= "z") && !(first >= "A" && first <= "Z")) { return ReadStatError.ERROR_NAME_BEGINS_WITH_ILLEGAL_CHARACTER; } return ReadStatError.OK; } function validateName(name: string, unicode: boolean, maxLen: number): ReadStatError { if (enc(name).length > maxLen) return ReadStatError.ERROR_NAME_IS_TOO_LONG; if (name.length === 0) return ReadStatError.ERROR_NAME_IS_ZERO_LENGTH; const e = validateNameChars(name, unicode); if (e !== ReadStatError.OK) return e; if (RESERVED.has(name)) return ReadStatError.ERROR_NAME_IS_RESERVED_WORD; if (/^str\d+$/.test(name)) return ReadStatError.ERROR_NAME_IS_RESERVED_WORD; return ReadStatError.OK; } // ---- emit sections ---- function emitHeaderDataLabel(writer: Writer, ctx: DtaWriteCtx): ReadStatError { let e = writeTag(writer, ctx, ""); } function emitHeaderTimestamp(writer: Writer, ctx: DtaWriteCtx): ReadStatError { const cfg = ctx.cfg; if (!cfg.timestampLen) return ReadStatError.OK; const tm = epochToTm(writer.timestamp); // localtime equivalent — use local getters to match C localtime() const d = new Date(writer.timestamp * 1000); tm.tm_mday = d.getDate(); tm.tm_mon = d.getMonth(); tm.tm_year = d.getFullYear() - 1900; tm.tm_hour = d.getHours(); tm.tm_min = d.getMinutes(); const tsStr = dtaFormatTimestamp(tm); // "DD Mon YYYY HH:MM" (17 chars) const tsBytes = latin1Bytes(tsStr); if (cfg.fileIsXmlish) { let e = writeTag(writer, ctx, ""); if (e !== ReadStatError.OK) return e; e = writer.writeBytes(new Uint8Array([tsBytes.length & 0xff])); if (e !== ReadStatError.OK) return e; e = writer.writeBytes(tsBytes); if (e !== ReadStatError.OK) return e; return writeTag(writer, ctx, ""); } const buf = new Uint8Array(cfg.timestampLen); buf.set(tsBytes.subarray(0, Math.min(tsBytes.length, cfg.timestampLen))); return writer.writeBytes(buf); } function emitTyplist(writer: Writer, ctx: DtaWriteCtx): ReadStatError { let e = writeTag(writer, ctx, ""); if (e !== ReadStatError.OK) return e; for (let i = 0; i < ctx.nvar; i++) { const tc = typecodeForVariable(writer.variables[i], ctx); if (tc.error !== ReadStatError.OK) return tc.error; ctx.typlist[i] = tc.code; } const b = new BinaryWriter(true, ctx.nvar * ctx.cfg.typlistEntryLen); for (let i = 0; i < ctx.nvar; i++) { if (ctx.cfg.typlistEntryLen === 1) b.u8(ctx.typlist[i]); else b.u16(ctx.typlist[i]); } e = writer.writeBytes(b.finish()); if (e !== ReadStatError.OK) return e; return writeTag(writer, ctx, ""); } function emitVarlist(writer: Writer, ctx: DtaWriteCtx): ReadStatError { for (let i = 0; i < ctx.nvar; i++) { fillField(ctx.varlist, ctx.cfg.variableNameLen * i, ctx.cfg.variableNameLen, writer.variables[i].name); } return writeChunk(writer, ctx, "", ctx.varlist, ""); } function emitSrtlist(writer: Writer, ctx: DtaWriteCtx): ReadStatError { ctx.srtlist.fill(0); return writeChunk(writer, ctx, "", ctx.srtlist, ""); } function emitFmtlist(writer: Writer, ctx: DtaWriteCtx): ReadStatError { for (let i = 0; i < ctx.nvar; i++) { const v = writer.variables[i]; if (v.format && v.format[0]) { fillField(ctx.fmtlist, ctx.cfg.fmtlistEntryLen * i, ctx.cfg.fmtlistEntryLen, v.format); } else { let formatLetter = "g"; let displayWidth = v.displayWidth; if (readstatTypeClass(v.type) === ReadStatTypeClass.STRING) formatLetter = "s"; if (!displayWidth) { switch (v.type) { case ReadStatType.INT8: displayWidth = DTA_DEFAULT_DISPLAY_WIDTH_BYTE; break; case ReadStatType.INT16: displayWidth = DTA_DEFAULT_DISPLAY_WIDTH_INT16; break; case ReadStatType.INT32: displayWidth = DTA_DEFAULT_DISPLAY_WIDTH_INT32; break; case ReadStatType.FLOAT: displayWidth = DTA_DEFAULT_DISPLAY_WIDTH_FLOAT; break; case ReadStatType.DOUBLE: displayWidth = DTA_DEFAULT_DISPLAY_WIDTH_DOUBLE; break; default: displayWidth = DTA_DEFAULT_DISPLAY_WIDTH_STRING; break; } } const dash = v.alignment === ReadStatAlignment.LEFT ? "-" : ""; const format = formatLetter === "g" ? `%${dash}${displayWidth}.0g` : `%${dash}${displayWidth}s`; fillField(ctx.fmtlist, ctx.cfg.fmtlistEntryLen * i, ctx.cfg.fmtlistEntryLen, format); } } return writeChunk(writer, ctx, "", ctx.fmtlist, ""); } function emitLbllist(writer: Writer, ctx: DtaWriteCtx): ReadStatError { for (let i = 0; i < ctx.nvar; i++) { const v = writer.variables[i]; if (v.labelSet) { fillField(ctx.lbllist, ctx.cfg.lbllistEntryLen * i, ctx.cfg.lbllistEntryLen, v.labelSet.name); } } return writeChunk(writer, ctx, "", ctx.lbllist, ""); } function emitVariableLabels(writer: Writer, ctx: DtaWriteCtx): ReadStatError { for (let i = 0; i < ctx.nvar; i++) { fillField(ctx.variableLabels, ctx.cfg.variableLabelsEntryLen * i, ctx.cfg.variableLabelsEntryLen, writer.variables[i].label); } return writeChunk(writer, ctx, "", ctx.variableLabels, ""); } function emitCharacteristics(writer: Writer, ctx: DtaWriteCtx): ReadStatError { const cfg = ctx.cfg; if (cfg.expansionLenLen === 0) return ReadStatError.OK; let e = writeTag(writer, ctx, ""); if (e !== ReadStatError.OK) return e; for (let i = 0; i < writer.notes.length; i++) { if (cfg.fileIsXmlish) e = writeTag(writer, ctx, ""); else e = writer.writeBytes(new Uint8Array([1])); if (e !== ReadStatError.OK) return e; const noteBytes = enc(writer.notes[i]); const len = noteBytes.length; const total = 2 * cfg.chMetadataLen + len + 1; if (cfg.expansionLenLen === 2) { const b = new BinaryWriter(true, 2); b.i16(total); e = writer.writeBytes(b.finish()); } else { const b = new BinaryWriter(true, 4); b.i32(total); e = writer.writeBytes(b.finish()); } if (e !== ReadStatError.OK) return e; const meta1 = new Uint8Array(cfg.chMetadataLen); fillField(meta1, 0, cfg.chMetadataLen, "_dta"); e = writer.writeBytes(meta1); if (e !== ReadStatError.OK) return e; const meta2 = new Uint8Array(cfg.chMetadataLen); fillField(meta2, 0, cfg.chMetadataLen, `note${i + 1}`); e = writer.writeBytes(meta2); if (e !== ReadStatError.OK) return e; const noteBuf = new Uint8Array(len + 1); noteBuf.set(noteBytes); e = writer.writeBytes(noteBuf); if (e !== ReadStatError.OK) return e; e = writeTag(writer, ctx, ""); if (e !== ReadStatError.OK) return e; } if (cfg.fileIsXmlish) return writeTag(writer, ctx, ""); return writer.writeZeros(1 + cfg.expansionLenLen); } function emitStrls(writer: Writer, ctx: DtaWriteCtx): ReadStatError { if (!ctx.cfg.fileIsXmlish) return ReadStatError.OK; let e = writer.writeBytes(latin1Bytes("")); if (e !== ReadStatError.OK) return e; for (const ref of writer.stringRefs) { e = writer.writeBytes(latin1Bytes("GSO")); if (e !== ReadStatError.OK) return e; const b = new BinaryWriter(true, 17); if (ctx.cfg.strlOLen > 4) { b.u32(ref.firstV).u64(BigInt(ref.firstO)).u8(DTA_GSO_TYPE_ASCII).i32(ref.len); } else { b.u32(ref.firstV).u32(ref.firstO).u8(DTA_GSO_TYPE_ASCII).i32(ref.len); } e = writer.writeBytes(b.finish()); if (e !== ReadStatError.OK) return e; e = writer.writeBytes(ref.data); if (e !== ReadStatError.OK) return e; } return writer.writeBytes(latin1Bytes("")); } function compareValueLabels(a: ValueLabel, b: ValueLabel): number { if (a.tag) { if (b.tag) return a.tag.charCodeAt(0) - b.tag.charCodeAt(0); return 1; } if (b.tag) return -1; return a.int32Key - b.int32Key; } function oldEmitValueLabels(writer: Writer, ctx: DtaWriteCtx): ReadStatError { const cfg = ctx.cfg; for (const ls of writer.labelSets) { let maxValue = 0; for (const vl of ls.valueLabels) { if (vl.tag) return ReadStatError.ERROR_TAGGED_VALUES_NOT_SUPPORTED; if (vl.int32Key < 0 || vl.int32Key > 1024) return ReadStatError.ERROR_NUMERIC_VALUE_IS_OUT_OF_RANGE; if (vl.int32Key > maxValue) maxValue = vl.int32Key; } const tableLen = 8 * (maxValue + 1); const hdr = new BinaryWriter(true, 2); hdr.i16(tableLen); let e = writer.writeBytes(hdr.finish()); if (e !== ReadStatError.OK) return e; const labname = new Uint8Array(cfg.valueLabelTableLabnameLen + cfg.valueLabelTablePaddingLen); fillField(labname, 0, cfg.valueLabelTableLabnameLen, ls.name); e = writer.writeBytes(labname); if (e !== ReadStatError.OK) return e; const labelBuffer = new Uint8Array(tableLen); for (const vl of ls.valueLabels) { const lb = enc(vl.label); labelBuffer.set(lb.subarray(0, Math.min(lb.length, 8)), 8 * vl.int32Key); } e = writer.writeBytes(labelBuffer); if (e !== ReadStatError.OK) return e; } return ReadStatError.OK; } function emitValueLabels(writer: Writer, ctx: DtaWriteCtx): ReadStatError { const cfg = ctx.cfg; if (cfg.valueLabelTableLenLen === 2) return oldEmitValueLabels(writer, ctx); let e = writeTag(writer, ctx, ""); if (e !== ReadStatError.OK) return e; for (const ls of writer.labelSets) { const n = ls.valueLabels.length; let txtlen = 0; const labelBytesList = ls.valueLabels.map((vl) => enc(vl.label)); for (const lb of labelBytesList) txtlen += lb.length + 1; e = writeTag(writer, ctx, ""); if (e !== ReadStatError.OK) return e; const tableLen = 8 + 8 * n + txtlen; const tlBuf = new BinaryWriter(true, 4); tlBuf.i32(tableLen); e = writer.writeBytes(tlBuf.finish()); if (e !== ReadStatError.OK) return e; const labname = new Uint8Array(cfg.valueLabelTableLabnameLen + cfg.valueLabelTablePaddingLen); fillField(labname, 0, cfg.valueLabelTableLabnameLen, ls.name); e = writer.writeBytes(labname); if (e !== ReadStatError.OK) return e; if (txtlen === 0) { const z = new BinaryWriter(true, 8); z.i32(0).i32(0); e = writer.writeBytes(z.finish()); if (e !== ReadStatError.OK) return e; e = writeTag(writer, ctx, ""); if (e !== ReadStatError.OK) return e; continue; } // sort labels (tag-aware) const order = ls.valueLabels.map((vl, idx) => ({ vl, lb: labelBytesList[idx] })); order.sort((x, y) => compareValueLabels(x.vl, y.vl)); const off = new Int32Array(n); const val = new Int32Array(n); const txt = new Uint8Array(txtlen); let offset = 0; for (let j = 0; j < n; j++) { const { vl, lb } = order[j]; off[j] = offset; if (vl.tag) { if (writer.version < 113) return ReadStatError.ERROR_TAGGED_VALUES_NOT_SUPPORTED; val[j] = DTA_113_MISSING_INT32_A + (vl.tag.charCodeAt(0) - 0x61); } else { val[j] = vl.int32Key; } txt.set(lb, offset); offset += lb.length; txt[offset++] = 0; } const body = new BinaryWriter(true, 8 + 8 * n + txtlen); body.i32(n).i32(txtlen); for (let j = 0; j < n; j++) body.i32(off[j]); for (let j = 0; j < n; j++) body.i32(val[j]); body.bytes(txt); e = writer.writeBytes(body.finish()); if (e !== ReadStatError.OK) return e; e = writeTag(writer, ctx, ""); if (e !== ReadStatError.OK) return e; } return writeTag(writer, ctx, ""); } // ---- map (needs sizes precomputed) ---- function measureTag(ctx: DtaWriteCtx, tag: string): number { return tagLen(ctx, tag); } function measureMap(ctx: DtaWriteCtx): number { return measureTag(ctx, "") + 14 * 8 + measureTag(ctx, ""); } function measureTyplist(ctx: DtaWriteCtx): number { return measureTag(ctx, "") + ctx.cfg.typlistEntryLen * ctx.nvar + measureTag(ctx, ""); } function measureVarlist(ctx: DtaWriteCtx): number { return measureTag(ctx, "") + ctx.varlistLen + measureTag(ctx, ""); } function measureSrtlist(ctx: DtaWriteCtx): number { return measureTag(ctx, "") + ctx.cfg.srtlistLen + measureTag(ctx, ""); } function measureFmtlist(ctx: DtaWriteCtx): number { return measureTag(ctx, "") + ctx.fmtlistLen + measureTag(ctx, ""); } function measureLbllist(ctx: DtaWriteCtx): number { return measureTag(ctx, "") + ctx.lbllistLen + measureTag(ctx, ""); } function measureVariableLabels(ctx: DtaWriteCtx): number { return measureTag(ctx, "") + ctx.variableLabelsLen + measureTag(ctx, ""); } function measureCharacteristics(writer: Writer, ctx: DtaWriteCtx): number { let chLen = 0; for (const note of writer.notes) { chLen += measureTag(ctx, "") + ctx.cfg.expansionLenLen + 2 * ctx.cfg.chMetadataLen + enc(note).length + 1 + measureTag(ctx, ""); } return measureTag(ctx, "") + chLen + measureTag(ctx, ""); } function measureData(writer: Writer, ctx: DtaWriteCtx): number { ctx.recordLen = 0; for (let i = 0; i < ctx.nvar; i++) { const tc = typecodeForVariable(writer.variables[i], ctx); const info = dtaTypeInfo(tc.code, ctx.cfg); ctx.recordLen += info.maxLen; } return measureTag(ctx, "") + ctx.recordLen * ctx.nobs + measureTag(ctx, ""); } function measureStrls(writer: Writer, ctx: DtaWriteCtx): number { let len = 0; for (const ref of writer.stringRefs) { len += 3 + (ctx.cfg.strlOLen > 4 ? 17 : 13) + ref.len; } return measureTag(ctx, "") + len + measureTag(ctx, ""); } function measureValueLabels(writer: Writer, ctx: DtaWriteCtx): number { let len = measureTag(ctx, ""); for (const ls of writer.labelSets) { const n = ls.valueLabels.length; let txtlen = 0; for (const vl of ls.valueLabels) txtlen += enc(vl.label).length + 1; len += measureTag(ctx, "") + 4 + ctx.cfg.valueLabelTableLabnameLen + ctx.cfg.valueLabelTablePaddingLen + 8 + 8 * n + txtlen + measureTag(ctx, ""); } return len + measureTag(ctx, ""); } function emitMap(writer: Writer, ctx: DtaWriteCtx): ReadStatError { if (!ctx.cfg.fileIsXmlish) return ReadStatError.OK; const map = new Array(14); map[0] = 0; map[1] = writer.bytesWritten; map[2] = map[1] + measureMap(ctx); map[3] = map[2] + measureTyplist(ctx); map[4] = map[3] + measureVarlist(ctx); map[5] = map[4] + measureSrtlist(ctx); map[6] = map[5] + measureFmtlist(ctx); map[7] = map[6] + measureLbllist(ctx); map[8] = map[7] + measureVariableLabels(ctx); map[9] = map[8] + measureCharacteristics(writer, ctx); map[10] = map[9] + measureData(writer, ctx); map[11] = map[10] + measureStrls(writer, ctx); map[12] = map[11] + measureValueLabels(writer, ctx); map[13] = map[12] + measureTag(ctx, ""); const b = new BinaryWriter(true, 14 * 8); for (let i = 0; i < 14; i++) b.u64(BigInt(map[i])); return writeChunk(writer, ctx, "", b.finish(), ""); } // ---- header + descriptors ---- function emitXmlishHeader(writer: Writer, ctx: DtaWriteCtx): ReadStatError { let e = writeTag(writer, ctx, ""); if (e !== ReadStatError.OK) return e; e = writeTag(writer, ctx, "
"); if (e !== ReadStatError.OK) return e; e = writer.writeBytes(latin1Bytes(`${writer.version}`)); if (e !== ReadStatError.OK) return e; e = writeChunk(writer, ctx, "", latin1Bytes("LSF"), ""); if (e !== ReadStatError.OK) return e; if (writer.version >= 119) { const b = new BinaryWriter(true, 4); b.u32(writer.variables.length); e = writeChunk(writer, ctx, "", b.finish(), ""); } else { const b = new BinaryWriter(true, 2); b.u16(writer.variables.length); e = writeChunk(writer, ctx, "", b.finish(), ""); } if (e !== ReadStatError.OK) return e; if (writer.version >= 118) { const b = new BinaryWriter(true, 8); b.u64(BigInt(writer.rowCount)); e = writeChunk(writer, ctx, "", b.finish(), ""); } else { const b = new BinaryWriter(true, 4); b.u32(writer.rowCount); e = writeChunk(writer, ctx, "", b.finish(), ""); } if (e !== ReadStatError.OK) return e; e = emitHeaderDataLabel(writer, ctx); if (e !== ReadStatError.OK) return e; e = emitHeaderTimestamp(writer, ctx); if (e !== ReadStatError.OK) return e; return writeTag(writer, ctx, "
"); } function emitHeader(writer: Writer, ctx: DtaWriteCtx): ReadStatError { if (ctx.cfg.fileIsXmlish) return emitXmlishHeader(writer, ctx); if (writer.variables.length > 32767) return ReadStatError.ERROR_TOO_MANY_COLUMNS; const b = new BinaryWriter(true, 8); b.u8(writer.version).u8(DTA_LOHI).u8(0x01).u8(0x00).u16(writer.variables.length).u32(writer.rowCount); let e = writer.writeBytes(b.finish()); if (e !== ReadStatError.OK) return e; e = emitHeaderDataLabel(writer, ctx); if (e !== ReadStatError.OK) return e; emitHeaderTimestamp(writer, ctx); return ReadStatError.OK; // C returns OK unconditionally here } function emitDescriptors(writer: Writer, ctx: DtaWriteCtx): ReadStatError { let e = emitTyplist(writer, ctx); if (e !== ReadStatError.OK) return e; e = emitVarlist(writer, ctx); if (e !== ReadStatError.OK) return e; e = emitSrtlist(writer, ctx); if (e !== ReadStatError.OK) return e; e = emitFmtlist(writer, ctx); if (e !== ReadStatError.OK) return e; return emitLbllist(writer, ctx); } // ---- value writers ---- function w8(writer: Writer, offset: number, value: number): void { new DataView(writer.row.buffer, writer.row.byteOffset).setInt8(offset, value); } function w16(writer: Writer, offset: number, value: number): void { new DataView(writer.row.buffer, writer.row.byteOffset).setInt16(offset, value, true); } function w32(writer: Writer, offset: number, value: number): void { new DataView(writer.row.buffer, writer.row.byteOffset).setInt32(offset, value | 0, true); } function w64(writer: Writer, offset: number, value: bigint): void { new DataView(writer.row.buffer, writer.row.byteOffset).setBigInt64(offset, value, true); } function wf32(writer: Writer, offset: number, value: number): void { new DataView(writer.row.buffer, writer.row.byteOffset).setFloat32(offset, value, true); } function wf64(writer: Writer, offset: number, value: number): void { new DataView(writer.row.buffer, writer.row.byteOffset).setFloat64(offset, value, true); } function writeMissingNumeric113(writer: Writer, offset: number, v: Variable): ReadStatError { switch (v.type) { case ReadStatType.INT8: w8(writer, offset, DTA_113_MISSING_INT8); break; case ReadStatType.INT16: w16(writer, offset, DTA_113_MISSING_INT16); break; case ReadStatType.INT32: w32(writer, offset, DTA_113_MISSING_INT32); break; case ReadStatType.FLOAT: w32(writer, offset, DTA_113_MISSING_FLOAT); break; case ReadStatType.DOUBLE: w64(writer, offset, DTA_113_MISSING_DOUBLE); break; } return ReadStatError.OK; } function writeMissingNumericOld(writer: Writer, offset: number, v: Variable): ReadStatError { switch (v.type) { case ReadStatType.INT8: w8(writer, offset, DTA_OLD_MISSING_INT8); break; case ReadStatType.INT16: w16(writer, offset, DTA_OLD_MISSING_INT16); break; case ReadStatType.INT32: w32(writer, offset, DTA_OLD_MISSING_INT32); break; case ReadStatType.FLOAT: w32(writer, offset, DTA_OLD_MISSING_FLOAT); break; case ReadStatType.DOUBLE: w64(writer, offset, DTA_OLD_MISSING_DOUBLE); break; } return ReadStatError.OK; } const DTA_113_MAX_FLOAT_F = new DataView((() => { const b = new ArrayBuffer(4); new DataView(b).setInt32(0, DTA_113_MAX_FLOAT, true); return b; })()).getFloat32(0, true); const DTA_113_MAX_DOUBLE_D = new DataView((() => { const b = new ArrayBuffer(8); new DataView(b).setBigInt64(0, DTA_113_MAX_DOUBLE, true); return b; })()).getFloat64(0, true); function writeString(writer: Writer, offset: number, v: Variable, value: string): ReadStatError { const maxLen = v.storageWidth; writer.row.fill(0, offset, offset + maxLen); if (value && value.length > 0) { const b = enc(value); if (b.length > maxLen) return ReadStatError.ERROR_STRING_VALUE_IS_TOO_LONG; writer.row.set(b.subarray(0, Math.min(b.length, maxLen)), offset); } return ReadStatError.OK; } function writeStringRef118(writer: Writer, offset: number, ref: StringRef): ReadStatError { if (!ref) return ReadStatError.ERROR_STRING_REF_IS_REQUIRED; w16(writer, offset, ref.firstV); // write low 6 bytes of firstO, little-endian const dv = new DataView(new ArrayBuffer(8)); dv.setBigInt64(0, BigInt(ref.firstO), true); for (let k = 0; k < 6; k++) writer.row[offset + 2 + k] = dv.getUint8(k); return ReadStatError.OK; } function writeStringRef117(writer: Writer, offset: number, ref: StringRef): ReadStatError { if (!ref) return ReadStatError.ERROR_STRING_REF_IS_REQUIRED; w32(writer, offset, ref.firstV); w32(writer, offset + 4, ref.firstO); return ReadStatError.OK; } function writeMissingTagged113(writer: Writer, offset: number, v: Variable, tag: string): ReadStatError { const t = tag.charCodeAt(0); if (t < 0x61 || t > 0x7a) return ReadStatError.ERROR_TAGGED_VALUE_IS_OUT_OF_RANGE; const d = t - 0x61; switch (v.type) { case ReadStatType.INT8: w8(writer, offset, DTA_113_MISSING_INT8_A + d); break; case ReadStatType.INT16: w16(writer, offset, DTA_113_MISSING_INT16_A + d); break; case ReadStatType.INT32: w32(writer, offset, DTA_113_MISSING_INT32_A + d); break; case ReadStatType.FLOAT: w32(writer, offset, DTA_113_MISSING_FLOAT_A + (d << 11)); break; case ReadStatType.DOUBLE: w64(writer, offset, DTA_113_MISSING_DOUBLE_A + (BigInt(d) << 40n)); break; default: return ReadStatError.ERROR_TAGGED_VALUES_NOT_SUPPORTED; } return ReadStatError.OK; } // ---- lifecycle ---- function dtaBeginData(writer: Writer): ReadStatError { if (!writer.initialized) return ReadStatError.ERROR_WRITER_NOT_INITIALIZED; let ctx: DtaWriteCtx; try { ctx = new DtaWriteCtx(writer.version, writer.variables.length, writer.rowCount); } catch { return ReadStatError.ERROR_UNSUPPORTED_FILE_FORMAT_VERSION; } writer.moduleCtx = ctx; let e = emitHeader(writer, ctx); if (e !== ReadStatError.OK) return e; e = emitMap(writer, ctx); if (e !== ReadStatError.OK) return e; e = emitDescriptors(writer, ctx); if (e !== ReadStatError.OK) return e; e = emitVariableLabels(writer, ctx); if (e !== ReadStatError.OK) return e; e = emitCharacteristics(writer, ctx); if (e !== ReadStatError.OK) return e; return writeTag(writer, ctx, ""); } function dtaEndData(writer: Writer): ReadStatError { if (!writer.initialized) return ReadStatError.ERROR_WRITER_NOT_INITIALIZED; const ctx = writer.moduleCtx as DtaWriteCtx; let e = writeTag(writer, ctx, ""); if (e !== ReadStatError.OK) return e; e = emitStrls(writer, ctx); if (e !== ReadStatError.OK) return e; e = emitValueLabels(writer, ctx); if (e !== ReadStatError.OK) return e; return writeTag(writer, ctx, "
"); } function metadataOk(writer: Writer): ReadStatError { if (writer.compression !== ReadStatCompress.NONE) return ReadStatError.ERROR_UNSUPPORTED_COMPRESSION; if (writer.version > 119 || writer.version < 104) return ReadStatError.ERROR_UNSUPPORTED_FILE_FORMAT_VERSION; return ReadStatError.OK; } export function beginWritingDta(writer: Writer, userCtx: unknown, rowCount: number): ReadStatError { if (writer.version === 0) writer.version = DTA_FILE_VERSION_DEFAULT; const version = writer.version; writer.callbacks.metadataOk = metadataOk; writer.callbacks.variableWidth = (type, userWidth) => variableWidth(version, type, userWidth); if (version >= 118) writer.callbacks.variableOk = (v) => validateName(v.name, true, DTA_118_MAX_NAME_LEN); else if (version >= 110) writer.callbacks.variableOk = (v) => validateName(v.name, false, DTA_110_MAX_NAME_LEN); else writer.callbacks.variableOk = (v) => validateName(v.name, false, DTA_OLD_MAX_NAME_LEN); if (version >= 118) writer.callbacks.writeStringRef = (w, off, _v, ref) => writeStringRef118(w, off, ref); else if (version === 117) writer.callbacks.writeStringRef = (w, off, _v, ref) => writeStringRef117(w, off, ref); if (version >= 113) { writer.callbacks.writeInt8 = (w, off, _v, value) => value > DTA_113_MAX_INT8 ? ReadStatError.ERROR_NUMERIC_VALUE_IS_OUT_OF_RANGE : (w8(w, off, value), ReadStatError.OK); writer.callbacks.writeInt16 = (w, off, _v, value) => value > DTA_113_MAX_INT16 ? ReadStatError.ERROR_NUMERIC_VALUE_IS_OUT_OF_RANGE : (w16(w, off, value), ReadStatError.OK); writer.callbacks.writeInt32 = (w, off, _v, value) => value > DTA_113_MAX_INT32 ? ReadStatError.ERROR_NUMERIC_VALUE_IS_OUT_OF_RANGE : (w32(w, off, value), ReadStatError.OK); writer.callbacks.writeMissingNumber = (w, off, v) => writeMissingNumeric113(w, off, v); writer.callbacks.writeMissingTagged = (w, off, v, tag) => writeMissingTagged113(w, off, v, tag); } else { writer.callbacks.writeInt8 = (w, off, _v, value) => value > DTA_OLD_MAX_INT8 ? ReadStatError.ERROR_NUMERIC_VALUE_IS_OUT_OF_RANGE : (w8(w, off, value), ReadStatError.OK); writer.callbacks.writeInt16 = (w, off, _v, value) => value > DTA_OLD_MAX_INT16 ? ReadStatError.ERROR_NUMERIC_VALUE_IS_OUT_OF_RANGE : (w16(w, off, value), ReadStatError.OK); writer.callbacks.writeInt32 = (w, off, _v, value) => value > DTA_OLD_MAX_INT32 ? ReadStatError.ERROR_NUMERIC_VALUE_IS_OUT_OF_RANGE : (w32(w, off, value), ReadStatError.OK); writer.callbacks.writeMissingNumber = (w, off, v) => writeMissingNumericOld(w, off, v); } writer.callbacks.writeFloat = (w, off, v, value) => { if (value > DTA_113_MAX_FLOAT_F) return ReadStatError.ERROR_NUMERIC_VALUE_IS_OUT_OF_RANGE; if (Number.isNaN(value)) return writeMissingNumeric113(w, off, v); wf32(w, off, value); return ReadStatError.OK; }; writer.callbacks.writeDouble = (w, off, v, value) => { if (value > DTA_113_MAX_DOUBLE_D) return ReadStatError.ERROR_NUMERIC_VALUE_IS_OUT_OF_RANGE; if (Number.isNaN(value)) return writeMissingNumeric113(w, off, v); wf64(w, off, value); return ReadStatError.OK; }; writer.callbacks.writeString = (w, off, v, value) => writeString(w, off, v, value); writer.callbacks.writeMissingString = (w, off, v) => writeString(w, off, v, ""); writer.callbacks.beginData = dtaBeginData; writer.callbacks.endData = dtaEndData; return writer.beginWritingFile(userCtx, rowCount); }