@remato/personal-code-to-birthday 1.1.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/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "@remato/personal-code-to-birthday",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Converts personal identification codes from various countries into birthdate",
5
5
  "license": "MIT",
6
6
  "main": "src/index.ts",
7
+ "repository": {
8
+ "url": "https://github.com/rematocorp/personal-code-to-birthday"
9
+ },
7
10
  "scripts": {
8
11
  "build": "ncc build src/index.ts",
9
12
  "test": "jest",
@@ -47,5 +50,6 @@
47
50
  "extends": [
48
51
  "@remato/eslint-config/typescript"
49
52
  ]
50
- }
53
+ },
54
+ "prettier": "@remato/prettier-config"
51
55
  }
package/src/index.test.ts CHANGED
@@ -9,8 +9,9 @@ describe('valid codes', () => {
9
9
  })
10
10
 
11
11
  test('Latvia', () => {
12
- expect(personalCodeToBirthday('050393-12344')).toEqual({ day: 5, month: 3, year: 1993 })
13
- expect(personalCodeToBirthday('050303-22344')).toEqual({ day: 5, month: 3, year: 2003 })
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 })
14
15
  })
15
16
 
16
17
  test('Lithuania', () => {
@@ -19,23 +20,32 @@ describe('valid codes', () => {
19
20
 
20
21
  test('Finland', () => {
21
22
  expect(personalCodeToBirthday('101085-7001')).toEqual({ day: 10, month: 10, year: 1985 })
22
- expect(personalCodeToBirthday('101085+789W')).toEqual({ day: 10, month: 10, year: 1885 })
23
23
  expect(personalCodeToBirthday('150752-308N')).toEqual({ day: 15, month: 7, year: 1952 })
24
24
  expect(personalCodeToBirthday('010100A123D')).toEqual({ day: 1, month: 1, year: 2000 })
25
25
  })
26
26
 
27
27
  test('Sweden', () => {
28
- expect(personalCodeToBirthday('199001011234')).toEqual({ day: 1, month: 1, year: 1990 })
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 })
29
35
  })
30
36
 
31
37
  test('Norway', () => {
32
- expect(personalCodeToBirthday('01020352345')).toEqual({ day: 1, month: 2, year: 2003 })
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 })
33
42
  })
34
43
 
35
44
  test('Denmark', () => {
36
- expect(personalCodeToBirthday('050305-4567')).toEqual({ day: 5, month: 3, year: 2005 })
37
- expect(personalCodeToBirthday('050384-1235')).toEqual({ day: 5, month: 3, year: 1984 })
45
+ expect(personalCodeToBirthday('070589-8901')).toEqual({ day: 7, month: 5, year: 1989 })
46
+ expect(personalCodeToBirthday('230309-4456')).toEqual({ day: 23, month: 3, year: 2009 })
38
47
  expect(personalCodeToBirthday('050384-4567')).toEqual({ day: 5, month: 3, year: 1984 })
48
+ expect(personalCodeToBirthday('230309-3020')).toEqual({ day: 23, month: 3, year: 1909 })
39
49
  })
40
50
 
