@remato/personal-code-to-birthday
Advanced tools
Comparing version 1.1.0 to 1.1.1
{ | ||
"name": "@remato/personal-code-to-birthday", | ||
"version": "1.1.0", | ||
"version": "1.1.1", | ||
"description": "Converts personal identification codes from various countries into birthdate", | ||
"license": "MIT", | ||
"main": "src/index.ts", | ||
"repository": { | ||
"url": "https://github.com/rematocorp/personal-code-to-birthday" | ||
}, | ||
"scripts": { | ||
@@ -50,3 +53,4 @@ "build": "ncc build src/index.ts", | ||
] | ||
} | ||
}, | ||
"prettier": "@remato/prettier-config" | ||
} |
@@ -12,4 +12,5 @@ import personalCodeToBirthday from './index' | ||
test('Latvia', () => { | ||
expect(personalCodeToBirthday('050393-12344')).toEqual({ day: 5, month: 3, year: 1993 }) | ||
expect(personalCodeToBirthday('050303-22344')).toEqual({ day: 5, month: 3, year: 2003 }) | ||
expect(personalCodeToBirthday('240860-28074')).toEqual({ day: 24, month: 8, year: 1960 }) | ||
expect(personalCodeToBirthday('071023-38935')).toEqual({ day: 7, month: 10, year: 2023 }) | ||
expect(personalCodeToBirthday('240860-20090')).toEqual({ day: 24, month: 8, year: 1960 }) | ||
}) | ||
@@ -23,3 +24,2 @@ | ||
expect(personalCodeToBirthday('101085-7001')).toEqual({ day: 10, month: 10, year: 1985 }) | ||
expect(personalCodeToBirthday('101085+789W')).toEqual({ day: 10, month: 10, year: 1885 }) | ||
expect(personalCodeToBirthday('150752-308N')).toEqual({ day: 15, month: 7, year: 1952 }) | ||
@@ -30,13 +30,23 @@ expect(personalCodeToBirthday('010100A123D')).toEqual({ day: 1, month: 1, year: 2000 }) | ||
test('Sweden', () => { | ||
expect(personalCodeToBirthday('199001011234')).toEqual({ day: 1, month: 1, year: 1990 }) | ||
expect(personalCodeToBirthday('8112289874')).toEqual({ day: 28, month: 12, year: 1981 }) | ||
expect(personalCodeToBirthday('811228-9874')).toEqual({ day: 28, month: 12, year: 1981 }) | ||
expect(personalCodeToBirthday('670919-9530')).toEqual({ day: 19, month: 9, year: 1967 }) | ||
expect(personalCodeToBirthday('230919-9533')).toEqual({ day: 19, month: 9, year: 2023 }) | ||
expect(personalCodeToBirthday('196709199530')).toEqual({ day: 19, month: 9, year: 1967 }) | ||
expect(personalCodeToBirthday('19670919-9530')).toEqual({ day: 19, month: 9, year: 1967 }) | ||
expect(personalCodeToBirthday('20230919-9533')).toEqual({ day: 19, month: 9, year: 2023 }) | ||
}) | ||
test('Norway', () => { | ||
expect(personalCodeToBirthday('01020352345')).toEqual({ day: 1, month: 2, year: 2003 }) | ||
expect(personalCodeToBirthday('10021559844')).toEqual({ day: 10, month: 2, year: 2015 }) | ||
expect(personalCodeToBirthday('100215 59844')).toEqual({ day: 10, month: 2, year: 2015 }) | ||
expect(personalCodeToBirthday('10028439895')).toEqual({ day: 10, month: 2, year: 1984 }) | ||
expect(personalCodeToBirthday('27081439368')).toEqual({ day: 27, month: 8, year: 2014 }) | ||
}) | ||
test('Denmark', () => { | ||
expect(personalCodeToBirthday('050305-4567')).toEqual({ day: 5, month: 3, year: 2005 }) | ||
expect(personalCodeToBirthday('050384-1235')).toEqual({ day: 5, month: 3, year: 1984 }) | ||
expect(personalCodeToBirthday('070589-8901')).toEqual({ day: 7, month: 5, year: 1989 }) | ||
expect(personalCodeToBirthday('230309-4456')).toEqual({ day: 23, month: 3, year: 2009 }) | ||
expect(personalCodeToBirthday('050384-4567')).toEqual({ day: 5, month: 3, year: 1984 }) | ||
expect(personalCodeToBirthday('230309-3020')).toEqual({ day: 23, month: 3, year: 1909 }) | ||
}) | ||
@@ -63,3 +73,4 @@ | ||
test('Latvia', () => { | ||
expect(personalCodeToBirthday('123456-12345')).toBeNull() | ||
expect(personalCodeToBirthday('071023-38934')).toBeNull() | ||
expect(personalCodeToBirthday('071523-38937')).toBeNull() | ||
}) | ||
@@ -79,5 +90,4 @@ | ||
test('Sweden', () => { | ||
expect(personalCodeToBirthday('1990010123456')).toBeNull() | ||
expect(personalCodeToBirthday('199013011234')).toBeNull() | ||
expect(personalCodeToBirthday('199001341234')).toBeNull() | ||
expect(personalCodeToBirthday('8112289875')).toBeNull() | ||
expect(personalCodeToBirthday('8115289871')).toBeNull() | ||
}) | ||
@@ -87,2 +97,4 @@ | ||
expect(personalCodeToBirthday('32129999999')).toBeNull() | ||
expect(personalCodeToBirthday('32132811465')).toBeNull() | ||
expect(personalCodeToBirthday('32112812600')).toBeNull() | ||
}) | ||
@@ -92,6 +104,8 @@ | ||
expect(personalCodeToBirthday('999999-1234')).toBeNull() | ||
expect(personalCodeToBirthday('051484-4569')).toBeNull() | ||
expect(personalCodeToBirthday('230309-3080')).toBeNull() | ||
}) | ||
test('Ukraine', () => { | ||
expect(personalCodeToBirthday('3406105373')).toBeNull() | ||
expect(personalCodeToBirthday('3416105373')).toBeNull() | ||
}) | ||
@@ -98,0 +112,0 @@ |
@@ -12,4 +12,6 @@ import denmarkParser from './parsers/denmark' | ||
export default function personalCodeToBirthday(code: string): ParsedDate | null { | ||
code = code.replace(' ', '') | ||
if (/^\d{10}$/.test(code)) { | ||
return ukraineParser(code) | ||
return ukraineParser(code) || swedenParser(code) | ||
} else if (/^\d{11}$/.test(code)) { | ||
@@ -23,4 +25,8 @@ return estoniaLithuaniaParser(code) || polandParser(code) || norwayParser(code) | ||
return finlandParser(code) | ||
} else if (/^\d{6}[+-A]\d{4}$/.test(code)) { | ||
return finlandParser(code) || denmarkParser(code) | ||
} else if (/^\d{6}[-]\d{4}$/.test(code)) { | ||
return finlandParser(code) || swedenParser(code) || denmarkParser(code) | ||
} else if (/^\d{6}[A]\d{4}$/.test(code)) { | ||
return finlandParser(code) | ||
} else if (/^\d{8}[-]\d{4}$/.test(code)) { | ||
return swedenParser(code) | ||
} | ||
@@ -27,0 +33,0 @@ |
@@ -5,5 +5,3 @@ import { ParsedDate } from '../types' | ||
export default function denmarkParser(code: string): ParsedDate | null { | ||
if (code.includes('-')) { | ||
code = code.replace('-', '') | ||
} | ||
code = code.replace('-', '') | ||
@@ -26,2 +24,8 @@ const day = parseInt(code.slice(0, 2), 10) | ||
// Checksum was dropped in 2007 | ||
// https://en.wikipedia.org/wiki/Personal_identification_number_(Denmark)#New_development_in_2007 | ||
if (year < 2007 && !isValidChecksum(code)) { | ||
return null | ||
} | ||
if (!isValidDate(day, month, year)) { | ||
@@ -33,1 +37,21 @@ return null | ||
} | ||
function isValidChecksum(code: string): boolean { | ||
const weights = [4, 3, 2, 7, 6, 5, 4, 3, 2] // Weights for the first 9 digits | ||
// Convert the first 9 digits to an array of numbers | ||
const digits = code.substring(0, 9).split('').map(Number) | ||
// Calculate the weighted sum | ||
const sum = digits.reduce((acc, digit, index) => acc + digit * weights[index], 0) | ||
// Calculate the check digit (modulo 11) | ||
const checksum = sum % 11 === 0 ? 0 : 11 - (sum % 11) | ||
// If the modulo result is 10, it's considered invalid (CPR could not use this number) | ||
if (checksum === 10) { | ||
return false | ||
} | ||
return checksum === parseInt(code[9], 10) | ||
} |
@@ -17,5 +17,3 @@ import { ParsedDate } from '../types' | ||
// Handle the century based on the marker | ||
if (centuryMarker === '+') { | ||
year += 1800 | ||
} else if (centuryMarker === '-') { | ||
if (centuryMarker === '-') { | ||
year += 1900 | ||
@@ -26,3 +24,2 @@ } else if (centuryMarker === 'A') { | ||
// Validate the parsed date | ||
if (!isValidDate(day, month, year)) { | ||
@@ -29,0 +26,0 @@ return null |
@@ -6,9 +6,14 @@ import { ParsedDate } from '../types' | ||
export default function latviaParser(code: string): ParsedDate | null { | ||
code = code.replace('-', '') | ||
if (!isValidChecksum(code)) { | ||
return null | ||
} | ||
const day = parseInt(code.slice(0, 2), 10) | ||
const month = parseInt(code.slice(2, 4), 10) | ||
let year = parseInt(code.slice(4, 6), 10) | ||
const centuryCode = parseInt(code[7], 10) | ||
const centuryCode = parseInt(code[6], 10) | ||
// Latvian codes are always from the 20th century, so add 1900 | ||
if (centuryCode === 2) { | ||
if (centuryCode === 3) { | ||
year += 2000 | ||
@@ -19,3 +24,2 @@ } else { | ||
// Validate the parsed date | ||
if (!isValidDate(day, month, year)) { | ||
@@ -27,1 +31,21 @@ return null | ||
} | ||
// Function to validate the check digit of a Latvian personal code | ||
function isValidChecksum(personalCode: string): boolean { | ||
const weights = [1, 6, 3, 7, 9, 10, 5, 8, 4, 2] | ||
// Convert the first 10 digits to an array of numbers | ||
const digits = personalCode.substring(0, 10).split('').map(Number) | ||
// Calculate the weighted sum | ||
const sum = digits.reduce((acc, digit, index) => acc + digit * weights[index], 0) | ||
// Calculate the check digit | ||
let checksum = sum % 11 | ||
if (checksum === 10) { | ||
checksum = 0 // If modulo result is 10, check digit is 0 | ||
} | ||
// Return true if the calculated check digit matches the provided check digit | ||
return checksum === parseInt(personalCode[10], 10) | ||
} |
@@ -5,2 +5,6 @@ import { ParsedDate } from '../types' | ||
export default function norwayParser(code: string): ParsedDate | null { | ||
if (!isValidChecksum(code)) { | ||
return null | ||
} | ||
const day = parseInt(code.slice(0, 2), 10) | ||
@@ -10,12 +14,11 @@ const month = parseInt(code.slice(2, 4), 10) | ||
const individual = parseInt(code.slice(6, 9), 10) | ||
const centuryMarker = parseInt(code.slice(6, 7), 10) | ||
// Determine century based on individual number and year | ||
if (individual <= 499 || (individual >= 500 && year >= 40)) { | ||
year += 1900 | ||
} else if (individual <= 999 && year <= 39) { | ||
// Read more https://en.wikipedia.org/wiki/National_identity_number_(Norway)#Numerical_components | ||
if (centuryMarker <= 4 || centuryMarker === 9) { | ||
year += year >= 40 ? 1900 : 2000 | ||
} else if (centuryMarker >= 5 && centuryMarker <= 8) { | ||
year += 2000 | ||
} | ||
// Now validate the date after adjusting the century | ||
if (!isValidDate(day, month, year)) { | ||
@@ -27,1 +30,39 @@ return null | ||
} | ||
// Function to validate the check digits of a Norwegian personal code | ||
function isValidChecksum(code: string): boolean { | ||
const weights1 = [3, 7, 6, 1, 8, 9, 4, 5, 2] // Weights for the first checksum | ||
const weights2 = [5, 4, 3, 2, 7, 6, 5, 4, 3, 2] // Weights for the second checksum, including first checksum | ||
// Extract the first 9 digits for checksum calculation | ||
const digits = code.substring(0, 9).split('').map(Number) | ||
// Extract the check digits provided in the input | ||
const providedFirstCheckDigit = parseInt(code[9], 10) | ||
const providedSecondCheckDigit = parseInt(code[10], 10) | ||
// First checksum calculation | ||
const sum1 = digits.reduce((sum, digit, index) => sum + digit * weights1[index], 0) | ||
let firstCheckDigit = 11 - (sum1 % 11) | ||
if (firstCheckDigit === 11) { | ||
firstCheckDigit = 0 // Handle special case where remainder is 11 | ||
} | ||
if (firstCheckDigit === 10) { | ||
return false // Invalid first check digit | ||
} | ||
// Add the first check digit for the second checksum calculation | ||
const digitsWithFirstCheck = [...digits, firstCheckDigit] | ||
// Second checksum calculation | ||
const sum2 = digitsWithFirstCheck.reduce((sum, digit, index) => sum + digit * weights2[index], 0) | ||
let secondCheckDigit = 11 - (sum2 % 11) | ||
if (secondCheckDigit === 11) { | ||
secondCheckDigit = 0 // Handle special case where remainder is 11 | ||
} | ||
if (secondCheckDigit === 10) { | ||
return false // Invalid second check digit | ||
} | ||
return firstCheckDigit === providedFirstCheckDigit && secondCheckDigit === providedSecondCheckDigit | ||
} |
@@ -5,6 +5,28 @@ import { ParsedDate } from '../types' | ||
export default function swedenParser(code: string): ParsedDate | null { | ||
const year = parseInt(code.slice(0, 4), 10) | ||
const month = parseInt(code.slice(4, 6), 10) | ||
const day = parseInt(code.slice(6, 8), 10) | ||
code = code.replace('-', '') | ||
if (!isValidChecksum(code)) { | ||
return null | ||
} | ||
let year: number | ||
let month: number | ||
let day: number | ||
if (code.length === 12) { | ||
year = parseInt(code.slice(0, 4), 10) | ||
month = parseInt(code.slice(4, 6), 10) | ||
day = parseInt(code.slice(6, 8), 10) | ||
} else { | ||
year = parseInt(code.slice(0, 2), 10) | ||
month = parseInt(code.slice(2, 4), 10) | ||
day = parseInt(code.slice(4, 6), 10) | ||
if (new Date().getFullYear() - (1900 + year) > 99) { | ||
year += 2000 | ||
} else { | ||
year += 1900 | ||
} | ||
} | ||
if (!isValidDate(day, month, year)) { | ||
@@ -16,1 +38,24 @@ return null | ||
} | ||
function isValidChecksum(code: string) { | ||
// Use the last 9 digits without checksum number | ||
const digits = code.length === 12 ? code.slice(2, 11) : code.slice(0, 9) | ||
let sum = 0 | ||
for (let i = 0; i < digits.length; i++) { | ||
let digit = parseInt(digits[i]) | ||
if (i % 2 === 0) { | ||
digit *= 2 | ||
if (digit > 9) { | ||
digit -= 9 | ||
} | ||
} | ||
sum += digit | ||
} | ||
const expectedChecksum = (10 - (sum % 10)) % 10 | ||
return expectedChecksum === Number(code[code.length - 1]) | ||
} |
import { ParsedDate } from '../types' | ||
import { isValidDate } from '../utils' | ||
@@ -19,2 +20,6 @@ export default function ukrainianParser(code: string): ParsedDate | null { | ||
if (!isValidDate(day, month, year)) { | ||
return null | ||
} | ||
return { day, month, year } | ||
@@ -21,0 +26,0 @@ } |
export function isValidDate(day: number, month: number, year: number): boolean { | ||
const date = new Date(year, month - 1, day) // month is zero-indexed in JS Date | ||
if (year < 1900 || year > new Date().getFullYear()) { | ||
return false | ||
} | ||
return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
21691
463
0
17