npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@irbisadm/statfmt

v0.1.1

Published

TypeScript port of ReadStat — read and write SPSS (.sav/.zsav/.por), Stata (.dta), and SAS (.sas7bdat/.sas7bcat/.xpt) data files. No native dependencies.

Readme

@irbisadm/statfmt

npm license types Conventional Commits semantic-release

Read and write statistical data files from SPSS, Stata, and SAS in pure TypeScript — no native addons, no WASM, no system libraries.

This is a TypeScript port of ReadStat (© 2013–2016 Evan Miller and ReadStat contributors, MIT licensed). It is a derivative work that faithfully re-implements ReadStat's readers, writers and binary-format logic in TypeScript. All credit for the original design and the hard reverse-engineering of these proprietary formats belongs to the ReadStat authors. See Relationship to ReadStat.

Format support

| Format | Extension | Read | Write | | --- | --- | :---: | :---: | | SPSS | .sav | ✅ | ✅ | | SPSS compressed | .zsav | ✅ | ✅ | | SPSS portable | .por | ✅ | ✅ | | Stata | .dta (v104–119) | ✅ | ✅ | | SAS database | .sas7bdat | ✅ | ✅ | | SAS catalog | .sas7bcat | ✅ | ✅ | | SAS transport | .xpt (v5/v8) | ✅ | ✅ |

Plain-text data with an external schema can also be read via readTxt(): Stata dictionary (.dct), SPSS command (.spsDATA LIST / VALUE LABELS / VARIABLE LABELS), and SAS command (.sasINPUT / INFILE / LABEL / FORMAT / VALUE) files.

See Known limitations for the (intentional) gaps inherited from ReadStat.

Install

npm install @irbisadm/statfmt

ESM-only, requires Node 18+ (uses the built-in TextDecoder for character-set decoding and node:zlib for .zsav). The non-zlib formats work in the browser too; supply a custom Codec / zlib shim for the rest.

High-level API

Reading

import { readFile } from "node:fs/promises";
import { readSav, readDta, readData, detectFormat } from "@irbisadm/statfmt";

const bytes = new Uint8Array(await readFile("survey.sav"));
const ds = readSav(bytes);

ds.metadata.rowCount;          // number of rows (-1 if unknown)
ds.variables[0].name;          // "id"
ds.variables[2].valueLabels;   // [{ value: 1, label: "Male" }, ...]
ds.rows[0];                    // [1, 3.5, 1, "Alice"]
ds.toObjects();                // [{ id: 1, score: 3.5, ... }, ...]

// auto-detect from magic bytes
const fmt = detectFormat(bytes); // "sav" | "zsav" | "dta" | ...
const ds2 = readData(fmt!, bytes);

ReadOptions let you override the encoding or window the rows:

readDta(bytes, { inputEncoding: "WINDOWS-1251", rowLimit: 100, rowOffset: 0 });

Writing

import { writeSav, writeDta, ReadStatType, ReadStatMeasure } from "@irbisadm/statfmt";

const bytes = writeSav({
  fileLabel: "My dataset",
  valueLabelSets: {
    sex: [{ value: 1, label: "Male" }, { value: 2, label: "Female" }],
  },
  variables: [
    { name: "id",    type: ReadStatType.INT32,  measure: ReadStatMeasure.SCALE },
    { name: "score", type: ReadStatType.DOUBLE, label: "Test score" },
    { name: "sex",   type: ReadStatType.INT32,  valueLabels: "sex" },
    { name: "name",  type: ReadStatType.STRING, storageWidth: 40 },
  ],
  rows: [
    [1, 3.5, 1, "Alice"],
    [2, 9.25, 2, "Bob"],
    [3, null, 1, "Carol"],   // null => missing
  ],
});

// await writeFile("out.sav", bytes);

writeZsav(spec) produces a zlib-compressed .sav; writeDta(spec) writes Stata .dta (default version 118 — pass version: 117 etc. to target older Stata).

Low-level streaming API

The high-level helpers are built on the same callback API as C ReadStat, exposed for when you want to stream millions of rows without materializing them:

import { ReadStatParser, BufferIoContext, parseSav } from "@irbisadm/statfmt";

const parser = new ReadStatParser();
parser.setMetadataHandler((md) => console.log(md.rowCount, md.varCount));
parser.setVariableHandler((index, variable, valLabels) => { /* ... */ });
parser.setValueHandler((obsIndex, variable, value) => {
  console.log(obsIndex, variable.getName(), value.toJS());
});
parseSav(parser, new BufferIoContext(bytes), null);

The Writer class exposes the incremental writer API (addVariable, beginRow, insertDoubleValue, endRow, …).

Plain-text with a schema

import { readTxt } from "@irbisadm/statfmt";

const data = new Uint8Array(await readFile("data.txt"));
const schema = new Uint8Array(await readFile("layout.dct"));
const ds = readTxt(data, schema, "stata"); // "stata" | "spss" | "sas"

Character encodings

