@remato/personal-code-to-birthday 1.0.0 β 1.1.0
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 +5 -5
- package/package.json +4 -4
- package/{index.test.ts β src/index.test.ts} +33 -12
- package/src/index.ts +27 -0
- package/src/parsers/denmark.ts +30 -0
- package/src/parsers/estoniaLithuania.ts +52 -0
- package/src/parsers/finland.ts +43 -0
- package/src/parsers/latvia.ts +24 -0
- package/src/parsers/norway.ts +24 -0
- package/src/parsers/poland.ts +37 -0
- package/src/parsers/sweden.ts +14 -0
- package/src/parsers/ukraine.ts +35 -0
- package/src/types.ts +5 -0
- package/src/utils.ts +5 -0
- package/tsconfig.json +1 -1
- package/index.ts +0 -192
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
|
-
-
|
|
46
|
-
- Azerbaijan
|
|
47
|
+
- South Africa
|
|
47
48
|
- Russia
|
|
48
|
-
- Ukraine
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remato/personal-code-to-birthday",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
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
7
|
"scripts": {
|
|
8
|
-
"build": "ncc build index.ts",
|
|
8
|
+
"build": "ncc build src/index.ts",
|
|
9
9
|
"test": "jest",
|
|
10
10
|
"prettier": "prettier --list-different \"**/*.ts\"",
|
|
11
11
|
"lint": "eslint \"**/*.ts\""
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"collectCoverage": true,
|
|
42
42
|
"collectCoverageFrom": [
|
|
43
|
-
"
|
|
43
|
+
"src/**"
|
|
44
44
|
]
|
|
45
45
|
},
|
|
46
46
|
"eslintConfig": {
|
|
@@ -2,8 +2,10 @@ import personalCodeToBirthday from './index'
|
|
|
2
2
|
|
|
3
3
|
describe('valid codes', () => {
|
|
4
4
|
test('Estonia', () => {
|
|
5
|
-
expect(personalCodeToBirthday('
|
|
6
|
-
expect(personalCodeToBirthday('
|
|
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 })
|
|
7
9
|
})
|
|
8
10
|
|
|
9
11
|
test('Latvia', () => {
|
|
@@ -12,7 +14,7 @@ describe('valid codes', () => {
|
|
|
12
14
|
})
|
|
13
15
|
|
|
14
16
|
test('Lithuania', () => {
|
|
15
|
-
expect(personalCodeToBirthday('
|
|
17
|
+
expect(personalCodeToBirthday('39905280216')).toEqual({ day: 28, month: 5, year: 1999 })
|
|
16
18
|
})
|
|
17
19
|
|
|
18
20
|
test('Finland', () => {
|
|
@@ -31,17 +33,27 @@ describe('valid codes', () => {
|
|
|
31
33
|
})
|
|
32
34
|
|
|
33
35
|
test('Denmark', () => {
|
|
34
|
-
expect(personalCodeToBirthday('
|
|
35
|
-
expect(personalCodeToBirthday('0503841235')).toEqual({ day: 5, month: 3, year: 1984 })
|
|
36
|
+
expect(personalCodeToBirthday('050305-4567')).toEqual({ day: 5, month: 3, year: 2005 })
|
|
36
37
|
expect(personalCodeToBirthday('050384-1235')).toEqual({ day: 5, month: 3, year: 1984 })
|
|
37
|
-
expect(personalCodeToBirthday('
|
|
38
|
+
expect(personalCodeToBirthday('050384-4567')).toEqual({ day: 5, month: 3, year: 1984 })
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('Ukraine', () => {
|
|
42
|
+
expect(personalCodeToBirthday('3406105372')).toEqual({ day: 3, month: 4, year: 1993 })
|
|
43
|
+
expect(personalCodeToBirthday('3406105350')).toEqual({ day: 3, month: 4, year: 1993 })
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('Poland', () => {
|
|
47
|
+
expect(personalCodeToBirthday('44051401359')).toEqual({ day: 14, month: 5, year: 1944 })
|
|
48
|
+
expect(personalCodeToBirthday('02070803628')).toEqual({ day: 8, month: 7, year: 1902 })
|
|
49
|
+
expect(personalCodeToBirthday('24231012344')).toEqual({ day: 10, month: 3, year: 2024 })
|
|
38
50
|
})
|
|
39
51
|
})
|
|
40
52
|
|
|
41
53
|
describe('invalid personal code', () => {
|
|
42
54
|
test('Estonia', () => {
|
|
43
|
-
|
|
44
|
-
expect(
|
|
55
|
+
expect(personalCodeToBirthday('39911025555')).toBeNull()
|
|
56
|
+
expect(personalCodeToBirthday('39913035552')).toBeNull()
|
|
45
57
|
})
|
|
46
58
|
|
|
47
59
|
test('Latvia', () => {
|
|
@@ -56,6 +68,7 @@ describe('invalid personal code', () => {
|
|
|
56
68
|
expect(personalCodeToBirthday('999999A123T')).toBeNull()
|
|
57
69
|
expect(personalCodeToBirthday('131052Z3087')).toBeNull()
|
|
58
70
|
expect(personalCodeToBirthday('321052A3087')).toBeNull()
|
|
71
|
+
expect(personalCodeToBirthday('910100A123F')).toBeNull()
|
|
59
72
|
})
|
|
60
73
|
|
|
61
74
|
test('Sweden', () => {
|
|
@@ -69,11 +82,19 @@ describe('invalid personal code', () => {
|
|
|
69
82
|
})
|
|
70
83
|
|
|
71
84
|
test('Denmark', () => {
|
|
72
|
-
expect(personalCodeToBirthday('
|
|
85
|
+
expect(personalCodeToBirthday('999999-1234')).toBeNull()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('Ukraine', () => {
|
|
89
|
+
expect(personalCodeToBirthday('3406105373')).toBeNull()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('Poland', () => {
|
|
93
|
+
expect(personalCodeToBirthday('24231012343')).toBeNull()
|
|
94
|
+
expect(personalCodeToBirthday('24234012341')).toBeNull()
|
|
73
95
|
})
|
|
74
96
|
|
|
75
|
-
test('
|
|
76
|
-
|
|
77
|
-
expect(result).toBeNull()
|
|
97
|
+
test('unidentified personal code format', () => {
|
|
98
|
+
expect(personalCodeToBirthday('abcd1234')).toBeNull()
|
|
78
99
|
})
|
|
79
100
|
})
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
if (/^\d{10}$/.test(code)) {
|
|
13
|
+
return ukraineParser(code)
|
|
14
|
+
} else if (/^\d{11}$/.test(code)) {
|
|
15
|
+
return estoniaLithuaniaParser(code) || polandParser(code) || norwayParser(code)
|
|
16
|
+
} else if (/^\d{12}$/.test(code)) {
|
|
17
|
+
return swedenParser(code)
|
|
18
|
+
} else if (/^\d{6}-\d{5}$/.test(code)) {
|
|
19
|
+
return latviaParser(code)
|
|
20
|
+
} else if (/^\d{6}[+-A]\d{3}[A-Za-z]$/.test(code)) {
|
|
21
|
+
return finlandParser(code)
|
|
22
|
+
} else if (/^\d{6}[+-A]\d{4}$/.test(code)) {
|
|
23
|
+
return finlandParser(code) || denmarkParser(code)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ParsedDate } from '../types'
|
|
2
|
+
import { isValidDate } from '../utils'
|
|
3
|
+
|
|
4
|
+
export default function denmarkParser(code: string): ParsedDate | null {
|
|
5
|
+
if (code.includes('-')) {
|
|
6
|
+
code = code.replace('-', '')
|
|
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
|
+
const centuryIndicator = parseInt(code.slice(6, 7), 10)
|
|
13
|
+
|
|
14
|
+
// Determine century based on the 7th digit and the year
|
|
15
|
+
if (centuryIndicator >= 0 && centuryIndicator <= 3) {
|
|
16
|
+
year += 1900
|
|
17
|
+
} else if (centuryIndicator >= 4 && centuryIndicator <= 9) {
|
|
18
|
+
if (year >= 0 && year <= 36) {
|
|
19
|
+
year += 2000
|
|
20
|
+
} else {
|
|
21
|
+
year += 1900
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!isValidDate(day, month, year)) {
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return { day, month, year }
|
|
30
|
+
}
|
|
@@ -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,43 @@
|
|
|
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 += 1800
|
|
19
|
+
} else if (centuryMarker === '-') {
|
|
20
|
+
year += 1900
|
|
21
|
+
} else if (centuryMarker === 'A') {
|
|
22
|
+
year += 2000
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Validate the parsed date
|
|
26
|
+
if (!isValidDate(day, month, year)) {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { day, month, year }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isValidChecksum(code: string): boolean {
|
|
34
|
+
const validChars = '0123456789ABCDEFHJKLMNPRSTUVWXY'
|
|
35
|
+
|
|
36
|
+
const serial = code.slice(0, 6) + code.slice(7, 10)
|
|
37
|
+
const checksum = code[10]
|
|
38
|
+
const num = parseInt(serial, 10)
|
|
39
|
+
const remainder = num % 31
|
|
40
|
+
const expectedChecksum = validChars.charAt(remainder)
|
|
41
|
+
|
|
42
|
+
return expectedChecksum === checksum.toUpperCase()
|
|
43
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
const day = parseInt(code.slice(0, 2), 10)
|
|
7
|
+
const month = parseInt(code.slice(2, 4), 10)
|
|
8
|
+
let year = parseInt(code.slice(4, 6), 10)
|
|
9
|
+
const centuryCode = parseInt(code[7], 10)
|
|
10
|
+
|
|
11
|
+
// Latvian codes are always from the 20th century, so add 1900
|
|
12
|
+
if (centuryCode === 2) {
|
|
13
|
+
year += 2000
|
|
14
|
+
} else {
|
|
15
|
+
year += 1900
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Validate the parsed date
|
|
19
|
+
if (!isValidDate(day, month, year)) {
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return { day, month, year }
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ParsedDate } from '../types'
|
|
2
|
+
import { isValidDate } from '../utils'
|
|
3
|
+
|
|
4
|
+
export default function norwayParser(code: string): ParsedDate | null {
|
|
5
|
+
const day = parseInt(code.slice(0, 2), 10)
|
|
6
|
+
const month = parseInt(code.slice(2, 4), 10)
|
|
7
|
+
let year = parseInt(code.slice(4, 6), 10)
|
|
8
|
+
|
|
9
|
+
const individual = parseInt(code.slice(6, 9), 10)
|
|
10
|
+
|
|
11
|
+
// Determine century based on individual number and year
|
|
12
|
+
if (individual <= 499 || (individual >= 500 && year >= 40)) {
|
|
13
|
+
year += 1900
|
|
14
|
+
} else if (individual <= 999 && year <= 39) {
|
|
15
|
+
year += 2000
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Now validate the date after adjusting the century
|
|
19
|
+
if (!isValidDate(day, month, year)) {
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return { day, month, year }
|
|
24
|
+
}
|
|
@@ -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,14 @@
|
|
|
1
|
+
import { ParsedDate } from '../types'
|
|
2
|
+
import { isValidDate } from '../utils'
|
|
3
|
+
|
|
4
|
+
export default function swedenParser(code: string): ParsedDate | null {
|
|
5
|
+
const year = parseInt(code.slice(0, 4), 10)
|
|
6
|
+
const month = parseInt(code.slice(4, 6), 10)
|
|
7
|
+
const day = parseInt(code.slice(6, 8), 10)
|
|
8
|
+
|
|
9
|
+
if (!isValidDate(day, month, year)) {
|
|
10
|
+
return null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return { day, month, year }
|
|
14
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ParsedDate } from '../types'
|
|
2
|
+
|
|
3
|
+
export default function ukrainianParser(code: string): ParsedDate | null {
|
|
4
|
+
if (!isValidChecksum(code)) {
|
|
5
|
+
return null
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// The first 5 digits represent the number of days since 31-12-1899
|
|
9
|
+
const daysSinceBaseDate = parseInt(code.slice(0, 5), 10)
|
|
10
|
+
const baseDate = new Date(1899, 11, 31) // Dec 31, 1899
|
|
11
|
+
|
|
12
|
+
// Calculate the birthdate from the number of days since the base date
|
|
13
|
+
const birthDate = new Date(baseDate.getTime() + daysSinceBaseDate * 24 * 60 * 60 * 1000)
|
|
14
|
+
|
|
15
|
+
const day = birthDate.getDate()
|
|
16
|
+
const month = birthDate.getMonth() + 1 // getMonth is zero-based
|
|
17
|
+
const year = birthDate.getFullYear()
|
|
18
|
+
|
|
19
|
+
return { day, month, year }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isValidChecksum(code: string): boolean {
|
|
23
|
+
const digits = code.slice(0, 9).split('').map(Number)
|
|
24
|
+
|
|
25
|
+
const weights = [-1, 5, 7, 9, 4, 6, 10, 5, 7]
|
|
26
|
+
const controlSum = digits.reduce((sum, digit, index) => sum + digit * weights[index], 0)
|
|
27
|
+
|
|
28
|
+
let controlNumber = controlSum % 11
|
|
29
|
+
|
|
30
|
+
if (controlNumber === 10) {
|
|
31
|
+
controlNumber = 0
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return controlNumber === parseInt(code[9], 10)
|
|
35
|
+
}
|
package/src/types.ts
ADDED
package/src/utils.ts
ADDED
package/tsconfig.json
CHANGED
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
|
-
}
|