@remato/personal-code-to-birthday 1.0.0 β†’ 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,21 +28,21 @@ console.log(birthday) // outputs { day: 26, month: 9, year: 1993 }
28
28
  - πŸ‡ΈπŸ‡ͺ Sweden
29
29
  - πŸ‡³πŸ‡΄ Norway
30
30
  - πŸ‡©πŸ‡° Denmark
31
+ - πŸ‡ΊπŸ‡¦ Ukraine
32
+ - πŸ‡΅πŸ‡± Poland
31
33
 
32
34
  Countries we would like to support (PRs are welcome):
33
35
 
34
36
  - Iceland
35
- - South Africa
36
37
  - Romania
37
38
  - Hungary
38
39
  - Bulgaria
40
+ - Moldova
41
+ - Azerbaijan
39
42
  - North Macedonia
40
43
  - Czech Republic
41
44
  - Slovakia
42
- - Poland
43
45
  - South Korea
44
46
  - Turkey
45
- - Moldova
46
- - Azerbaijan
47
+ - South Africa
47
48
  - Russia
48
- - Ukraine
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@remato/personal-code-to-birthday",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Converts personal identification codes from various countries into birthdate",
5
5
  "license": "MIT",
6
- "main": "index.ts",
6
+ "main": "src/index.ts",
7
+ "repository": {
8
+ "url": "https://github.com/rematocorp/personal-code-to-birthday"
9
+ },
7
10
  "scripts": {
8
- "build": "ncc build index.ts",
11
+ "build": "ncc build src/index.ts",
9
12
  "test": "jest",
10
13
  "prettier": "prettier --list-different \"**/*.ts\"",
11
14
  "lint": "eslint \"**/*.ts\""
@@ -40,12 +43,13 @@
40
43
  },
41
44
  "collectCoverage": true,
42
45
  "collectCoverageFrom": [
43
- "index.ts"
46
+ "src/**"
44
47
  ]
45
48
  },
46
49
  "eslintConfig": {
47
50
  "extends": [
48
51
  "@remato/eslint-config/typescript"
49
52
  ]
50
- }
53
+ },
54
+ "prettier": "@remato/prettier-config"
51
55
  }
