Skip to content

Latest commit

 

History

History
639 lines (467 loc) · 19 KB

README.md

File metadata and controls

639 lines (467 loc) · 19 KB

is-kennitala

Small, robust, type-safe and fast Icelandic kennitala parsing/handling library for JavaScript/TypeScript.

Tip

A "kennitala" is the Icelandic national id assigned to both private individuals and legal entities.

npm install is-kennitala

Contents:


Features

This library aims to be a best-of-breed JavaScript/TypeScript library for parsing and handling Icelandic kennitalas.

It was written after careful review of the existing npm packages.

It's philosophy and main features are:

  • Be tiny and fast/efficient and tree-shake incredibly well.
  • Make a clear distinction between "validating" and "parsing". The parser returns a helpful data object with a cleaned kennitala "value".
  • Make the "input-cleanup"/"gunk-acceptance" levels sensibly careful by default, with optional aggressiveness.
  • Provide good developer ergonomics, while promoting good, type-safe coding practices.
  • Provide first-class TypeScript signatures (including "branded types", which happen to play very nicely with valibot, zod, etc.)
  • Provide good JSDoc comments for inline IDE help, with @see links to this readme
  • Build on a suite of extensive unit tests.
  • Strike a sensible balance between speed and correctness, but provide ways to tilt towards more correctness.

API/Methods


parseKennitala

Syntax: parseKennitala(value: string, opts?: KennitalaParsingOptions): KennitalaData | undefined

Parses a string value to see if may be a technically valid kennitala, and if so, it returns a KennitalaData object with the cleaned up (and branded!) kennitala value along with some basic meta-data and a pretty-formatted version.

If the parsing/validation fails, it returns undefined.

import { parseKennitala } from 'is-kennitala';

// Minor trimming/cleaning occurs by default:
const personKtInput: string = ' 081153-6049';
const companyKtInput: string = '530269 – 7609 ';
const robotKtInput: string = ' 010130-2989';
const kerfisKtInput: string = '812345 6793';

const ktData = parseKennitala(personKtInput);
console.log(ktData.value); // '0811536049'
console.log(ktData.type); // 'person'
console.log(ktData.robot); // true
console.log(ktData.temporary); // false
console.log(ktData.formatted); // '081153-6049'

const ktData1 = parseKennitala(companyKtInput);
console.log(ktData1.value); // '5302697609'
console.log(ktData1.type); // 'company'
// etc...

// This input is too dirty:
const ktData2 = parseKennitala(`kt. 081153-6049`);
// Returns: undefined

// You can opt-in to more aggressive clean-up:
const ktData3 = parseKennitala('kt. 08 11 53 - 6049 yo!', {
  clean: 'aggressive',
});
// Returns: `KennitalaData` (same as `ktData` above)

// Reject company kennitalas:
const ktData4 = parseKennitala(companyKtInput, { type: 'person' });
// Returns: undefined

// Reject personal kennitalas:
const ktData5 = parseKennitala(personKtInput, { type: 'company' });
// Returns: undefined

// Reject robot ("Gervimaður") kennitalas by default:
const ktData6 = parseKennitala(robotKtInput);
// Returns: undefined

// Opt-in to accepting robot kennitalas:
const ktData7 = parseKennitala(robotKtInput, { robot: true });
// Returns: `KennitalaData`

// Opt-out of accepting temporary "Kerfiskennitala":
const ktData8 = parseKennitala(kerfisKtInput, { rejectTemporary: true });
// Returns: undefined

KennitalaParsingOptions

type?: KennitalaType

Set this to "person" or "company" to limit the validation to only either private persons or legal entities.

Default: undefined (allows both)

robot?: boolean

Set this to true if the parser should accept known "Gervimaður" kennitalas (only used for mocking or systems-testing).

Default: false

rejectTemporary?: boolean

Set this to true to reject short-term temporary kennitalas ("kerfiskennitala") given to short-stay (or no-stay) individuals/workers.

Default: false

Why are temporary kennitalas accepted by default?

  • "Kerfiskennitalas" are, by definition, valid kennitalas.
  • These are kennitalas of actual people, not some fake "Gervimaður".
  • This is a low-stakes library, with the simple purpose of catching obvious mistakes and show error messages fast.
  • Any real-stakes filtering (including for age, etc.) should/must occur in the next step anyway.

