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%
4.1 KB · 113 lines swift
Raw Blame History
1//2//  DTATests.swift3//  Metrika4//5//  Author:  Simon-Pierre Boucher6//  Contact: contact@spboucher.ai7//  Copyright © 2026 Simon-Pierre Boucher. All rights reserved.8//910import Foundation11import Testing12import ZQData1314/// Native .dta reader/writer tests: haven-written fixtures (formats 11715/// and 118) must load identically to the CSV, and a write/read roundtrip16/// must be lossless. All loads go through the public ZQDataStore path.17@Suite("Stata .dta reader/writer")18struct DTATests {19    let store: ZQDataStore20    let csvFrame: ZQDataFrame21    let fixturesDirectory: URL2223    init() async throws {24        let fixtures = try Fixtures()25        self.store = try ZQDataStore()26        self.csvFrame = try await store.load(contentsOf: fixtures.datasetURL)27        self.fixturesDirectory = fixtures.datasetURL.deletingLastPathComponent()28    }2930    private func expectMatchesCSV(_ frame: ZQDataFrame) throws {31        #expect(frame.rowCount == 60)32        #expect(frame.columnNames == [33            "revenue", "price", "region", "firm_id", "purchase", "orders",34            "z1", "z2", "firm_name",35        ])3637        // Numeric columns: bit-identical to the CSV load (both sides parse38        // the same decimal text into the nearest double).39        for name in csvFrame.columnNames {40            let expected = try csvFrame.requireNumeric(name)41            let actual = try frame.requireNumeric(name)42            #expect(actual.missing == expected.missing, "missing mask of \(name)")43            for i in 0..<60 where !expected.missing[i] {44                #expect(actual.values[i] == expected.values[i], "\(name)[\(i)]")45            }46        }4748        // String column: haven wrote firm_<id>, with row 5 blank (missing).49        guard case .string(let names)? = frame.column(named: "firm_name")?.data else {50            Issue.record("firm_name is not a string column")51            return52        }53        #expect(names[0] == "firm_1")54        #expect(names[4] == nil)     // "" is Stata's string missing55        #expect(names[59] == "firm_12")56    }5758    @Test("reads haven-written format 118")59    func readsFormat118() async throws {60        let url = fixturesDirectory.appendingPathComponent("regression_v118.dta")61        try expectMatchesCSV(try await store.load(contentsOf: url))62    }6364    @Test("reads haven-written format 117")65    func readsFormat117() async throws {66        let url = fixturesDirectory.appendingPathComponent("regression_v117.dta")67        try expectMatchesCSV(try await store.load(contentsOf: url))68    }6970    @Test("write/read roundtrip is lossless")71    func roundtrip() async throws {72        let url = fixturesDirectory.appendingPathComponent("regression_v118.dta")73        let original = try await store.load(contentsOf: url)7475        let out = FileManager.default.temporaryDirectory76            .appendingPathComponent("metrika_roundtrip_\(UUID().uuidString).dta")77        defer { try? FileManager.default.removeItem(at: out) }78        try await store.save(original, to: out)79        let reloaded = try await store.load(contentsOf: out)8081        #expect(reloaded == original)82    }8384    @Test("rejects pre-117 files with a clear message")85    func rejectsOldFormats() async throws {86        // Format 115 files start with the raw version byte 0x73.87        let url = try temporaryFile(Data([0x73, 0x02, 0x01, 0x00]))88        defer { try? FileManager.default.removeItem(at: url) }89        do {90            _ = try await store.load(contentsOf: url)91            Issue.record("expected a format error")92        } catch {93            #expect("\(error)".contains("older than 117"))94        }95    }9697    @Test("rejects non-dta bytes")98    func rejectsGarbage() async throws {99        let url = try temporaryFile(Data("not a dta file at all".utf8))100        defer { try? FileManager.default.removeItem(at: url) }101        await #expect(throws: (any Error).self) {102            _ = try await store.load(contentsOf: url)103        }104    }105106    private func temporaryFile(_ data: Data) throws -> URL {107        let url = FileManager.default.temporaryDirectory108            .appendingPathComponent("metrika_dta_\(UUID().uuidString).dta")109        try data.write(to: url)110        return url111    }112}113