UNPKG

finnish-ssn

Version:

Small utility for validating and creating Finnish social security numbers. No more, no less, no dependencies.

182 lines (153 loc) 5.53 kB
'use strict' enum Sex { FEMALE = 'female', MALE = 'male', } interface SSN { valid: boolean sex: Sex ageInYears: number dateOfBirth: Date } export class FinnishSSN { public static FEMALE = Sex.FEMALE public static MALE = Sex.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) 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() let dayOfMonth = randomDay(year, month) const rollingId = randomNumber(800) + 99 // No need for padding when rollingId >= 100 if (!birthDayPassed(new Date(year, Number(month) - 1, Number(dayOfMonth)), today)) { if (this.isLeapYear(year)) { if (dayOfMonth === '29' && month === february) { dayOfMonth = '28' } } year-- } const possibleCenturySigns: string[] = [] centuryMap.forEach((value: number, key: string) => { if (value === Math.floor(year / 100) * 100) { possibleCenturySigns.push(key) } }) const centurySign = possibleCenturySigns[Math.floor(Math.random() * possibleCenturySigns.length)] year = year % 100 const yearString = yearToPaddedString(year) const checksumBase = parseInt(dayOfMonth + month + yearString + rollingId, 10) const 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('F', 2000) centuryMap.set('E', 2000) centuryMap.set('D', 2000) centuryMap.set('C', 2000) centuryMap.set('B', 2000) centuryMap.set('A', 2000) centuryMap.set('U', 1900) centuryMap.set('V', 1900) centuryMap.set('W', 1900) centuryMap.set('X', 1900) centuryMap.set('Y', 1900) centuryMap.set('-', 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[-U-Y]|[012]\d[A-F])\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) { 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()) ) }