@remato/personal-code-to-birthday 1.1.0 → 1.2.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.
@@ -0,0 +1,6 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "npm"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
@@ -0,0 +1,128 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, religion, or sexual identity
10
+ and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the
26
+ overall community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or
31
+ advances of any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email
35
+ address, without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official e-mail address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ .
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series
86
+ of actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or
93
+ permanent ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within
113
+ the community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.0, available at
119
+ https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120
+
121
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct
122
+ enforcement ladder](https://github.com/mozilla/diversity).
123
+
124
+ [homepage]: https://www.contributor-covenant.org
125
+
126
+ For answers to common questions about this code of conduct, see the FAQ at
127
+ https://www.contributor-covenant.org/faq. Translations are available at
128
+ https://www.contributor-covenant.org/translations.
@@ -0,0 +1,2 @@
1
+ - Make sure your changes are [prettified](https://prettier.io/) (you can use `npm run prettier`)
2
+ - Add unit tests to maintain 100% coverage
@@ -0,0 +1,4 @@
1
+ ##### Checklist
2
+
3
+ - [ ] Updated README.md
4
+ - [ ] Wrote tests
package/README.md CHANGED
@@ -3,15 +3,26 @@
3
3
  [![Build](https://img.shields.io/github/actions/workflow/status/rematocorp/personal-code-to-birthday/ci.yml)](https://github.com/rematocorp/personal-code-to-birthday/actions/workflows/ci.yml)
4
4
  [![Codecov](https://img.shields.io/codecov/c/github/rematocorp/personal-code-to-birthday?token=NDT35FM2LG&style=flat)](https://codecov.io/gh/rematocorp/personal-code-to-birthday)
5
5
 
6
- Converts personal identification codes (or national ID numbers) from various countries into birthdate.
6
+ Converts personal identification codes from various countries into birthdate.
7
7
 
8
- ### Install
8
+ ### Features
9
+
10
+ - **Automatic Birthdate Extraction**: Effortlessly extracts birthdates from national ID codes.
11
+ - **Built-in Validation**: Confirms personal code validity using checksum verification before returning a date.
12
+ - **Country-Agnostic**: Automatically identifies and processes the country format, requiring no additional parameters.
13
+ - **Lightweight & Seamless Integration**: Designed for easy integration into any project with minimal setup.
14
+
15
+ ### Limitations
16
+
17
+ - **Date Range Restriction**: Only supports personal codes issued from 1900 to the present. Future-dated codes are not currently supported.
18
+
19
+ ### Installation
9
20
 
10
21
  ```bash
11
22
  npm install @remato/personal-code-to-birthday
12
23
  ```
13
24
 
14
- ### How to use
25
+ ### Usage
15
26
 
16
27
  ```ts
17
28
  const birthday = personalCodeToBirthday('39309262855')
@@ -21,28 +32,15 @@ console.log(birthday) // outputs { day: 26, month: 9, year: 1993 }
21
32
 
22
33
  ### Countries supported
23
34
 
24
- - 🇪🇪 Estonia
25
- - 🇱🇻 Latvia
26
- - 🇱🇹 Lithuania
27
- - 🇫🇮 Finland
28
- - 🇸🇪 Sweden
29
- - 🇳🇴 Norway
30
- - 🇩🇰 Denmark
31
- - 🇺🇦 Ukraine
32
- - 🇵🇱 Poland
33
-
34
- Countries we would like to support (PRs are welcome):
35
-
36
- - Iceland
37
- - Romania
38
- - Hungary
39
- - Bulgaria
40
- - Moldova
41
- - Azerbaijan
42
- - North Macedonia
43
- - Czech Republic
44
- - Slovakia
45
- - South Korea
46
- - Turkey
47
- - South Africa
48
- - Russia
35
+ 1. 🇺🇦 Ukraine
36
+ 2. 🇪🇪 Estonia
37
+ 3. 🇱🇻 Latvia
38
+ 4. 🇱🇹 Lithuania
39
+ 5. 🇵🇱 Poland
40
+ 6. 🇫🇮 Finland
41
+ 7. 🇸🇪 Sweden
42
+ 8. 🇳🇴 Norway
43
+ 9. 🇩🇰 Denmark
44
+ 10. 🇷🇴 Romania
45
+
46
+ Need support for a new country or have feedback? [Create an issue.](https://github.com/rematocorp/personal-code-to-birthday/issues/new)
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.2.0",
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', () => {
@@ -48,6 +58,12 @@ describe('valid codes', () => {
48
58
  expect(personalCodeToBirthday('02070803628')).toEqual({ day: 8, month: 7, year: 1902 })
49
59
  expect(personalCodeToBirthday('24231012344')).toEqual({ day: 10, month: 3, year: 2024 })
50
60
  })
61
+
62
+ test('Romania', () => {
63
+ expect(personalCodeToBirthday('1900101220018')).toEqual({ day: 1, month: 1, year: 1990 })
64
+ expect(personalCodeToBirthday('6010303275841')).toEqual({ day: 3, month: 3, year: 2001 })
65
+ expect(personalCodeToBirthday('2990721275847')).toEqual({ day: 21, month: 7, year: 1999 })
66
+ })
51
67
  })
52
68
 
53
69
  describe('invalid personal code', () => {
@@ -57,7 +73,8 @@ describe('invalid personal code', () => {
57
73
  })
58
74
 
59
75
  test('Latvia', () => {
60
- expect(personalCodeToBirthday('123456-12345')).toBeNull()
76
+ expect(personalCodeToBirthday('071023-38934')).toBeNull()
77
+ expect(personalCodeToBirthday('071523-38937')).toBeNull()
61
78
  })
62
79
 
63
80
  test('Lithuania', () => {
@@ -72,21 +89,24 @@ describe('invalid personal code', () => {
72
89
  })
73
90
 
74
91
  test('Sweden', () => {
75
- expect(personalCodeToBirthday('1990010123456')).toBeNull()
76
- expect(personalCodeToBirthday('199013011234')).toBeNull()
77
- expect(personalCodeToBirthday('199001341234')).toBeNull()
92
+ expect(personalCodeToBirthday('8112289875')).toBeNull()
93
+ expect(personalCodeToBirthday('8115289871')).toBeNull()
78
94
  })
79
95
 
80
96
  test('Norway', () => {
81
97
  expect(personalCodeToBirthday('32129999999')).toBeNull()
98
+ expect(personalCodeToBirthday('32132811465')).toBeNull()
99
+ expect(personalCodeToBirthday('32112812600')).toBeNull()
82
100
  })
83
101
 
84
102
  test('Denmark', () => {
85
103
  expect(personalCodeToBirthday('999999-1234')).toBeNull()
104
+ expect(personalCodeToBirthday('051484-4569')).toBeNull()
105
+ expect(personalCodeToBirthday('230309-3080')).toBeNull()
86
106
  })
87
107
 
88
108
  test('Ukraine', () => {
89
- expect(personalCodeToBirthday('3406105373')).toBeNull()
109
+ expect(personalCodeToBirthday('3416105373')).toBeNull()
90
110
  })
91
111
 
92
112
  test('Poland', () => {
@@ -94,6 +114,11 @@ describe('invalid personal code', () => {
94
114
  expect(personalCodeToBirthday('24234012341')).toBeNull()
95
115
  })
96
116
 
117
+ test('Romania', () => {
118
+ expect(personalCodeToBirthday('1980521275840')).toBeNull()
119
+ expect(personalCodeToBirthday('1234567890128')).toBeNull()
120
+ })
121
+
97
122
  test('unidentified personal code format', () => {
98
123
  expect(personalCodeToBirthday('abcd1234')).toBeNull()
99
124
  })
package/src/index.ts CHANGED
@@ -4,23 +4,32 @@ import finlandParser from './parsers/finland'
4
4
  import latviaParser from './parsers/latvia'
5
5
  import norwayParser from './parsers/norway'
6
6
  import polandParser from './parsers/poland'
7
+ import romaniaParser from './parsers/romania'
7
8
  import swedenParser from './parsers/sweden'
8
9
  import ukraineParser from './parsers/ukraine'
9
10
  import { ParsedDate } from './types'
10
11
 
11
12
  export default function personalCodeToBirthday(code: string): ParsedDate | null {
13
+ code = code.replace(' ', '')
14
+
12
15
  if (/^\d{10}$/.test(code)) {
13
- return ukraineParser(code)
16
+ return ukraineParser(code) || swedenParser(code)
14
17
  } else if (/^\d{11}$/.test(code)) {
15
18
  return estoniaLithuaniaParser(code) || polandParser(code) || norwayParser(code)
16
19
  } else if (/^\d{12}$/.test(code)) {
17
20
  return swedenParser(code)
21
+ } else if (/^\d{13}$/.test(code)) {
22
+ return romaniaParser(code)
18
23
  } else if (/^\d{6}-\d{5}$/.test(code)) {
19
24
  return latviaParser(code)
20
25
  } else if (/^\d{6}[+-A]\d{3}[A-Za-z]$/.test(code)) {
21
26
  return finlandParser(code)
22
- } else if (/^\d{6}[+-A]\d{4}$/.test(code)) {
23
- return finlandParser(code) || denmarkParser(code)
27
+ } else if (/^\d{6}[-]\d{4}$/.test(code)) {
28
+ return finlandParser(code) || swedenParser(code) || denmarkParser(code)
29
+ } else if (/^\d{6}[A]\d{4}$/.test(code)) {
30
+ return finlandParser(code)
31
+ } else if (/^\d{8}[-]\d{4}$/.test(code)) {
32
+ return swedenParser(code)
24
33
  }
25
34
 
26
35
  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
+ }
@@ -0,0 +1,38 @@
1
+ import { ParsedDate } from '../types'
2
+ import { isValidDate } from '../utils'
3
+
4
+ export default function romaniaParser(code: string): ParsedDate | null {
5
+ if (!isValidChecksum(code)) {
6
+ return null
7
+ }
8
+
9
+ let year = parseInt(code.slice(1, 3), 10)
10
+ const month = parseInt(code.slice(3, 5), 10)
11
+ const day = parseInt(code.slice(5, 7), 10)
12
+ const gender = parseInt(code[0], 10)
13
+
14
+ if (gender === 1 || gender === 2) {
15
+ year += 1900
16
+ } else if (gender === 5 || gender === 6) {
17
+ year += 2000
18
+ }
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 weights = [2, 7, 9, 1, 4, 6, 3, 5, 8, 2, 7, 9]
29
+ let sum = 0
30
+
31
+ for (let i = 0; i < 12; i++) {
32
+ sum += parseInt(code[i]) * weights[i]
33
+ }
34
+
35
+ const checksum = sum % 11 === 10 ? 1 : sum % 11
36
+
37
+ return checksum === parseInt(code[12])
38
+ }
@@ -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): boolean {
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");