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

@deathbycode/civitas-id-core

v1.0.1

Published

Core interfaces and utilities for official identity number validation

Readme

Civitas ID

TypeScript pnpm License Tests

A comprehensive TypeScript library for validating and working with Swedish personal identification numbers (personnummer), coordination numbers (samordningsnummer), and organisation numbers (organisationsnummer).

Table of Contents

Overview

This library provides comprehensive functionality for working with Swedish official identification numbers according to official rules and formats from Skatteverket and Bolagsverket.

Features

Personal Identification

  • Personal Numbers (personnummer): Validation and formatting of Swedish personal ID numbers
  • Coordination Numbers (samordningsnummer): Support for coordination numbers assigned to individuals without Swedish personal numbers
  • Support for different formats (10-digit, 12-digit, with or without separators)
  • Handling of personal numbers from different time periods (19th, 20th, and 21st centuries)
  • Birth date extraction and age calculation
  • Gender determination (male/female)

Organisation Identification

  • Organisation Numbers (organisationsnummer): Validation and formatting of Swedish organisation numbers
  • Organisation Form Detection: Automatic detection of organisation type (e.g., Aktiebolag, Ekonomiska foreningar, etc.)
    • Support for all 33 official Swedish organisation forms
    • Distinction between legal persons (juridisk person) and physical persons (enskild firma)
  • Hybrid Format Support: Accepts both official 10-digit format and legacy 12-digit format for compatibility
    • Input: Accepts 556012-3456 (10-digit) or 165560123456 (12-digit legacy)
    • Output: Always returns official 10-digit format

Additional Features

  • Checksum validation using the Luhn algorithm
  • Type-safe API with discriminated unions and type guards
  • Comprehensive error messages via InvalidIdNumberError
  • Extensive test coverage (34,026 tests)
  • Test utilities (fakers) for generating valid test data

Installation

npm install @deathbycode/civitas-id-sweden
# or
pnpm add @deathbycode/civitas-id-sweden

Usage Examples

Personal Numbers

import { PersonalId } from "@deathbycode/civitas-id-sweden";

// Parse and validate a personal number (returns undefined on failure)
const personalId = PersonalId.parse("202407132394");

if (personalId) {
  const birthDate = personalId.getBirthDate(); // LocalDate { year: 2024, month: 7, day: 13 }
  const longFormat = personalId.longFormat();    // "202407132394"
}

// Or use parseOrThrow() when you expect valid input
const id = PersonalId.parseOrThrow("202407132394");
const birthDate = id.getBirthDate();

// Format in different ways
id.longFormat();                  // "202407132394"
id.shortFormat();                 // "2407132394"
id.shortFormatWithSeparator();    // "240713-2394"

// Check gender
id.isMale();    // true or false
id.isFemale();  // true or false

// Check age
id.getAge();    // current age in years
id.isAdult();   // true if 18+
id.isChild();   // true if under 18

// Check if valid
PersonalId.isValid("202407132394"); // true

Coordination Numbers

import { CoordinationId } from "@deathbycode/civitas-id-sweden";

// Parse a coordination number (returns undefined on failure)
const coordId = CoordinationId.parse("198206822390");

// Or use parseOrThrow() when you expect valid input
const id = CoordinationId.parseOrThrow("198206822390");

// Coordination numbers have birth day + 60
const birthDate = id.getBirthDate(); // 1982-06-22 (not 1982-06-82)

Organisation Numbers

import { OrganisationId, OrganisationNumberType } from "@deathbycode/civitas-id-sweden";

// Parse an organisation number (accepts both 10 and 12-digit formats)
const orgId = OrganisationId.parse("556012-3456");

// Or use parseOrThrow() when you expect valid input
const id = OrganisationId.parseOrThrow("556012-3456");

// Get the organisation form
const form = id.getOrganisationForm();
console.log(form.code);        // 56
console.log(form.description); // "Ovriga aktiebolag"

// Check if legal or physical person
id.isLegalPerson();   // true for companies
id.isPhysicalPerson(); // true for sole proprietors

// Format (always outputs 10-digit official format)
id.longFormatWithSeparator(); // "556012-3456"

// Validate with type checking
OrganisationId.isValid("556012-3456", OrganisationNumberType.LEGAL_PERSON);

Parsing Any Swedish ID Type