Decoding uses the platform TextDecoder, which covers the encodings SPSS/SAS/Stata files declare (Windows-125x, ISO-8859-x, Shift-JIS, GBK, Big5, KOI8, …). For writing to a non-UTF-8 target encoding, inject a custom Codec (e.g. backed by iconv-lite).

Testing & validation approach

Correctness for binary format work is meaningless unless it is checked against the canonical implementation. This port is validated against the real ReadStat C library in both directions, for every format, so "it round-trips through itself" is never the whole story.

Layers

  1. Unit tests — per-format encoders/decoders and the shared core (RLE/RDC compression, IEEE↔IBM float conversion, POR base-30 numbers, endianness, timestamps, value/variable modelling). These run with no external dependencies:

    npm test
  2. End-to-end cross-validation against reference ReadStat. The suite builds the actual ReadStat C CLI and drives it as a golden oracle. Three complementary directions:

    | Suite | Direction | What it proves | | --- | --- | --- | | test/e2e/write.e2e.test.ts | generation — TS writes → C reads | every file we emit is valid per ReadStat: exact variable names, values (numeric & string), system-missing, column count and file label are recovered by the C CLI | | test/e2e/read.e2e.test.ts | reading — C writes → TS reads | genuine files produced by ReadStat (each format transcoded from a validated base) are parsed to the exact same values, plus value labels where the format stores them | | test/e2e/roundtrip.e2e.test.ts | structure — TS → C → TS | variable labels, value labels and user-defined missing ranges — things the CLI's CSV/metadata dump can't surface — survive a full round-trip through the C library |

    Together these cover the two things a port must guarantee: what we write is readable by the reference, and what the reference writes is readable by us.

Running the e2e layer

The reference CLI is built automatically on first vitest run (pinned to ReadStat v1.1.9) into .reference/ — this needs a C compiler, make, zlib and iconv. You can also build it explicitly or point at an existing binary:

npm run build:reference                 # download/clone + build into .reference/
npm run test:e2e                        # run only the e2e layer
READSTAT_BIN=/path/to/readstat npm test # reuse an existing ReadStat build
READSTAT_NO_AUTOBUILD=1 npm test        # skip the auto-build (e2e tests then fail fast)

The build script prefers the pinned release tarball (which ships a generated ./configure, so no autotools are needed) and falls back to git clone + autogen. If the reference cannot be built, the e2e tests fail with actionable guidance rather than silently passing.

Why go this far

The binary formats here are proprietary and reverse-engineered. A self-consistent round-trip (write then read with the same code) can hide a shared bug on both sides. Pinning every format to an independent, widely-used reference implementation is what makes "compatible with SPSS/Stata/SAS tooling" a claim backed by tests rather than hope.

Relationship to ReadStat

@irbisadm/statfmt is a port, not an original work. It re-implements ReadStat in TypeScript:

  • the streaming, callback-based parser/writer architecture mirrors ReadStat's (readstat_parser_tReadStatParser, readstat_writer_tWriter);
  • the readers, writers and binary-layout logic for every format are ported from the corresponding ReadStat C sources;
  • hand-written grammars replace the Ragel-generated C for the format/number/schema parsers.

All of the original insight — decoding SPSS/Stata/SAS binary layouts, the SAS RLE and RDC schemes, the XPORT IBM float format, the POR encoding — is the work of Evan Miller and the ReadStat contributors. This project stands entirely on that work and preserves ReadStat's MIT copyright notice (see LICENSE).

Please support and cite the upstream project: https://github.com/WizardMac/ReadStat.

Known limitations

These are inherited by design from ReadStat and are not bugs in the port:

  • Generated .sas7bdat files may not open in SAS itself. The format is proprietary and undocumented; ReadStat (and therefore this port) produces files that ReadStat and compatible readers accept, but not every byte SAS's strict loader expects is known. Reading real SAS files works; producing files SAS will open is an open reverse-engineering problem upstream.
  • .sas7bdat value labels live in a separate .sas7bcat catalog, not the data file — a standalone .sas7bdat legitimately carries none.
  • Text metadata formats are read-only. .dct / .sps / .sas command and dictionary files are import schemas; there is no writer for them (there is nothing to serialize back). The binary .sas7bcat catalog is writable.

Contributing

Issues and pull requests are welcome — see CONTRIBUTING.md. Any change to a format must keep the end-to-end suite green (npm run test:e2e). Improvements to binary-format fidelity that also benefit upstream should be offered to ReadStat as well.

Commits follow Conventional Commits, and releases are fully automated: pushing to main runs semantic-release, which computes the next version, tags it, updates the changelog and publishes to npm — all derived from the commit messages. Publishing uses npm Trusted Publishing (OIDC) — no long-lived npm token is stored, and every release ships a provenance attestation. See CONTRIBUTING.md for the commit format and Releasing for the publisher setup.

License

MIT © 2026 Igor Sheko.

Derivative work of ReadStat, © 2013–2016 Evan Miller and ReadStat contributors (MIT). ReadStat's original copyright and permission notice are reproduced in LICENSE as required.