@@ -0,0 +1,114 @@
1
+ import personalCodeToBirthday from './index'
2
+
3
+ describe('valid codes', () => {
4
+ test('Estonia', () => {
5
+ expect(personalCodeToBirthday('39309262754')).toEqual({ day: 26, month: 9, year: 1993 })
6
+ expect(personalCodeToBirthday('49111012217')).toEqual({ day: 1, month: 11, year: 1991 })
7
+ expect(personalCodeToBirthday('30001010060')).toEqual({ day: 1, month: 1, year: 1900 })
8
+ expect(personalCodeToBirthday('62305060020')).toEqual({ day: 6, month: 5, year: 2023 })
9
+ })
10
+
11
+ test('Latvia', () => {
12
+ expect(personalCodeToBirthday('240860-28074')).toEqual({ day: 24, month: 8, year: 1960 })
13
+ expect(personalCodeToBirthday('071023-38935')).toEqual({ day: 7, month: 10, year: 2023 })
14
+ expect(personalCodeToBirthday('240860-20090')).toEqual({ day: 24, month: 8, year: 1960 })
15
+ })
16
+
17
+ test('Lithuania', () => {
18
+ expect(personalCodeToBirthday('39905280216')).toEqual({ day: 28, month: 5, year: 1999 })
19
+ })
20
+
21
+ test('Finland', () => {
22
+ expect(personalCodeToBirthday('101085-7001')).toEqual({ day: 10, month: 10, year: 1985 })
23
+ expect(personalCodeToBirthday('150752-308N')).toEqual({ day: 15, month: 7, year: 1952 })
24
+ expect(personalCodeToBirthday('010100A123D')).toEqual({ day: 1, month: 1, year: 2000 })
25
+ })
26
+
27
+ test('Sweden', () => {
28
+ expect(personalCodeToBirthday('8112289874')).toEqual({ day: 28, month: 12, year: 1981 })
29
+ expect(personalCodeToBirthday('811228-9874')).toEqual({ day: 28, month: 12, year: 1981 })
30
+ expect(personalCodeToBirthday('670919-9530')).toEqual({ day: 19, month: 9, year: 1967 })
31
+ expect(personalCodeToBirthday('230919-9533')).toEqual({ day: 19, month: 9, year: 2023 })
32
+ expect(personalCodeToBirthday('196709199530')).toEqual({ day: 19, month: 9, year: 1967 })
33
+ expect(personalCodeToBirthday('19670919-9530')).toEqual({ day: 19, month: 9, year: 1967 })
34
+ expect(personalCodeToBirthday('20230919-9533')).toEqual({ day: 19, month: 9, year: 2023 })
35
+ })
36
+
37
+ test('Norway', () => {
38
+ expect(personalCodeToBirthday('10021559844')).toEqual({ day: 10, month: 2, year: 2015 })
39
+ expect(personalCodeToBirthday('100215 59844')).toEqual({ day: 10, month: 2, year: 2015 })
40
+ expect(personalCodeToBirthday('10028439895')).toEqual({ day: 10, month: 2, year: 1984 })
41
+ expect(personalCodeToBirthday('27081439368')).toEqual({ day: 27, month: 8, year: 2014 })
42
+ })
43
+
44
+ test('Denmark', () => {
45
+ expect(personalCodeToBirthday('070589-8901')).toEqual({ day: 7, month: 5, year: 1989 })
46
+ expect(personalCodeToBirthday('230309-4456')).toEqual({ day: 23, month: 3, year: 2009 })
47
+ expect(personalCodeToBirthday('050384-4567')).toEqual({ day: 5, month: 3, year: 1984 })
48
+ expect(personalCodeToBirthday('230309-3020')).toEqual({ day: 23, month: 3, year: 1909 })
49
+ })
50
+
51
+ test('Ukraine', () => {
52
+ expect(personalCodeToBirthday('3406105372')).toEqual({ day: 3, month: 4, year: 1993 })
53
+ expect(personalCodeToBirthday('3406105350')).toEqual({ day: 3, month: 4, year: 1993 })
54
+ })
55
+
56
+ test('Poland', () => {
57
+ expect(personalCodeToBirthday('44051401359')).toEqual({ day: 14, month: 5, year: 1944 })
58
+ expect(personalCodeToBirthday('02070803628')).toEqual({ day: 8, month: 7, year: 1902 })
59
+ expect(personalCodeToBirthday('24231012344')).toEqual({ day: 10, month: 3, year: 2024 })
60
+ })
61
+ })
62
+
63
+ describe('invalid personal code', () => {
64
+ test('Estonia', () => {
65
+ expect(personalCodeToBirthday('39911025555')).toBeNull()
66
+ expect(personalCodeToBirthday('39913035552')).toBeNull()
67
+ })
68
+
69
+ test('Latvia', () => {
70
+ expect(personalCodeToBirthday('071023-38934')).toBeNull()
71
+ expect(personalCodeToBirthday('071523-38937')).toBeNull()
72
+ })
73
+
74
+ test('Lithuania', () => {
75
+ expect(personalCodeToBirthday('99905280217')).toBeNull()
76
+ })
77
+
78
+ test('Finland', () => {
79
+ expect(personalCodeToBirthday('999999A123T')).toBeNull()
80
+ expect(personalCodeToBirthday('131052Z3087')).toBeNull()
81
+ expect(personalCodeToBirthday('321052A3087')).toBeNull()
82
+ expect(personalCodeToBirthday('910100A123F')).toBeNull()
83
+ })
84
+
85
+ test('Sweden', () => {
86
+ expect(personalCodeToBirthday('8112289875')).toBeNull()
87
+ expect(personalCodeToBirthday('8115289871')).toBeNull()
88
+ })
89
+
90
+ test('Norway', () => {
91
+ expect(personalCodeToBirthday('32129999999')).toBeNull()
92
+ expect(personalCodeToBirthday('32132811465')).toBeNull()
93
+ expect(personalCodeToBirthday('32112812600')).toBeNull()
94
+ })
95
+
96
+ test('Denmark', () => {
97
+ expect(personalCodeToBirthday('999999-1234')).toBeNull()
98
+ expect(personalCodeToBirthday('051484-4569')).toBeNull()
99
+ expect(personalCodeToBirthday('230309-3080')).toBeNull()
100
+ })
101
+
102
+ test('Ukraine', () => {
103
+ expect(personalCodeToBirthday('3416105373')).toBeNull()
104
+ })
105
+
106
+ test('Poland', () => {
107
+ expect(personalCodeToBirthday('24231012343')).toBeNull()
108
+ expect(personalCodeToBirthday('24234012341')).toBeNull()
109
+ })
110
+
111
+ test('unidentified personal code format', () => {
112
+ expect(personalCodeToBirthday('abcd1234')).toBeNull()
113
+ })
114
+ })
package/src/index.ts ADDED
@@ -0,0 +1,33 @@
1
+ import denmarkParser from './parsers/denmark'
2
+ import estoniaLithuaniaParser from './parsers/estoniaLithuania'
3
+ import finlandParser from './parsers/finland'
4
+ import latviaParser from './parsers/latvia'
5
+ import norwayParser from './parsers/norway'
6
+ import polandParser from './parsers/poland'
7
+ import swedenParser from './parsers/sweden'
8
+ import ukraineParser from './parsers/ukraine'
9
+ import { ParsedDate } from './types'
10
+
11
+ export default function personalCodeToBirthday(code: string): ParsedDate | null {
12
+ code = code.replace(' ', '')
13
+
14
+ if (/^\d{10}$/.test(code)) {
15
+ return ukraineParser(code) || swedenParser(code)
16
+ } else if (/^\d{11}$/.test(code)) {
17
+ return estoniaLithuaniaParser(code) || polandParser(code) || norwayParser(code)
18
+ } else if (/^\d{12}$/.test(code)) {
19
+ return swedenParser(code)
20
+ } else if (/^\d{6}-\d{5}$/.test(code)) {
21
+ return latviaParser(code)
22
+ } else if (/^\d{6}[+-A]\d{3}[A-Za-z]$/.test(code)) {
23
+ return finlandParser(code)
24
+ } else if (/^\d{6}[-]\d{4}$/.test(code)) {
25
+ return finlandParser(code) || swedenParser(code) || denmarkParser(code)
26
+ } else if (/^\d{6}[A]\d{4}$/.test(code)) {
27
+ return finlandParser(code)
28
+ } else if (/^\d{8}[-]\d{4}$/.test(code)) {
29
+ return swedenParser(code)
30
+ }
31
+
32
+ return null
33
+ }
@@ -0,0 +1,54 @@
1
+ import { ParsedDate } from '../types'
2
+ import { isValidDate } from '../utils'
3
+
4
+ export default function denmarkParser(code: string): ParsedDate | null {
5
+ code = code.replace('-', '')
6
+
7
+ const day = parseInt(code.slice(0, 2), 10)
8
+ const month = parseInt(code.slice(2, 4), 10)
9
+ let year = parseInt(code.slice(4, 6), 10)
10
+ const centuryIndicator = parseInt(code.slice(6, 7), 10)
11
+
12
+ // Determine century based on the 7th digit and the year
13
+ if (centuryIndicator >= 0 && centuryIndicator <= 3) {
14
+ year += 1900
15
+ } else if (centuryIndicator >= 4 && centuryIndicator <= 9) {
16
+ if (year >= 0 && year <= 36) {
17
+ year += 2000
18
+ } else {
19
+ year += 1900
20
+ }
21
+ }
22
+
23
+ // Checksum was dropped in 2007
24
+ // https://en.wikipedia.org/wiki/Personal_identification_number_(Denmark)#New_development_in_2007
25
+ if (year < 2007 && !isValidChecksum(code)) {
26
+ return null
27
+ }
28
+
29
+ if (!isValidDate(day, month, year)) {
30
+ return null
31
+ }
32
+
33
+ return { day, month, year }
34
+ }
35
+
36
+ function isValidChecksum(code: string): boolean {
37
+ const weights = [4, 3, 2, 7, 6, 5, 4, 3, 2] // Weights for the first 9 digits
38
+
39
+ // Convert the first 9 digits to an array of numbers
40
+ const digits = code.substring(0, 9).split('').map(Number)
41
+
42
+ // Calculate the weighted sum
43
+ const sum = digits.reduce((acc, digit, index) => acc + digit * weights[index], 0)
44
+
45
+ // Calculate the check digit (modulo 11)
46
+ const checksum = sum % 11 === 0 ? 0 : 11 - (sum % 11)
47
+
48
+ // If the modulo result is 10, it's considered invalid (CPR could not use this number)
49
+ if (checksum === 10) {
50
+ return false
51
+ }
52
+
53
+ return checksum === parseInt(code[9], 10)
54
+ }
@@ -0,0 +1,52 @@
1
+ import { ParsedDate } from '../types'
2
+ import { isValidDate } from '../utils'
3
+
4
+ export default function estoniaLithuaniaParser(code: string): ParsedDate | null {
5
+ if (!isValidChecksum(code)) {
6
+ return null
7
+ }
8
+
9
+ const centuryCode = parseInt(code[0], 10)
10
+ const day = parseInt(code.slice(5, 7), 10)
11
+ const month = parseInt(code.slice(3, 5), 10)
12
+ let year = parseInt(code.slice(1, 3), 10)
13
+
14
+ if (centuryCode === 3 || centuryCode === 4) {
15
+ year += 1900
16
+ }
17
+ if (centuryCode === 5 || centuryCode === 6) {
18
+ year += 2000
19
+ }
20
+
21
+ if (!isValidDate(day, month, year)) {
22
+ return null
23
+ }
24
+
25
+ return { day, month, year }
26
+ }
27
+
28
+ function isValidChecksum(code: string): boolean {
29
+ const weights1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 1]
30
+ const weights2 = [3, 4, 5, 6, 7, 8, 9, 1, 2, 3]
31
+ let checksum = 0
32
+ let total = 0
33
+
34
+ for (let i = 0; i < 10; ++i) {
35
+ total += Number(code.charAt(i)) * weights1[i]
36
+ }
37
+ checksum = total % 11
38
+
39
+ total = 0
40
+ if (checksum === 10) {
41
+ for (let i = 0; i < 10; ++i) {
42
+ total += Number(code.charAt(i)) * weights2[i]
43
+ }
44
+ checksum = total % 11
45
+
46
+ if (10 === checksum) {
47
+ checksum = 0
48
+ }
49
+ }
50
+
51
+ return checksum === Number(code[10])
52
+ }
@@ -0,0 +1,40 @@
1
+ import { ParsedDate } from '../types'
2
+ import { isValidDate } from '../utils'
3
+
4
+ export default function finlandParser(code: string): ParsedDate | null {
5
+ if (!isValidChecksum(code)) {
6
+ return null
7
+ }
8
+
9
+ const day = parseInt(code.slice(0, 2), 10)
10
+ const month = parseInt(code.slice(2, 4), 10)
11
+ const yearSuffix = parseInt(code.slice(4, 6), 10)
12
+ const centuryMarker = code[6]
13
+
14
+ let year = yearSuffix
15
+
16
+ // Handle the century based on the marker
17
+ if (centuryMarker === '-') {
18
+ year += 1900
19
+ } else if (centuryMarker === 'A') {
20
+ year += 2000
21
+ }
22
+
23
+ if (!isValidDate(day, month, year)) {
24
+ return null
25
+ }
26
+
27
+ return { day, month, year }
28
+ }
29
+
30
+ function isValidChecksum(code: string): boolean {
31
+ const validChars = '0123456789ABCDEFHJKLMNPRSTUVWXY'
32
+
33
+ const serial = code.slice(0, 6) + code.slice(7, 10)
34
+ const checksum = code[10]
35
+ const num = parseInt(serial, 10)
36
+ const remainder = num % 31
37
+ const expectedChecksum = validChars.charAt(remainder)
38
+
39
+ return expectedChecksum === checksum.toUpperCase()
40
+ }
@@ -0,0 +1,48 @@
1
+ import { ParsedDate } from '../types'
2
+ import { isValidDate } from '../utils'
3
+
4
+ // Correct old school format is DDMMYY-CXXXX
5
+ export default function latviaParser(code: string): ParsedDate | null {
6
+ code = code.replace('-', '')
7
+
8
+ if (!isValidChecksum(code)) {
9
+ return null
10
+ }
11
+
12
+ const day = parseInt(code.slice(0, 2), 10)
13
+ const month = parseInt(code.slice(2, 4), 10)
14
+ let year = parseInt(code.slice(4, 6), 10)
15
+ const centuryCode = parseInt(code[6], 10)
16
+
17
+ if (centuryCode === 3) {
18
+ year += 2000
19
+ } else {
20
+ year += 1900
21
+ }
22
+
23
+ if (!isValidDate(day, month, year)) {
24
+ return null
25
+ }
26
+
27
+ return { day, month, year }
28
+ }
29
+
30
+ // Function to validate the check digit of a Latvian personal code
31
+ function isValidChecksum(personalCode: string): boolean {
32
+ const weights = [1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
33
+
34
+ // Convert the first 10 digits to an array of numbers
35
+ const digits = personalCode.substring(0, 10).split('').map(Number)
36
+
37
+ // Calculate the weighted sum
38
+ const sum = digits.reduce((acc, digit, index) => acc + digit * weights[index], 0)
39
+
40
+ // Calculate the check digit
41
+ let checksum = sum % 11
42
+ if (checksum === 10) {
43
+ checksum = 0 // If modulo result is 10, check digit is 0
44
+ }
45
+
46
+ // Return true if the calculated check digit matches the provided check digit
47
+ return checksum === parseInt(personalCode[10], 10)
48
+ }
@@ -0,0 +1,65 @@
1
+ import { ParsedDate } from '../types'
2
+ import { isValidDate } from '../utils'
3
+
4
+ export default function norwayParser(code: string): ParsedDate | null {
5
+ if (!isValidChecksum(code)) {
6
+ return null
7
+ }
8
+
9
+ const day = parseInt(code.slice(0, 2), 10)
10
+ const month = parseInt(code.slice(2, 4), 10)
11
+ let year = parseInt(code.slice(4, 6), 10)
12
+
13
+ const centuryMarker = parseInt(code.slice(6, 7), 10)
14
+
15
+ // Read more https://en.wikipedia.org/wiki/National_identity_number_(Norway)#Numerical_components
16
+ if (centuryMarker <= 4 || centuryMarker === 9) {
17
+ year += year >= 40 ? 1900 : 2000
18
+ } else if (centuryMarker >= 5 && centuryMarker <= 8) {
19
+ year += 2000
20
+ }
21
+
22
+ if (!isValidDate(day, month, year)) {
23
+ return null
24
+ }
25
+
26
+ return { day, month, year }
27
+ }
28
+
29
+ // Function to validate the check digits of a Norwegian personal code
30
+ function isValidChecksum(code: string): boolean {
31
+ const weights1 = [3, 7, 6, 1, 8, 9, 4, 5, 2] // Weights for the first checksum
32
+ const weights2 = [5, 4, 3, 2, 7, 6, 5, 4, 3, 2] // Weights for the second checksum, including first checksum
33
+
34
+ // Extract the first 9 digits for checksum calculation
35
+ const digits = code.substring(0, 9).split('').map(Number)
36
+
37
+ // Extract the check digits provided in the input
38
+ const providedFirstCheckDigit = parseInt(code[9], 10)
39
+ const providedSecondCheckDigit = parseInt(code[10], 10)
40
+
41
+ // First checksum calculation
42
+ const sum1 = digits.reduce((sum, digit, index) => sum + digit * weights1[index], 0)
43
+ let firstCheckDigit = 11 - (sum1 % 11)
44
+ if (firstCheckDigit === 11) {
45
+ firstCheckDigit = 0 // Handle special case where remainder is 11
46
+ }
47
+ if (firstCheckDigit === 10) {
48
+ return false // Invalid first check digit
49
+ }
50
+
51
+ // Add the first check digit for the second checksum calculation
52
+ const digitsWithFirstCheck = [...digits, firstCheckDigit]
53
+
54
+ // Second checksum calculation
55
+ const sum2 = digitsWithFirstCheck.reduce((sum, digit, index) => sum + digit * weights2[index], 0)
56
+ let secondCheckDigit = 11 - (sum2 % 11)
57
+ if (secondCheckDigit === 11) {
58
+ secondCheckDigit = 0 // Handle special case where remainder is 11
59
+ }
60
+ if (secondCheckDigit === 10) {
61
+ return false // Invalid second check digit
62
+ }
63
+
64
+ return firstCheckDigit === providedFirstCheckDigit && secondCheckDigit === providedSecondCheckDigit
65
+ }
@@ -0,0 +1,37 @@
1
+ import { ParsedDate } from '../types'
2
+ import { isValidDate } from '../utils'
3
+
4
+ export default function polandParser(code: string): ParsedDate | null {
5
+ if (!isValidChecksum(code)) {
6
+ return null
7
+ }
8
+
9
+ const day = parseInt(code.slice(4, 6), 10)
10
+ let month = parseInt(code.slice(2, 4), 10)
11
+ let year = parseInt(code.slice(0, 2), 10)
12
+
13
+ // Determine the century based on the month encoding
14
+ if (month >= 1 && month <= 12) {
15
+ year += 1900
16
+ } else if (month >= 21 && month <= 32) {
17
+ year += 2000
18
+ month -= 20
19
+ }
20
+
21
+ if (!isValidDate(day, month, year)) {
22
+ return null
23
+ }
24
+
25
+ return { day, month, year }
26
+ }
27
+
28
+ function isValidChecksum(code: string): boolean {
29
+ const weights = [1, 3, 7, 9, 1, 3, 7, 9, 1, 3]
30
+ const digits = code.slice(0, 10).split('').map(Number)
31
+
32
+ const sum = digits.reduce((acc, digit, index) => acc + digit * weights[index], 0)
33
+
34
+ const expectedChecksum = (10 - (sum % 10)) % 10
35
+
36
+ return expectedChecksum === parseInt(code[10], 10)
37
+ }
@@ -0,0 +1,59 @@
1
+ import { ParsedDate } from '../types'
2
+ import { isValidDate } from '../utils'
3
+
4
+ export default function swedenParser(code: string): ParsedDate | null {
5
+ code = code.replace('-', '')
6
+
7
+ if (!isValidChecksum(code)) {
8
+ return null
9
+ }
10
+
11
+ let year: number
12
+ let month: number
13
+ let day: number
14
+
15
+ if (code.length === 12) {
16
+ year = parseInt(code.slice(0, 4), 10)
17
+ month = parseInt(code.slice(4, 6), 10)
18
+ day = parseInt(code.slice(6, 8), 10)
19
+ } else {
20
+ year = parseInt(code.slice(0, 2), 10)
21
+ month = parseInt(code.slice(2, 4), 10)
22
+ day = parseInt(code.slice(4, 6), 10)
23
+
24
+ if (new Date().getFullYear() - (1900 + year) > 99) {
25
+ year += 2000
26
+ } else {
27
+ year += 1900
28
+ }
29
+ }
30
+
31
+ if (!isValidDate(day, month, year)) {
32
+ return null
33
+ }
34
+
35
+ return { day, month, year }
36
+ }
37
+
38
+ function isValidChecksum(code: string) {
39
+ // Use the last 9 digits without checksum number
40
+ const digits = code.length === 12 ? code.slice(2, 11) : code.slice(0, 9)
41
+
42
+ let sum = 0
43
+ for (let i = 0; i < digits.length; i++) {
44
+ let digit = parseInt(digits[i])
45
+
46
+ if (i % 2 === 0) {
47
+ digit *= 2
48
+
49
+ if (digit > 9) {
50
+ digit -= 9
51
+ }
52
+ }
53
+
54
+ sum += digit
55
+ }
56
+ const expectedChecksum = (10 - (sum % 10)) % 10
57
+
58
+ return expectedChecksum === Number(code[code.length - 1])
59
+ }
@@ -0,0 +1,40 @@
1
+ import { ParsedDate } from '../types'
2
+ import { isValidDate } from '../utils'
3
+
4
+ export default function ukrainianParser(code: string): ParsedDate | null {
5
+ if (!isValidChecksum(code)) {
6
+ return null
7
+ }
8
+
9
+ // The first 5 digits represent the number of days since 31-12-1899
10
+ const daysSinceBaseDate = parseInt(code.slice(0, 5), 10)
11
+ const baseDate = new Date(1899, 11, 31) // Dec 31, 1899
12
+
13
+ // Calculate the birthdate from the number of days since the base date
14
+ const birthDate = new Date(baseDate.getTime() + daysSinceBaseDate * 24 * 60 * 60 * 1000)
15
+
16
+ const day = birthDate.getDate()
17
+ const month = birthDate.getMonth() + 1 // getMonth is zero-based
18
+ const year = birthDate.getFullYear()
19
+
20
+ if (!isValidDate(day, month, year)) {
21
+ return null
22
+ }
23
+
24
+ return { day, month, year }
25
+ }
26
+
27
+ function isValidChecksum(code: string): boolean {
28
+ const digits = code.slice(0, 9).split('').map(Number)
29
+
30
+ const weights = [-1, 5, 7, 9, 4, 6, 10, 5, 7]
31
+ const controlSum = digits.reduce((sum, digit, index) => sum + digit * weights[index], 0)
32
+
33
+ let controlNumber = controlSum % 11
34
+
35
+ if (controlNumber === 10) {
36
+ controlNumber = 0
37
+ }
38
+
39
+ return controlNumber === parseInt(code[9], 10)
40
+ }
package/src/types.ts ADDED
@@ -0,0 +1,5 @@
1
+ export interface ParsedDate {
2
+ day: number
3
+ month: number
4
+ year: number
5
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,9 @@
1
+ export function isValidDate(day: number, month: number, year: number): boolean {
2
+ const date = new Date(year, month - 1, day) // month is zero-indexed in JS Date
3
+
4
+ if (year < 1900 || year > new Date().getFullYear()) {
5
+ return false
6
+ }
7
+
8
+ return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day
9
+ }
package/tsconfig.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "compilerOptions": {
4
4
  "target": "ES2022",
5
5
  "module": "NodeNext",
6
- "rootDir": "./",
6
+ "rootDir": "./src",
7
7
  "moduleResolution": "NodeNext",
8
8
  "baseUrl": "./",
9
9
  "sourceMap": true,
package/index.test.ts DELETED
@@ -1,79 +0,0 @@
1
- import personalCodeToBirthday from './index'
2
-
3
- describe('valid codes', () => {
4
- test('Estonia', () => {
5
- expect(personalCodeToBirthday('39405280299')).toEqual({ day: 28, month: 5, year: 1994 })
6
- expect(personalCodeToBirthday('60105280299')).toEqual({ day: 28, month: 5, year: 2001 })
7
- })
8
-
9
- test('Latvia', () => {
10
- expect(personalCodeToBirthday('050393-12344')).toEqual({ day: 5, month: 3, year: 1993 })
11
- expect(personalCodeToBirthday('050303-22344')).toEqual({ day: 5, month: 3, year: 2003 })
12
- })
13
-
14
- test('Lithuania', () => {
15
- expect(personalCodeToBirthday('39905280217')).toEqual({ day: 28, month: 5, year: 1999 })
16
- })
17
-
18
- test('Finland', () => {
19
- expect(personalCodeToBirthday('101085-7001')).toEqual({ day: 10, month: 10, year: 1985 })
20
- expect(personalCodeToBirthday('101085+789W')).toEqual({ day: 10, month: 10, year: 1885 })
21
- expect(personalCodeToBirthday('150752-308N')).toEqual({ day: 15, month: 7, year: 1952 })
22
- expect(personalCodeToBirthday('010100A123D')).toEqual({ day: 1, month: 1, year: 2000 })
23
- })
24
-
25
- test('Sweden', () => {
26
- expect(personalCodeToBirthday('199001011234')).toEqual({ day: 1, month: 1, year: 1990 })
27
- })
28
-
29
- test('Norway', () => {
30
- expect(personalCodeToBirthday('01020352345')).toEqual({ day: 1, month: 2, year: 2003 })
31
- })
32
-
33
- test('Denmark', () => {
34
- expect(personalCodeToBirthday('0503054567')).toEqual({ day: 5, month: 3, year: 2005 })
35
- expect(personalCodeToBirthday('0503841235')).toEqual({ day: 5, month: 3, year: 1984 })
36
- expect(personalCodeToBirthday('050384-1235')).toEqual({ day: 5, month: 3, year: 1984 })
37
- expect(personalCodeToBirthday('0503844567')).toEqual({ day: 5, month: 3, year: 1984 })
38
- })
39
- })
40
-
41
- describe('invalid personal code', () => {
42
- test('Estonia', () => {
43
- const result = personalCodeToBirthday('39913025555')
44
- expect(result).toBeNull()
45
- })
46
-
47
- test('Latvia', () => {
48
- expect(personalCodeToBirthday('123456-12345')).toBeNull()
49
- })
50
-
51
- test('Lithuania', () => {
52
- expect(personalCodeToBirthday('99905280217')).toBeNull()
53
- })
54
-
55
- test('Finland', () => {
56
- expect(personalCodeToBirthday('999999A123T')).toBeNull()
57
- expect(personalCodeToBirthday('131052Z3087')).toBeNull()
58
- expect(personalCodeToBirthday('321052A3087')).toBeNull()
59
- })
60
-
61
- test('Sweden', () => {
62
- expect(personalCodeToBirthday('1990010123456')).toBeNull()
63
- expect(personalCodeToBirthday('199013011234')).toBeNull()
64
- expect(personalCodeToBirthday('199001341234')).toBeNull()
65
- })
66
-
67
- test('Norway', () => {
68
- expect(personalCodeToBirthday('32129999999')).toBeNull()
69
- })
70
-
71
- test('Denmark', () => {
72
- expect(personalCodeToBirthday('9999991234')).toBeNull()
73
- })
74
-
75
- test('unsupported personal code format', () => {
76
- const result = personalCodeToBirthday('abcd1234')
77
- expect(result).toBeNull()
78
- })
79
- })
package/index.ts DELETED
@@ -1,192 +0,0 @@
1
- interface ParsedDate {
2
- day: number
3
- month: number
4
- year: number
5
- }
6
-
7
- export default function personalCodeToBirthday(code: string): ParsedDate | null {
8
- if (/^\d{11}$/.test(code)) {
9
- // Could be Estonia, Lithuania, or Norway
10
- const birthday = estonianLithuanianParser(code)
11
- if (birthday) {
12
- return birthday
13
- }
14
-
15
- return norwayParser(code)
16
- } else if (/^\d{6}-\d{5}$/.test(code)) {
17
- return latvianParser(code)
18
- } else if (/^\d{6}[+-A]\d{3}[A-Za-z]$/.test(code)) {
19
- return finlandParser(code)
20
- } else if (/^\d{6}[+-A]\d{4}$/.test(code)) {
21
- // Could be Finland or Denmark
22
- const birthday = finlandParser(code)
23
- if (birthday) {
24
- return birthday
25
- }
26
-
27
- return denmarkParser(code)
28
- } else if (/^\d{12}$/.test(code)) {
29
- return swedenParser(code)
30
- } else if (/^\d{10}$/.test(code)) {
31
- return denmarkParser(code)
32
- }
33
-
34
- return null
35
- }
36
-
37
- // Estonia (EE) and Lithuanian (LT) Parser
38
- function estonianLithuanianParser(code: string): ParsedDate | null {
39
- const centuryCode = parseInt(code[0], 10)
40
- const day = parseInt(code.slice(5, 7), 10)
41
- const month = parseInt(code.slice(3, 5), 10)
42
- let year = parseInt(code.slice(1, 3), 10)
43
-
44
- if (centuryCode === 3 || centuryCode === 4) {
45
- year += 1900
46
- }
47
- if (centuryCode === 5 || centuryCode === 6) {
48
- year += 2000
49
- }
50
-
51
- if (!isValidDate(day, month, year)) {
52
- return null
53
- }
54
-
55
- return { day, month, year }
56
- }
57
-
58
- // Latvia (LV) Parser
59
- // Correct old school format is DDMMYY-CXXXX
60
- function latvianParser(code: string): ParsedDate | null {
61
- const day = parseInt(code.slice(0, 2), 10)
62
- const month = parseInt(code.slice(2, 4), 10)
63
- let year = parseInt(code.slice(4, 6), 10)
64
- const centuryCode = parseInt(code[7], 10)
65
-
66
- // Latvian codes are always from the 20th century, so add 1900
67
- if (centuryCode === 2) {
68
- year += 2000
69
- } else {
70
- year += 1900
71
- }
72
-
73
- // Validate the parsed date
74
- if (!isValidDate(day, month, year)) {
75
- return null
76
- }
77
-
78
- return { day, month, year }
79
- }
80
-
81
- // Finland (FI) Parser
82
- function finlandParser(code: string): ParsedDate | null {
83
- const day = parseInt(code.slice(0, 2), 10)
84
- const month = parseInt(code.slice(2, 4), 10)
85
- const yearSuffix = parseInt(code.slice(4, 6), 10)
86
- const centuryMarker = code[6]
87
-
88
- let year = yearSuffix
89
-
90
- // Handle the century based on the marker
91
- if (centuryMarker === '+') {
92
- year += 1800
93
- } else if (centuryMarker === '-') {
94
- year += 1900
95
- } else if (centuryMarker === 'A') {
96
- year += 2000
97
- }
98
-
99
- // Validate the parsed date
100
- if (!isValidDate(day, month, year)) {
101
- return null
102
- }
103
-
104
- if (!isValidFinnishChecksum(code)) {
105
- return null
106
- }
107
-
108
- return { day, month, year }
109
- }
110
-
111
- function isValidFinnishChecksum(code: string): boolean {
112
- const validChars = '0123456789ABCDEFHJKLMNPRSTUVWXY'
113
-
114
- const serial = code.slice(0, 6) + code.slice(7, 10)
115
- const checksum = code[10]
116
- const num = parseInt(serial, 10)
117
- const remainder = num % 31
118
- const expectedChecksum = validChars.charAt(remainder)
119
-
120
- return expectedChecksum === checksum.toUpperCase()
121
- }
122
-
123
- // Sweden (SE) Parser
124
- function swedenParser(code: string): ParsedDate | null {
125
- const year = parseInt(code.slice(0, 4), 10)
126
- const month = parseInt(code.slice(4, 6), 10)
127
- const day = parseInt(code.slice(6, 8), 10)
128
-
129
- if (!isValidDate(day, month, year)) {
130
- return null
131
- }
132
-
133
- return { day, month, year }
134
- }
135
-
136
- // Norway (NO) Parser
137
- function norwayParser(code: string): ParsedDate | null {
138
- const day = parseInt(code.slice(0, 2), 10)
139
- const month = parseInt(code.slice(2, 4), 10)
140
- let year = parseInt(code.slice(4, 6), 10)
141
-
142
- const individual = parseInt(code.slice(6, 9), 10)
143
-
144
- // Determine century based on individual number and year
145
- if (individual <= 499 || (individual >= 500 && year >= 40)) {
146
- year += 1900
147
- } else if (individual <= 999 && year <= 39) {
148
- year += 2000
149
- }
150
-
151
- // Now validate the date after adjusting the century
152
- if (!isValidDate(day, month, year)) {
153
- return null
154
- }
155
-
156
- return { day, month, year }
157
- }
158
-
159
- // Denmark (DK) Parser
160
- function denmarkParser(code: string): ParsedDate | null {
161
- if (code.includes('-')) {
162
- code = code.replace('-', '')
163
- }
164
-
165
- const day = parseInt(code.slice(0, 2), 10)
166
- const month = parseInt(code.slice(2, 4), 10)
167
- let year = parseInt(code.slice(4, 6), 10)
168
- const centuryIndicator = parseInt(code.slice(6, 7), 10)
169
-
170
- // Determine century based on the 7th digit and the year
171
- if (centuryIndicator >= 0 && centuryIndicator <= 3) {
172
- year += 1900
173
- } else if (centuryIndicator >= 4 && centuryIndicator <= 9) {
174
- if (year >= 0 && year <= 36) {
175
- year += 2000
176
- } else {
177
- year += 1900
178
- }
179
- }
180
-
181
- if (!isValidDate(day, month, year)) {
182
- return null
183
- }
184
-
185
- return { day, month, year }
186
- }
187
-
188
- function isValidDate(day: number, month: number, year: number): boolean {
189
- const date = new Date(year, month - 1, day) // month is zero-indexed in JS Date
190
-
191
- return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day
192
- }
@@ -1 +0,0 @@
1
- module.exports = require("@remato/prettier-config");