When you don't know the specific type of Swedish ID, use the unified parsing methods:

import {
  SwedishOfficialId,
  PersonalId,
  CoordinationId,
  OrganisationId,
  InvalidIdNumberError,
  isPersonalId,
  isCoordinationId,
  isOrganisationId,
} from "@deathbycode/civitas-id-sweden";

// Parse any Swedish ID type (returns undefined on failure)
const result = SwedishOfficialId.parseAny("202407132394");

if (result) {
  // Use type guards to narrow the type
  if (isPersonalId(result)) {
    console.log("Personal ID:", result.getBirthDate());
  } else if (isCoordinationId(result)) {
    console.log("Coordination ID:", result.getBirthDate());
  } else if (isOrganisationId(result)) {
    console.log("Organisation ID:", result.getOrganisationForm());
  }
}

// Or use parseAnyOrThrow() when you expect valid input
try {
  const id = SwedishOfficialId.parseAnyOrThrow("202407132394");

  // Use discriminated union with switch
  switch (id.type) {
    case "PERSONAL":
      console.log(id.getBirthDate());
      break;
    case "COORDINATION":
      console.log(id.getBirthDate());
      break;
    case "ORGANISATION":
      console.log(id.getOrganisationForm());
      break;
  }
} catch (e) {
  if (e instanceof InvalidIdNumberError) {
    console.error("Invalid Swedish ID:", e.message);
  } else {
    throw e;
  }
}

// Validate any Swedish ID type
SwedishOfficialId.isValid("202407132394"); // true

Parsing Priority: The unified parser attempts to parse in this order:

  1. Personal number (personnummer)
  2. Coordination number (samordningsnummer)
  3. Organisation number (organisationsnummer)

Error Handling

All failures from the throwing APIs (parseOrThrow, parseAnyOrThrow, format) throw InvalidIdNumberError, which extends the standard Error class and supports error cause chaining via ErrorOptions. Non-throwing parse helpers (parse, parseAny) return undefined on failure:

import { PersonalId, InvalidIdNumberError } from "@deathbycode/civitas-id-sweden";

// Catch and inspect parsing errors
try {
  const id = PersonalId.parseOrThrow("invalid-input");
} catch (e) {
  if (e instanceof InvalidIdNumberError) {
    console.error(e.message); // "Invalid personal ID: invalid-input"
  }
}

// Error cause chaining — wrap lower-level errors
try {
  processId(input);
} catch (e) {
  throw new InvalidIdNumberError("Failed to process ID", { cause: e });
}

Format Options

Personal and coordination IDs support six output formats via the PnrFormat union. The separator character is age-sensitive: - for persons under 100, + for centenarians.

| PnrFormat Value | Pattern | Example (born 1990-05-15) | Example (born 1890-05-15) | |-----------------|---------|---------------------------|---------------------------| | LONG_FORMAT | YYYYMMDDXXXX | 199005151239 | 189005151239 | | LONG_FORMAT_WITH_STANDARD_SEPARATOR | YYYYMMDD-XXXX | 19900515-1239 | 18900515-1239 | | LONG_FORMAT_WITH_SEPARATOR | YYYYMMDD-XXXX or YYYYMMDD+XXXX | 19900515-1239 | 18900515+1239 | | SHORT_FORMAT | YYMMDDXXXX | 9005151239 | 9005151239 | | SHORT_FORMAT_WITH_STANDARD_SEPARATOR | YYMMDD-XXXX | 900515-1239 | 900515-1239 | | SHORT_FORMAT_WITH_SEPARATOR | YYMMDD-XXXX or YYMMDD+XXXX | 900515-1239 | 900515+1239 |

import { PersonalId, PnrFormat } from "@deathbycode/civitas-id-sweden";

const id = PersonalId.parseOrThrow("199005151239");

// Convenience methods (use standard separator)
id.longFormat();               // "199005151239"
id.shortFormat();              // "9005151239"
id.longFormatWithSeparator();  // "19900515-1239"
id.shortFormatWithSeparator(); // "900515-1239"

// Explicit format selection
id.formatted(PnrFormat.LONG_FORMAT_WITH_SEPARATOR);  // "19900515-1239" (or +)
id.formatted(PnrFormat.SHORT_FORMAT_WITH_SEPARATOR); // "900515-1239" (or +)