clean?: 'careful' | 'aggressive' | 'none' | false;

Controls how much to clean up the input string before parsing it.

  • "careful" mode (default) performs only minimal cleaning on the incoming string ...trims it and then removes space (and/or dash) right before the last four digits.
  • "aggressive" mode strips away ALL spaces and dashes, and throws away any leading/trailing non-digit gunk.
  • false/"none" does no cleanup whatsoever, not even trimming.

strictDate?: boolean

Set this flag to true to opt into a slower, but more perfect check for valid dates in permanent (non-"Kerfiskennitala") kennitalas.

Defaults to false — which may result in the occasional false-positive on values starting with something subtly impossible like "3104…" (April 31st).


isValidKennitala

Syntax: isValidKennitala(value: string, opts?: KennitalaParsingOptions): boolean

This shorthand function runs the input through parseKennitala and returns true if the parsing was successful.

Options are the same as for parseKennitala, except that clean option defaults to "none". This allows using the isValidKennitala method as a type-guard, and reduces the risk of accidental false-positives and over-confidence in the input string.

import { isValidKennitala } from 'is-kennitala';

const personKtInput: string = '0811536049';
const companyKtInput: string = '5302697609';
const robotKtInput: string = '0101302989';

if (isValidKennitala(personKtInput)) {
  // personKtInput is now typed as `Kennitala`
  const kennitala: Kennitala = personKtInput;
}

isValidKennitala(companyKtInput); // true
isValidKennitala(companyKtInput, { type: 'person' }); // false

isValidKennitala(robotKtInput); // false
isValidKennitala(robotKtInput, { robot: true }); // true
// etc...

NOTE: More often than not, you'll want to use parseKennitala instead, to get the cleaned-up, branded value and other meta-data goodies.


Kennitala discriminators

The library also exports a set of fast type-guarding functions for valid kennitalas: isPersonKennitala, isCompanyKennitala, isTempKennitala

All of these functions assume that their input is already validated as Kennitala. They perform no internal validation and are therefore insanely fast — but unreliable if coerced into processing random strings.

import {
  isPersonKennitala,
  isCompanyKennitala,
  isTempKennitala,
} from 'is-kennitala';

declare const allKennitalas: Array<Kennitala>;

const personKennitalas: Array<KennitalaPerson> =
  allKennitalas.filter(isPersonKennitala);
const companyKennitalas: Array<KennitalaCompany> =
  allKennitalas.filter(isCompanyKennitala);
const temporaryKennitalas: Array<KennitalaTemporary> =
  personKennitalas.filter(isTempKennitala);

NOTE: To safely check if a plain, non-validated string input is a certain type of kennitala, use parseKennitala and check the .type of the retured data object. So, instead of isPersonKennitala(someString) do this:

import { parseKennitala } from 'is-kennitala';

cons res = parseKennitala(someString);
const isPerson = !!res && res.type === 'person';

...or this:

const res = parseKennitala(someString, { type: 'person' });
const isPerson = !!res;

That way you also get a cleaned-up, normalized Kennitala value, and other goodies.


getKennitalaBirthDate

Syntax: getKennitalaBirthDate(value: string): Date | undefined

Returns the (UTC) birth-date (or founding-date) of a roughly "kennitala-shaped" string. It does NOT check if it is a valid Kennitala and assumes such validation has already happened beforehand.

It returns undefined for malformed (non-kennitala shaped) strings, temporary "kerfiskennitalas" and kennitalas with nonsensical dates, even if they're otherwise numerically valid.

import { getKennitalaBirthDate } from 'is-kennitala';

// Kennitala of a person
const birthDate = getKennitalaBirthDate('0101302989');
// Returns: new Date(1930-01-01)

// The company kennitala of Reykjavík City
const birthDate = getKennitalaBirthDate(' 530269–7609 ');
// Returns: new Date(1969-02-13)
// Note that birth dates for Very Old™ legal entities are unreliable/bogus.

// Temporary "kerfiskennitala" starts with random gibberish
getKennitalaBirthDate('812345-6793');
// Returns: undefined

// Nonsensical dates return undefined
getKennitalaBirthDate('123456-7890'); // undefined

// Nonsense inputs return undefined
getKennitalaBirthDate('Not a kennitala!'); // undefined
getKennitalaBirthDate(''); // undefined

formatKennitala

