Skip to content
This repository was archived by the owner on Jan 8, 2025. It is now read-only.

Commit 229e5b0

Browse files
authored
Fix #38 implement Bulgaria Uniform civil number (#160)
1 parent 3f4147d commit 229e5b0

File tree

3 files changed

+115
-2
lines changed

3 files changed

+115
-2
lines changed

idnumbers/nationalid/BGR.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import re
2+
from datetime import date
3+
from types import SimpleNamespace
4+
from typing import TypedDict, Optional
5+
6+
from .util import validate_regexp, CHECK_DIGIT, weighted_modulus_digit
7+
from .constant import Gender
8+
9+
10+
class ParseResult(TypedDict):
11+
"""Parse result of UniformCivilNumber"""
12+
yyyymmdd: date
13+
"""year of birth"""
14+
checksum: CHECK_DIGIT
15+
"""check digits"""
16+
gender: Gender
17+
18+
19+
class UniformCivilNumber:
20+
"""
21+
Bulgaria Uniform civil number
22+
https://en.wikipedia.org/wiki/National_identification_number#Bulgaria
23+
"""
24+
METADATA = SimpleNamespace(**{
25+
'iso3166_alpha2': 'BG',
26+
# length without insignificant chars
27+
'min_length': 10,
28+
'max_length': 10,
29+
'parsable': True,
30+
'checksum': True,
31+
'regexp': re.compile(r'^(?P<yy>\d{2})'
32+
r'(?P<mm>\d{2})'
33+
r'(?P<dd>\d{2})'
34+
r'\d{2}'
35+
r'(?P<gender>\d)'
36+
r'(?P<checksum>\d)$')
37+
})
38+
39+
@staticmethod
40+
def validate(id_number: str) -> bool:
41+
"""
42+
Validate the BGR id number
43+
"""
44+
if not validate_regexp(id_number, UniformCivilNumber.METADATA.regexp):
45+
return False
46+
return UniformCivilNumber.parse(id_number) is not None
47+
48+
@staticmethod
49+
def parse(id_number: str) -> Optional[ParseResult]:
50+
"""
51+
Parse the result
52+
"""
53+
match_obj = UniformCivilNumber.METADATA.regexp.match(id_number)
54+
checksum = int(match_obj.group("checksum"))
55+
yy = int(match_obj.group("yy"))
56+
mm = int(match_obj.group("mm"))
57+
dd = int(match_obj.group("dd"))
58+
if UniformCivilNumber.checksum(id_number) != checksum:
59+
return None
60+
if mm > 40:
61+
mm -= 40
62+
yyyy = yy + 2000
63+
elif mm > 20:
64+
mm -= 20
65+
yyyy = yy + 1800
66+
else:
67+
yyyy = yy + 1900
68+
return {
69+
'yyyymmdd': date(yyyy, mm, dd),
70+
"checksum": int(checksum),
71+
'gender': Gender.MALE if int(match_obj.group("gender")) % 2 == 0 else Gender.FEMALE
72+
}
73+
74+
MULTIPLIER = [2, 4, 8, 5, 10, 9, 7, 3, 6]
75+
76+
@staticmethod
77+
def checksum(id_number: str) -> CHECK_DIGIT:
78+
"""
79+
Get the checksum digit
80+
https://en.wikipedia.org/wiki/Unique_citizenship_number
81+
"""
82+
digits_numbers = [int(i) for i in id_number[:-1]]
83+
return weighted_modulus_digit(digits_numbers, UniformCivilNumber.MULTIPLIER, 11, True)

idnumbers/nationalid/util.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from re import Pattern
2-
from typing import List, Literal, cast
2+
from typing import List, Literal, cast, Type
33

44
VERHOEFF = {
55
'D_TABLE': [
@@ -28,7 +28,7 @@
2828
}
2929
"""[Table](https://en.wikipedia.org/wiki/Verhoeff_algorithm#Table-based_algorithm) for the Verhoeff algorithm"""
3030

31-
CHECK_DIGIT = Literal[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
31+
CHECK_DIGIT: Type[int] = Literal[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
3232
"""Check digit type. Numeric check digits are only allowed in 0 to 9"""
3333

3434
CHECK_ALPHA = Literal['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K',

tests/nationalid/test_BGR.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from unittest import TestCase, main
2+
from datetime import date
3+
4+
from idnumbers.nationalid import BGR
5+
from idnumbers.nationalid.constant import Gender
6+
7+
8+
class TestBGRValidation(TestCase):
9+
def test_normal_case(self):
10+
self.assertTrue(BGR.UniformCivilNumber.validate('7501020018'))
11+
self.assertTrue(BGR.UniformCivilNumber.validate('7542011030'))
12+
13+
def test_error_case(self):
14+
self.assertFalse(BGR.UniformCivilNumber.validate('7501020011'))
15+
self.assertFalse(BGR.UniformCivilNumber.validate('750102 0018'))
16+
17+
def test_parse(self):
18+
result = BGR.UniformCivilNumber.parse('7501020018')
19+
self.assertEqual(date(1975, 1, 2), result['yyyymmdd'])
20+
self.assertEqual(Gender.FEMALE, result['gender'])
21+
self.assertEqual(8, result['checksum'])
22+
23+
result = BGR.UniformCivilNumber.parse('7552010005')
24+
self.assertEqual(date(2075, 12, 1), result['yyyymmdd'])
25+
self.assertEqual(Gender.MALE, result['gender'])
26+
self.assertEqual(5, result['checksum'])
27+
28+
29+
if __name__ == '__main__':
30+
main()

0 commit comments

Comments
 (0)