Organisation IDs also accept PnrFormat in their formatted() method, but long and short variants produce identical output since organisation numbers are always 10 digits (the internal 16 century prefix is always stripped).

Type Guards & Discriminated Unions

SwedishOfficialId is a union type with a type discriminant field:

type SwedishOfficialId = PersonalId | CoordinationId | OrganisationId;

Each variant carries a type field: "PERSONAL", "COORDINATION", or "ORGANISATION".

Type guard functions

Four type guard functions are exported for narrowing:

import {
  SwedishOfficialId,
  isPersonalId,
  isCoordinationId,
  isOrganisationId,
  isPersonOfficialId,
} from "@deathbycode/civitas-id-sweden";

const id = SwedishOfficialId.parseAnyOrThrow(input);

// Narrow to a specific type
if (isPersonalId(id)) {
  id.getBirthDate(); // PersonalId methods available
}

if (isCoordinationId(id)) {
  id.getBirthDate(); // CoordinationId methods available
}

if (isOrganisationId(id)) {
  id.getOrganisationForm(); // OrganisationId methods available
}

// Narrow to person types (PersonalId | CoordinationId)
if (isPersonOfficialId(id)) {
  id.getBirthDate(); // shared person methods available
  id.getAge();
  id.isMale();
}

Switch on discriminant

switch (id.type) {
  case "PERSONAL":
    console.log("Personal ID:", id.getBirthDate());
    break;
  case "COORDINATION":
    console.log("Coordination ID:", id.getBirthDate());
    break;
  case "ORGANISATION":
    console.log("Organisation ID:", id.getOrganisationForm());
    break;
}

Test Utilities (Fakers)

The library includes test utilities for generating valid test data, available via a separate subpath export to keep them out of production bundles:

import { PersonalIdFaker } from "@deathbycode/civitas-id-sweden/testing";

Personal ID Faker

import { PersonalIdFaker } from "@deathbycode/civitas-id-sweden/testing";
import { LocalDate } from "@deathbycode/civitas-id-core";

// Generate random valid personal ID
const randomId = PersonalIdFaker.create();

// Generate with specific birth date
const specificId = PersonalIdFaker.create(LocalDate.of(1990, 5, 15));

// Generate with specific date components
const id = PersonalIdFaker.createFor(1990, 5, 15);

// Generate gender-specific IDs
const male = PersonalIdFaker.createMale();
const female = PersonalIdFaker.createFemale();

// Generate centenarian (100+ years old)
const centenarian = PersonalIdFaker.createCentenarian();

Coordination ID Faker

import { CoordinationIdFaker } from "@deathbycode/civitas-id-sweden/testing";

// Generate random coordination ID
const randomId = CoordinationIdFaker.create();

// Gender-specific and centenarian methods also available
const male = CoordinationIdFaker.createMale();
const female = CoordinationIdFaker.createFemale();
const centenarian = CoordinationIdFaker.createCentenarian();

Organisation ID Faker

import { SwedishOrganisationIdFaker } from "@deathbycode/civitas-id-sweden/testing";

// Generate random organisation ID (legal person)
const legalPerson = SwedishOrganisationIdFaker.create();

// Generate specific types
const legal = SwedishOrganisationIdFaker.createLegalPerson();
const physical = SwedishOrganisationIdFaker.createPhysicalPerson();

Swedish Official ID Faker

import { SwedishOfficialIdFaker } from "@deathbycode/civitas-id-sweden/testing";

// Generate random Swedish ID (PersonalId, CoordinationId, or OrganisationId)
const randomId = SwedishOfficialIdFaker.create();

// Generate multiple IDs at once
const ids = SwedishOfficialIdFaker.createMany(10);

Note: All fakers generate cryptographically secure random IDs using crypto.getRandomValues() and ensure proper Luhn checksum validation.

Architecture

Multi-Country Design

The library is architected for multi-country support with a clear separation of concerns:

civitas-id/
├── @deathbycode/civitas-id-core          — Generic interfaces and utilities
│   ├── OfficialId, PersonOfficialId, OrganisationOfficialId interfaces
│   ├── Generic Luhn algorithm implementation
│   └── LocalDate value object
└── @deathbycode/civitas-id-sweden        — Swedish implementations
    ├── PersonalId, CoordinationId, OrganisationId
    ├── Swedish-specific validation and formatting
    └── @deathbycode/civitas-id-sweden/testing — Test fakers for generating Swedish IDs