41
51
  test('Ukraine', () => {
@@ -57,7 +67,8 @@ describe('invalid personal code', () => {
57
67
  })
58
68
 
59
69
  test('Latvia', () => {
60
- expect(personalCodeToBirthday('123456-12345')).toBeNull()
70
+ expect(personalCodeToBirthday('071023-38934')).toBeNull()
71
+ expect(personalCodeToBirthday('071523-38937')).toBeNull()
61
72
  })
62
73
 
63
74
  test('Lithuania', () => {
@@ -72,21 +83,24 @@ describe('invalid personal code', () => {
72
83
  })
73
84
 
74
85
  test('Sweden', () => {
75
- expect(personalCodeToBirthday('1990010123456')).toBeNull()
76
- expect(personalCodeToBirthday('199013011234')).toBeNull()
77
- expect(personalCodeToBirthday('199001341234')).toBeNull()
86
+ expect(personalCodeToBirthday('8112289875')).toBeNull()
87
+ expect(personalCodeToBirthday('8115289871')).toBeNull()
78
88
  })
79
89
 
80
90
  test('Norway', () => {
81
91
  expect(personalCodeToBirthday('32129999999')).toBeNull()
92
+ expect(personalCodeToBirthday('32132811465')).toBeNull()
93
+ expect(personalCodeToBirthday('32112812600')).toBeNull()
82
94
  })
83
95
 
84
96
  test('Denmark', () => {
85
97
  expect(personalCodeToBirthday('999999-1234')).toBeNull()
98
+ expect(personalCodeToBirthday('051484-4569')).toBeNull()
99
+ expect(personalCodeToBirthday('230309-3080')).toBeNull()
86
100
  })
87
101
 
88
102
  test('Ukraine', () => {
89
- expect(personalCodeToBirthday('3406105373')).toBeNull()
103
+ expect(personalCodeToBirthday('3416105373')).toBeNull()
90
104
  })
91
105
 
92
106
  test('Poland', () => {
package/src/index.ts CHANGED
@@ -9,8 +9,10 @@ import ukraineParser from './parsers/ukraine'
9
9
  import { ParsedDate } from './types'
10
10
 
11
11
  export default function personalCodeToBirthday(code: string): ParsedDate | null {
12
+ code = code.replace(' ', '')
13
+
12
14
  if (/^\d{10}$/.test(code)) {
13
- return ukraineParser(code)
15
+ return ukraineParser(code) || swedenParser(code)
14
16
  } else if (/^\d{11}$/.test(code)) {
15
17
  return estoniaLithuaniaParser(code) || polandParser(code) || norwayParser(code)
16
18
  } else if (/^\d{12}$/.test(code)) {
@@ -19,8 +21,12 @@ export default function personalCodeToBirthday(code: string): ParsedDate | null
19
21
  return latviaParser(code)
20
22
  } else if (/^\d{6}[+-A]\d{3}[A-Za-z]$/.test(code)) {
21
23
  return finlandParser(code)
22
- } else if (/^\d{6}[+-A]\d{4}$/.test(code)) {
23
- return finlandParser(code) || denmarkParser(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)
24
30
  }
25
31
 
26
32
  return null
@@ -2,9 +2,7 @@ import { ParsedDate } from '../types'
2
2
  import { isValidDate } from '../utils'
3
3
 
4
4
  export default function denmarkParser(code: string): ParsedDate | null {
5
- if (code.includes('-')) {
6
- code = code.replace('-', '')
7
- }
5
+ code = code.replace('-', '')
8
6
 
9
7
  const day = parseInt(code.slice(0, 2), 10)
10
8
  const month = parseInt(code.slice(2, 4), 10)
@@ -22,9 +20,35 @@ export default function denmarkParser(code: string): ParsedDate | null {
22
20
  }
23
21
  }
24
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
+
25
29
  if (!isValidDate(day, month, year)) {
26
30
  return null
27
31
  }
28
32
 
29
33
  return { day, month, year }
30
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
+ }
@@ -14,15 +14,12 @@ export default function finlandParser(code: string): ParsedDate | null {
14
14
  let year = yearSuffix
15
15
 
16
16
  // Handle the century based on the marker
17
- if (centuryMarker === '+') {
18
- year += 1800
19
- } else if (centuryMarker === '-') {
17
+ if (centuryMarker === '-') {
20
18
  year += 1900
21
19
  } else if (centuryMarker === 'A') {
22
20
  year += 2000
23
21
  }
24
22
 
25
- // Validate the parsed date
26
23
  if (!isValidDate(day, month, year)) {
27
24
  return null
28
25
  }
@@ -3,22 +3,46 @@ import { isValidDate } from '../utils'
3
3
 
4
4
  // Correct old school format is DDMMYY-CXXXX
5
5
  export default function latviaParser(code: string): ParsedDate | null {
6
+ code = code.replace('-', '')
7
+
8
+ if (!isValidChecksum(code)) {
9
+ return null
10
+ }
11
+
6
12
  const day = parseInt(code.slice(0, 2), 10)
7
13
  const month = parseInt(code.slice(2, 4), 10)
8
14
  let year = parseInt(code.slice(4, 6), 10)
9
- const centuryCode = parseInt(code[7], 10)
15
+ const centuryCode = parseInt(code[6], 10)
10
16
 
11
- // Latvian codes are always from the 20th century, so add 1900
12
- if (centuryCode === 2) {
17
+ if (centuryCode === 3) {
13
18
  year += 2000
14
19
  } else {
15
20
  year += 1900
16
21
  }
17
22
 
18
- // Validate the parsed date
19
23
  if (!isValidDate(day, month, year)) {
20
24
  return null
21
25
  }
22
26
 
23
27
  return { day, month, year }
24
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
+ }
@@ -2,23 +2,64 @@ import { ParsedDate } from '../types'
2
2
  import { isValidDate } from '../utils'
3
3
 
4
4
  export default function norwayParser(code: string): ParsedDate | null {
5
+ if (!isValidChecksum(code)) {
6
+ return null
7
+ }
8
+
5
9
  const day = parseInt(code.slice(0, 2), 10)
6
10
  const month = parseInt(code.slice(2, 4), 10)
7
11
  let year = parseInt(code.slice(4, 6), 10)
8
12
 
9
- const individual = parseInt(code.slice(6, 9), 10)
13
+ const centuryMarker = parseInt(code.slice(6, 7), 10)
10
14
 
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
+ // 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) {
15
19
  year += 2000
16
20
  }
17
21
 
18
- // Now validate the date after adjusting the century
19
22
  if (!isValidDate(day, month, year)) {
20
23
  return null
21
24
  }
22
25
 
23
26
  return { day, month, year }
24
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
+ }
@@ -2,9 +2,31 @@ import { ParsedDate } from '../types'
2
2
  import { isValidDate } from '../utils'
3
3
 
4
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)
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
+ }
8
30
 
9
31
  if (!isValidDate(day, month, year)) {
10
32
  return null
@@ -12,3 +34,26 @@ export default function swedenParser(code: string): ParsedDate | null {
12
34
 
13
35
  return { day, month, year }
14
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
+ }
@@ -1,4 +1,5 @@
1
1
  import { ParsedDate } from '../types'
2
+ import { isValidDate } from '../utils'
2
3
 
3
4
  export default function ukrainianParser(code: string): ParsedDate | null {
4
5
  if (!isValidChecksum(code)) {
@@ -16,6 +17,10 @@ export default function ukrainianParser(code: string): ParsedDate | null {
16
17
  const month = birthDate.getMonth() + 1 // getMonth is zero-based
17
18
  const year = birthDate.getFullYear()
18
19
 
20
+ if (!isValidDate(day, month, year)) {
21
+ return null
22
+ }
23
+
19
24
  return { day, month, year }
20
25
  }
21
26
 
package/src/utils.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  export function isValidDate(day: number, month: number, year: number): boolean {
2
2
  const date = new Date(year, month - 1, day) // month is zero-indexed in JS Date
3
3
 
4
+ if (year < 1900 || year > new Date().getFullYear()) {
5
+ return false
6
+ }
7
+
4
8
  return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day
5
9
  }
@@ -1 +0,0 @@
1
- module.exports = require("@remato/prettier-config");