Syntax: formatKennitala(value: string, separator?: string): string

Runs minimal cleanup on the input string and if it looks rougly like a kennitala, then it inserts a nice separator ('-' by default) before the last four digits.

It falls back to returning the input untouched.

import { formatKennitala } from 'is-kennitala';

formatKennitala('1234567890'); // '123456-7890'
formatKennitala(' 123456-7890\n'); // '123456-7890'
formatKennitala('123456 - 7890'); // '123456-7890'

// Not kennitala-shaped, returned unchanged:
formatKennitala('1234 567890'); // '1234 567890'
formatKennitala('12345 and 67890  '); // '12345 and 67890  '
formatKennitala('123456789012345'); // '123456789012345'

// With a fancy banana separator:
formatKennitala('1234567890', ' 🍌 '); // '123456 🍌 7890'

NOTE: The KennitalaData object returned by parseKennitala has a .formatted getter, which can often be used instead of this method.


cleanKennitalaCareful

Syntax: cleanKennitalaCareful(value: string): string

Trims the string and then only removes spaces and/or a dash (or en-dash) before the last four of the ten digits.

This lowers the chance of false-positives, when the result is parsed/validated, but still allows for some flexibility in the input.

Defaults to returning the (trimmed) original string, if the pattern doesn't match.

import { cleanKennitalaCareful } from 'is-kennitala';

// Cleaned:
cleanKennitalaCareful(' 123456-7890'); // Returns: '1234567890'
cleanKennitalaCareful('123456 7890 '); // Returns: '1234567890'
cleanKennitalaCareful(' 123456 - 7890'); // Returns: '1234567890'
cleanKennitalaCareful('123456 -7890'); // Returns: '1234567890'

// Only trimmed as the input is not "kennitala-shaped" enough:
// WAT‽
cleanKennitalaCareful(' abc '); // Returns: 'abc'
// preceeding non-digit gunk
cleanKennitalaCareful('kt. 123456-7890 '); // Returns: 'kt. 123456-7890'
cleanKennitalaCareful('tel: 123456-7890'); // Returns: 'tel: 123456-7890'
// Suspicious splits
cleanKennitalaCareful(' 1234-567890'); // Returns: '1234-567890'
cleanKennitalaCareful('123 456-7890'); // Returns: '123 456-7890'

NOTE: This is the default cleaning function used internally by parseKennitala.


cleanKennitalaAggressive

Syntax: cleanKennitalaAggressive(value: string): string

Aggressively strips away ALL spaces and dashes (or en-dashes) from the string, as well as any trailing and leading non-digit gunk.

Returns whatever is left.

Use with caution, as this level of aggression increases the chances of false-positives during parsing.

import { cleanKennitalaAggressive } from 'is-kennitala';

// Aggressive cleaning is aggressive:
cleanKennitalaAggressive('  12 34 56 - 78 90'); // Returns: '1234567890'
cleanKennitalaAggressive('1-2-3 4-5 6-7-8 9-0'); // Returns: '1234567890'

// Trailing/leading non-digit content is removed:
cleanKennitalaAggressive(' abc '); // Returns: ''
cleanKennitalaAggressive('(kt. 123456-7890)'); // Returns: '1234567890'
cleanKennitalaAggressive('(s. 765 4321) '); // Returns: '7654321'

// Non-digit/-space/-dash content in the middle is left in:
cleanKennitalaAggressive('(kt. 123456-7890, tel. 765 4321) ');
// Returns: '1234567890,tel.7654321'
cleanKennitalaAggressive('(tel. 123-4567, 765-4321)');
// Returns: '1234567,7654321'

NOTE: This is cleaning function used internally by parseKennitala when it's called with its clean option set to 'aggressive'.


generateKennitala

Syntax: generateKennitala(opts?: { type?: KennitalaType; birthDate?: Date; robot?: boolean; temporary?: boolean;}): Kennitala

Generates a technically valid Kennitala (possibly a real one!) for testing and generating mock-data.

Defaults to making a KennitalaPerson, unless opts.type is set to "company".

Picks a birth/founding date at random, unless a valid opts.birthDate is provided.

However, opts.birthDate is ignored when generating robot and temporary kennitalas.

import { generateKennitala } from 'is-kennitala';