Design Principles:

  • Core package: Contains only country-agnostic interfaces and truly generic utilities
  • Country packages: Implement core interfaces with country-specific validation rules
  • Test utilities: Available via @deathbycode/civitas-id-sweden/testing subpath to keep production bundles clean
  • Discriminated unions: Type-safe ID hierarchy using TypeScript discriminated unions with type field
  • Idiomatic TypeScript: Default generic parameters, exhaustiveness guards, discriminated union results, const object singletons

Extensibility: The architecture is ready for additional countries (Norway, Finland, Denmark, etc.) following the same pattern. Each country module is self-contained and independent.

Core Utilities

LocalDate

A minimal, immutable date value object from @deathbycode/civitas-id-core. No external date library dependencies.

import { LocalDate } from "@deathbycode/civitas-id-core";

// Factories
const date = LocalDate.of(1990, 5, 15);  // from components
const today = LocalDate.now();            // current UTC date
const parsed = LocalDate.parse("1990-05-15"); // from ISO string

// Read-only properties
date.year;   // 1990
date.month;  // 5
date.day;    // 15

// Methods
date.age();                       // age in years relative to today
date.age(LocalDate.of(2026, 1, 1)); // age relative to a specific date
date.isValid();                   // true if the date exists in the calendar
date.equals(other);               // structural equality
date.toString();                  // "1990-05-15"

Clock injection: Methods like getAge() on ID objects accept an optional clock function () => LocalDate so tests can control the current date:

const id = PersonalId.parseOrThrow("199005151239");
id.getAge(() => LocalDate.of(2026, 1, 1)); // 35

ValidationResult

A discriminated union for validation outcomes, from @deathbycode/civitas-id-core:

import { ValidationResult } from "@deathbycode/civitas-id-core";

// Factory methods
const ok = ValidationResult.valid();
//    ^? { valid: true }

const fail = ValidationResult.invalid("Bad checksum", "CHECKSUM");
//    ^? { valid: false, errorMessage: "Bad checksum", errorCode: "CHECKSUM" }

// Narrowing
function check(result: ValidationResult) {
  if (result.valid) {
    // result is { valid: true } — no error fields
  } else {
    console.error(result.errorMessage); // string
    console.error(result.errorCode);    // string | undefined
  }
}

// String representation
ValidationResult.toString(ok);   // "ValidationResult{valid=true}"
ValidationResult.toString(fail); // "ValidationResult{valid=false, errorMessage='Bad checksum', errorCode='CHECKSUM'}"

LuhnAlgorithm

A const object implementing the standard Luhn (mod-10) checksum algorithm, from @deathbycode/civitas-id-core:

import { LuhnAlgorithm } from "@deathbycode/civitas-id-core";

// Calculate the check digit for a digit string
LuhnAlgorithm.calculateCheckDigit("7992739871"); // 3

// Verify a complete number (last digit is the check digit)
LuhnAlgorithm.isChecksumValid("79927398713"); // true

Both methods accept an optional maxDigits parameter to limit validation to the last N digits of the input. The Swedish variant uses maxDigits=10 internally so that 12-digit personal numbers (YYYYMMDDXXXX) are validated on only the 10-digit suffix (YYMMDDXXXX).

Requirements

  • Node.js 18+
  • TypeScript 5.7+ (for consumers using TypeScript)

Building the Project

# Install dependencies
pnpm install

# Build all packages
pnpm -r build

# Run all tests
pnpm -r test

# Type check
pnpm -r exec tsc --noEmit

# Lint
pnpm biome check .

API Design

The library provides a safe API with two parsing approaches:

| Method | Return Type | Use Case | |--------|-------------|----------| | parse(string) | T \| undefined | Uncertain input (user-provided, external APIs) | | parseOrThrow(string) | T | Expected valid input (database, known data) | | isValid(string) | boolean | Validation only |

Format Methods

All ID types implement the OfficialId interface and share a common set of format methods:

| Method | PersonalId (1990-05-15) | OrganisationId (556012-3456) | |--------|-------------------------|------------------------------| | longFormat() | "199005151239" | "5560123456" | | shortFormat() | "9005151239" | "5560123456" | | longFormatWithSeparator() | "19900515-1239" | "556012-3456" | | shortFormatWithSeparator() | "900515-1239" | "556012-3456" | | formatted(PnrFormat) | (see Format Options) | (long = short for org IDs) |

