finnish-ssn-2
Version:
Small utility for validating and creating Finnish social security numbers. No more, no less, no dependencies.
179 lines (152 loc) • 5.51 kB
text/typescript
/**
* Project: finnish-ssn
* Purpose: Validate and generate Finnish SSN's according to https://fi.wikipedia.org/wiki/Henkil%C3%B6tunnus
* Author: Ville Komulainen
*/
interface SSN {
valid: boolean
sex: string
ageInYears: number
dateOfBirth: Date
}
export class FinnishSSN {
public static FEMALE = 'female'
public static MALE = 'male'
/**
* Parse parameter given SSN string into Object representation.
* @param ssn - {String} SSN to parse
*/
public static parse(ssn: string): SSN {
// Sanity and format check, which allows to make safe assumptions on the format.
if (!SSN_REGEX.test(ssn)) {
throw new Error('Not valid SSN format')
}
const dayOfMonth = parseInt(ssn.substring(0, 2), 10)
const month = ssn.substring(2, 4)
const centuryId = ssn.charAt(6)
// tslint:disable-next-line:no-non-null-assertion
const year = parseInt(ssn.substring(4, 6), 10) + centuryMap.get(centuryId)!
const rollingId = ssn.substring(7, 10)
const checksum = ssn.substring(10, 11)
const sex = parseInt(rollingId, 10) % 2 ? this.MALE : this.FEMALE
const daysInMonth = daysInGivenMonth(year, month)
if (!daysInMonthMap.get(month) || dayOfMonth > daysInMonth) {
throw new Error('Not valid SSN')
}
const checksumBase = parseInt(ssn.substring(0, 6) + rollingId, 10)
const dateOfBirth = new Date(year, parseInt(month, 10) - 1, dayOfMonth, 0, 0, 0, 0)
const today = new Date()
return {
valid: checksum === checksumTable[checksumBase % 31],
sex,
dateOfBirth,
ageInYears: ageInYears(dateOfBirth, today)
}
}
/**
* Validates parameter given SSN. Returns true if SSN is valid, otherwise false.
* @param ssn - {String} For example '010190-123A'
*/
public static validate(ssn: string): boolean {
try {
return this.parse(ssn).valid
} catch (error) {
return false
}
}
/**
* Creates a valid SSN using the given age (Integer). Creates randomly male and female SSN'n.
* In case an invalid age is given, throws exception.
*
* @param age as Integer. Min valid age is 1, max valid age is 200
*/
public static createWithAge(age: number): string {
if (age < MIN_AGE || age > MAX_AGE) {
throw new Error(`Given age (${age}) is not between sensible age range of ${MIN_AGE} and ${MAX_AGE}`)
}
const today = new Date()
let year = today.getFullYear() - age
const month = randomMonth()
const dayOfMonth = randomDay(year, month)
let centurySign
let checksumBase
let checksum
const rollingId = randomNumber(800) + 99 // No need for padding when rollingId >= 100
centuryMap.forEach((value: number, key: string) => {
if (value === Math.floor(year / 100) * 100) {
centurySign = key
}
})
if (!birthDayPassed(new Date(year, Number(month) - 1, Number(dayOfMonth)), today)) {
year--
}
year = year % 100
const yearString = yearToPaddedString(year)
checksumBase = parseInt(dayOfMonth + month + yearString + rollingId, 10)
checksum = checksumTable[checksumBase % 31]
return dayOfMonth + month + yearString + centurySign + rollingId + checksum
}
public static isLeapYear(year: number): boolean {
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0
}
}
const centuryMap: Map<string, number> = new Map()
centuryMap.set('A', 2000)
centuryMap.set('B', 2000)
centuryMap.set('C', 2000)
centuryMap.set('D', 2000)
centuryMap.set('E', 2000)
centuryMap.set('F', 2000)
centuryMap.set('-', 1900)
centuryMap.set('U', 1900)
centuryMap.set('V', 1900)
centuryMap.set('W', 1900)
centuryMap.set('X', 1900)
centuryMap.set('Y', 1900)
centuryMap.set('+', 1800)
const february = '02'
const daysInMonthMap: Map<string, number> = new Map()
daysInMonthMap.set('01', 31)
daysInMonthMap.set('02', 28)
daysInMonthMap.set('03', 31)
daysInMonthMap.set('04', 30)
daysInMonthMap.set('05', 31)
daysInMonthMap.set('06', 30)
daysInMonthMap.set('07', 31)
daysInMonthMap.set('08', 31)
daysInMonthMap.set('09', 30)
daysInMonthMap.set('10', 31)
daysInMonthMap.set('11', 30)
daysInMonthMap.set('12', 31)
const checksumTable: string[] = '0123456789ABCDEFHJKLMNPRSTUVWXY'.split('')
const MIN_AGE = 1
const MAX_AGE = 200
const SSN_REGEX = /^(0[1-9]|[12]\d|3[01])(0[1-9]|1[0-2])([5-9]\d\+|\d\d[-UVWXY]|[012]\d[ABCDEF])\d{3}[\dA-Z]$/
function randomMonth(): string {
return `00${randomNumber(12)}`.substr(-2, 2)
}
function yearToPaddedString(year: number): string {
return year % 100 < 10 ? `0${year}` : year.toString()
}
function randomDay(year: number, month: string): string {
const maxDaysInMonth = daysInGivenMonth(year, month)
return `00${randomNumber(maxDaysInMonth)}`.substr(-2, 2)
}
function daysInGivenMonth(year: number, month: string) {
// tslint:disable-next-line:no-non-null-assertion
const daysInMonth = daysInMonthMap.get(month)!
return month === february && FinnishSSN.isLeapYear(year) ? daysInMonth + 1 : daysInMonth
}
function randomNumber(max: number): number {
return Math.floor(Math.random() * max) + 1 // no zero
}
function ageInYears(dateOfBirth: Date, today: Date): number {
return today.getFullYear() - dateOfBirth.getFullYear() - (birthDayPassed(dateOfBirth, today) ? 0 : 1)
}
function birthDayPassed(dateOfBirth: Date, today: Date): boolean {
return (
dateOfBirth.getMonth() < today.getMonth() ||
(dateOfBirth.getMonth() === today.getMonth() && dateOfBirth.getDate() <= today.getDate())
)
}