SPB Git

spb/metrika Public

Stata-class statistics, GPU-accelerated by Apple Silicon. Native Swift — no Electron, no Python runtime, no compromises.

Swift 92.4% HTML 3.3% R 3% Shell 1.3%
5.2 KB · 158 lines swift
Raw Blame History
1//2//  DTAFormat.swift3//  Metrika4//5//  Author:  Simon-Pierre Boucher6//  Contact: contact@spboucher.ai7//  Copyright © 2026 Simon-Pierre Boucher. All rights reserved.8//910import Foundation1112/// Shared constants and byte-cursor plumbing for the native Stata .dta13/// reader/writer (formats 117–119, CLAUDE.md §6).14enum DTAFormat {15    /// Variable type codes (dta 117+).16    static let typeStrL: UInt16 = 3276817    static let typeDouble: UInt16 = 6552618    static let typeFloat: UInt16 = 6552719    static let typeLong: UInt16 = 6552820    static let typeInt: UInt16 = 6552921    static let typeByte: UInt16 = 6553022    static let maxFixedStringLength = 20452324    /// Smallest value treated as missing, per numeric storage type.25    /// Stata reserves the top of each range for `.` and `.a`–`.z`; the26    /// distinctions collapse into one missing state here.27    static let doubleMissingThreshold = 8.988465674311579e30728    static let floatMissingThreshold: Float = 1.701e3829    static let longMissingThreshold: Int32 = 2_147_483_62130    static let intMissingThreshold: Int16 = 3274131    static let byteMissingThreshold: Int8 = 1013233    /// Canonical `.` (system missing) for doubles: +2¹⁰²³.34    static let doubleMissingValue = Double(bitPattern: 0x7FE0_0000_0000_0000)3536    /// Per-variable field widths that differ across format versions.37    struct Layout {38        let release: Int39        let variableCountBytes: Int   // K40        let observationCountBytes: Int// N41        let nameBytes: Int42        let formatBytes: Int43        let labelNameBytes: Int44        let variableLabelBytes: Int45        let sortEntryBytes: Int46        let datasetLabelLengthBytes: Int47        let strLVarBytes: Int         // v in a data-section strL cell48        let strLObsBytes: Int         // o in a data-section strL cell49        let gsoObsBytes: Int          // o in a GSO record5051        init?(release: Int) {52            self.release = release53            switch release {54            case 117:55                variableCountBytes = 256                observationCountBytes = 457                nameBytes = 3358                formatBytes = 4959                labelNameBytes = 3360                variableLabelBytes = 8161                sortEntryBytes = 262                datasetLabelLengthBytes = 163                strLVarBytes = 464                strLObsBytes = 465                gsoObsBytes = 466            case 118:67                variableCountBytes = 268                observationCountBytes = 869                nameBytes = 12970                formatBytes = 5771                labelNameBytes = 12972                variableLabelBytes = 32173                sortEntryBytes = 274                datasetLabelLengthBytes = 275                strLVarBytes = 276                strLObsBytes = 677                gsoObsBytes = 878            case 119:79                variableCountBytes = 480                observationCountBytes = 881                nameBytes = 12982                formatBytes = 5783                labelNameBytes = 12984                variableLabelBytes = 32185                sortEntryBytes = 486                datasetLabelLengthBytes = 287                strLVarBytes = 488                strLObsBytes = 689                gsoObsBytes = 890            default:91                return nil92            }93        }94    }95}9697/// Sequential binary cursor with endian-aware integer reads.98struct DTACursor {99    let data: Data100    var offset: Int = 0101    var bigEndian = false102103    init(data: Data) { self.data = data }104105    var remaining: Int { data.count - offset }106107    mutating func readBytes(_ count: Int) throws(ZQDataError) -> Data {108        guard count >= 0, offset + count <= data.count else {109            throw ZQDataError("corrupt .dta: unexpected end of file at offset \(offset)")110        }111        let slice = data.subdata(in: offset..<(offset + count))112        offset += count113        return slice114    }115116    /// Consumes an exact ASCII tag or fails with its name.117    mutating func expect(_ tag: String) throws(ZQDataError) {118        let expected = Data(tag.utf8)119        let actual = try readBytes(expected.count)120        guard actual == expected else {121            throw ZQDataError(122                "corrupt .dta: expected '\(tag)' at offset \(offset - expected.count)"123            )124        }125    }126127    mutating func readUInt(_ byteCount: Int) throws(ZQDataError) -> UInt64 {128        let bytes = try readBytes(byteCount)129        var value: UInt64 = 0130        if bigEndian {131            for byte in bytes { value = value << 8 | UInt64(byte) }132        } else {133            for byte in bytes.reversed() { value = value << 8 | UInt64(byte) }134        }135        return value136    }137138    mutating func readDouble() throws(ZQDataError) -> Double {139        Double(bitPattern: try readUInt(8))140    }141142    mutating func readFloat() throws(ZQDataError) -> Float {143        Float(bitPattern: UInt32(truncatingIfNeeded: try readUInt(4)))144    }145146    /// Fixed-width, null-padded text field.147    mutating func readPaddedString(148        _ width: Int, release: Int149    ) throws(ZQDataError) -> String {150        let bytes = try readBytes(width)151        let trimmed = bytes.prefix { $0 != 0 }152        if release >= 118 {153            return String(data: trimmed, encoding: .utf8) ?? ""154        }155        return String(data: trimmed, encoding: .isoLatin1) ?? ""156    }157}158