For organisation IDs, long and short variants produce identical output because organisation numbers are always 10 digits.

The base SwedishOfficialId namespace provides unified parsing when the ID type is unknown:

| Method | Return Type | Use Case | |--------|-------------|----------| | parseAny(string) | SwedishOfficialId \| undefined | Parse any type (uncertain input) | | parseAnyOrThrow(string) | SwedishOfficialId | Parse any type (expected valid) |

Swedish Organisation Number Format

Official Standard: 10-Digit Format Only

Swedish organisation numbers are officially ALWAYS 10 digits.

Format: NNNNNN-NNNN where:

  • Positions 1-2: Legal entity group code (organisation form)
  • Position 3: Always >= 2 (to distinguish from personal numbers)
  • Position 10: Check digit (Luhn algorithm)

The 12-Digit Convention

The "16" prefix (e.g., 16NNNNNNNNNN) is not an official standard and stems from legacy IT systems using "16" as a placeholder century.

Civitas-ID implementation:

  • Input: Accepts both 10-digit (556012-3456) and 12-digit (165560123456) for legacy compatibility
  • Output: Always returns the official 10-digit format
// Both inputs work
const org1 = OrganisationId.parseOrThrow("556012-3456");   // 10-digit
const org2 = OrganisationId.parseOrThrow("165560123456");   // 12-digit legacy

// Output is always 10-digit
org1.longFormat();              // "5560123456"
org2.longFormat();              // "5560123456"
org1.longFormatWithSeparator(); // "556012-3456"
org2.longFormatWithSeparator(); // "556012-3456"

Organisation Forms

The library supports all 33 official Swedish organisation forms. The organisation form is automatically extracted from the first two digits of the organisation number.

| Code | Organisation Form (Swedish) | Description | |------|----------------------------|-------------| | 0 | Ingen organisationsform | Physical person (not a legal entity) | | 21 | Enkla bolag | Simple companies | | 22 | Partrederier | Shipping partnerships | | 31 | Handelsbolag, kommanditbolag | Trading partnerships, limited partnerships | | 32 | Gruvbolag | Mining companies | | 41 | Bankaktiebolag | Banking companies | | 42 | Forsakringsaktiebolag | Insurance companies | | 43 | Europabolag | European companies (SE) | | 49 | Ovriga aktiebolag | Other limited companies (most common) | | 51 | Ekonomiska foreningar | Economic associations | | 53 | Bostadsrattsforeningar | Tenant-ownership associations | | 54 | Kooperativ Hyresrattsforening | Cooperative rental associations | | 55 | Europakooperativ, EGTS och Eric-konsortier | European cooperatives, EGTC, Eric consortia | | 61 | Ideella foreningar | Non-profit associations | | 62 | Samfalligheter | Joint property management associations | | 63 | Registrerat trossamfund | Registered religious communities | | 71 | Familjestiftelser | Family foundations | | 72 | Ovriga stiftelser och fonder | Other foundations and funds | | 81 | Statliga enheter | State entities | | 82 | Kommuner | Municipalities | | 83 | Kommunalforbund | Municipal federations | | 84 | Regioner | Regions | | 85 | Allmanna forsakringskassor | General insurance funds | | 87 | Offentliga korporationer och anstalter | Public corporations and agencies | | 88 | Hypoteksforeningar | Mortgage associations | | 89 | Regionala statliga myndigheter | Regional state authorities | | 91 | Oskiftade dodsbon | Undivided estates | | 92 | Omsesidiga forsakringsbolag | Mutual insurance companies | | 93 | Sparbanker | Savings banks | | 94 | Understodsforeningar och Forsakringsforeningar | Benefit societies and insurance associations | | 95 | Arbetsloshetskassor | Unemployment insurance funds | | 96 | Utlandska juridiska personer | Foreign legal entities | | 98 | Ovriga svenska juridiska personer | Other Swedish legal entities (special legislation) | | 99 | Juridisk form ej utredd | Legal form not determined |

Total: 34 forms (1 physical person code + 33 legal entity forms)

Source: Swedish Companies Registration Office (Bolagsverket)

License

This project is licensed under the MIT License - see the LICENSE file for details.

Official References