// // DTAFormat.swift // Metrika // // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // Copyright © 2026 Simon-Pierre Boucher. All rights reserved. // import Foundation /// Shared constants and byte-cursor plumbing for the native Stata .dta /// reader/writer (formats 117–119, CLAUDE.md §6). enum DTAFormat { /// Variable type codes (dta 117+). static let typeStrL: UInt16 = 32768 static let typeDouble: UInt16 = 65526 static let typeFloat: UInt16 = 65527 static let typeLong: UInt16 = 65528 static let typeInt: UInt16 = 65529 static let typeByte: UInt16 = 65530 static let maxFixedStringLength = 2045 /// Smallest value treated as missing, per numeric storage type. /// Stata reserves the top of each range for `.` and `.a`–`.z`; the /// distinctions collapse into one missing state here. static let doubleMissingThreshold = 8.988465674311579e307 static let floatMissingThreshold: Float = 1.701e38 static let longMissingThreshold: Int32 = 2_147_483_621 static let intMissingThreshold: Int16 = 32741 static let byteMissingThreshold: Int8 = 101 /// Canonical `.` (system missing) for doubles: +2¹⁰²³. static let doubleMissingValue = Double(bitPattern: 0x7FE0_0000_0000_0000) /// Per-variable field widths that differ across format versions. struct Layout { let release: Int let variableCountBytes: Int // K let observationCountBytes: Int// N let nameBytes: Int let formatBytes: Int let labelNameBytes: Int let variableLabelBytes: Int let sortEntryBytes: Int let datasetLabelLengthBytes: Int let strLVarBytes: Int // v in a data-section strL cell let strLObsBytes: Int // o in a data-section strL cell let gsoObsBytes: Int // o in a GSO record init?(release: Int) { self.release = release switch release { case 117: variableCountBytes = 2 observationCountBytes = 4 nameBytes = 33 formatBytes = 49 labelNameBytes = 33 variableLabelBytes = 81 sortEntryBytes = 2 datasetLabelLengthBytes = 1 strLVarBytes = 4 strLObsBytes = 4 gsoObsBytes = 4 case 118: variableCountBytes = 2 observationCountBytes = 8 nameBytes = 129 formatBytes = 57 labelNameBytes = 129 variableLabelBytes = 321 sortEntryBytes = 2 datasetLabelLengthBytes = 2 strLVarBytes = 2 strLObsBytes = 6 gsoObsBytes = 8 case 119: variableCountBytes = 4 observationCountBytes = 8 nameBytes = 129 formatBytes = 57 labelNameBytes = 129 variableLabelBytes = 321 sortEntryBytes = 4 datasetLabelLengthBytes = 2 strLVarBytes = 4 strLObsBytes = 6 gsoObsBytes = 8 default: return nil } } } } /// Sequential binary cursor with endian-aware integer reads. struct DTACursor { let data: Data var offset: Int = 0 var bigEndian = false init(data: Data) { self.data = data } var remaining: Int { data.count - offset } mutating func readBytes(_ count: Int) throws(ZQDataError) -> Data { guard count >= 0, offset + count <= data.count else { throw ZQDataError("corrupt .dta: unexpected end of file at offset \(offset)") } let slice = data.subdata(in: offset..<(offset + count)) offset += count return slice } /// Consumes an exact ASCII tag or fails with its name. mutating func expect(_ tag: String) throws(ZQDataError) { let expected = Data(tag.utf8) let actual = try readBytes(expected.count) guard actual == expected else { throw ZQDataError( "corrupt .dta: expected '\(tag)' at offset \(offset - expected.count)" ) } } mutating func readUInt(_ byteCount: Int) throws(ZQDataError) -> UInt64 { let bytes = try readBytes(byteCount) var value: UInt64 = 0 if bigEndian { for byte in bytes { value = value << 8 | UInt64(byte) } } else { for byte in bytes.reversed() { value = value << 8 | UInt64(byte) } } return value } mutating func readDouble() throws(ZQDataError) -> Double { Double(bitPattern: try readUInt(8)) } mutating func readFloat() throws(ZQDataError) -> Float { Float(bitPattern: UInt32(truncatingIfNeeded: try readUInt(4))) } /// Fixed-width, null-padded text field. mutating func readPaddedString( _ width: Int, release: Int ) throws(ZQDataError) -> String { let bytes = try readBytes(width) let trimmed = bytes.prefix { $0 != 0 } if release >= 118 { return String(data: trimmed, encoding: .utf8) ?? "" } return String(data: trimmed, encoding: .isoLatin1) ?? "" } }