const kt1: KennitalaPerson = generateKennitala();
const kt2: KennitalaPerson = generateKennitala({
  birthDate: new Date('1980-01-01'), // specific birth date
});
const kt5: KennitalaPerson = generateKennitala({ robot: true });

const kt3: KennitalaCompany = generateKennitala({ type: 'company' });
const kt4: KennitalaCompany = generateKennitala({
  type: 'company',
  birthDate: new Date('2005-06-17'), // specific founding date
});

const kt6: KennitalaTemporary = generateKennitala({ temporary: true });

NOTE: This method is dumb and slow. It uses brute-force to search for valid checksum characters. If you need more speed, please feel free to submit a PR with a better implementation.


Exported Types


Branded Kennitala Types

This library exports a set of branded types for parsed/validated kennitala string values.

These are useful to ensure that a given string value has been parsed by this library, and is not just some random unsafe string.

import type {
  Kennitala, // Any valid kennitala
  KennitalaCompany,
  KennitalaPerson, // includes KennitalaTemporary
  KennitalaTemporary, // Temporary kennitala (for short-term workers)
} from 'is-kennitala';

interface Customer {
  kennitala: Kennitala;
  name: string;
  email: string;
  // ...other props
}

interface Individual extends Customer {
  kennitala: KennitalaPerson;
}
interface Business extends Customer {
  kennitala: KennitalaCompany;
}

const getIndividual = (kt: KennitalaPerson): Promise<Individual> => {
  // ...
};
const getBusiness = (kt: KennitalaCompany): Promise<Business> => {
  // ...
};

If you have APIs that you trust to return validated kennitala fields, you should cast them to the branded Kennitala* types, before passing them around in your application. Example:

import type { KennitalaPerson } from 'is-kennitala';

const mapAPIIndividual = (apiResult: APIIndividual): Individual => {
  const { kt, fullName, emailAddress /* other props */ } = apiResult;
  return {
    kennitala: kt as KennitalaPerson,
    name: fullName,
    email: emailAddress,
    // ...map other props
  };
};

Valibot example

Here's a quick example of how parseKennitala can be used in a valibot transform to return the branded types above.

import * as v from 'valibot';
import { parseKennitala } from 'is-kennitala';

const kennitalaSchema = v.pipe(
  v.string(),
  v.rawTransform(({ dataset, addIssue, NEVER }) => {
    const kt = parseKennitala(dataset.value);
    if (!kt) {
      addIssue({ message: 'Not a valid kennitala' });
      return NEVER;
    }
    return kt.value; // branded string value
  })
);

Zod validation example

Here's a quick example of how parseKennitala can be used in a zod transform to return the branded types above.

import { z } from 'zod';
import { parseKennitala } from 'is-kennitala';

const kennitalaSchema = z
  .string()
  .transform((value: string, ctx: z.RefinementCtx) => {
    const kennitala = parseKennitala(value /*, options */);
    if (!kennitala) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: 'Not a valid kennitala',
      });
      return z.NEVER;
    }
    return kennitala.value; // branded string value
  });

type KennitalaType

Union of the two main types of kennitalas: "person" for private persons, and "company" for legal entities.

Used in type options of parseKennitala() and isValidKennitala().

import type { KennitalaType } from 'is-kennitala';

const ktType: KennitalaType = 'person';

type KennitalaData

The data object returned by the parseKennitala() method.

It contains the cleaned-up (and branded) kennitala value, as well as information about it's type and other properties.

import type { KennitalaData } from 'is-kennitala';

const logKennitalaProps = (data: KennitalaData) => {
  console.log(data.value); // Branded string type
  console.log(data.formatted); // pretty-printed version of the kennitala
  console.log(data.type); // "person" or "company"
  console.log(data.robot); // boolean
  console.log(data.temporary); // boolean (if type is "person")
};

The library also exports more narrow types KennitalaDataPerson and KennitalaDataCompany, with discriminated values for the type, robot and temporary properties.

import type { KennitalaDataPerson, KennitalaDataCompany } from 'is-kennitala';

The union of these two types forms the broader KennitalaData type.


Contributing

This project uses the Bun runtime for development (tests, build, etc.)

PRs are welcoms!


Change Log

See CHANGELOG.md

Other Iceland-Themed Libraries

  • postnumer - Icelandic post-codes (Póstnúmer) and town/locality names and their National Registry ID codes.
  • fridagar - Icelandic public holidays and other commonly observed 'special' days.