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

Commit 5ce7c3a

Browse files
authored
Fix #54 - implement personal code of Lithuania (#190)
1 parent 3b5c13f commit 5ce7c3a

File tree

2 files changed

+142
-0
lines changed

2 files changed

+142
-0
lines changed

idnumbers/nationalid/LTU.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import re
2+
from datetime import date
3+
from math import floor
4+
from types import SimpleNamespace
5+
from typing import Optional, Tuple, TypedDict
6+
from .util import CHECK_DIGIT, validate_regexp
7+
from .constant import Gender
8+
9+
10+
class ParseResult(TypedDict):
11+
yyyymmdd: date
12+
"""birthday of this ID"""
13+
gender: Gender
14+
"""only male or female"""
15+
sn: str
16+
"""serial number"""
17+
checksum: CHECK_DIGIT
18+
"""checksum digits"""
19+
20+
21+
class PersonalCode:
22+
"""
23+
Lithuania personal code, asmens kodas
24+
https://en.wikipedia.org/wiki/National_identification_number#Lithuania
25+
https://www.oecd.org/tax/automatic-exchange/crs-implementation-and-assistance/tax-identification-numbers/Lithuania-TIN.pdf
26+
"""
27+
METADATA = SimpleNamespace(**{
28+
'iso3166_alpha2': 'LT',
29+
'min_length': 11,
30+
'max_length': 11,
31+
'parsable': True,
32+
'checksum': True,
33+
'regexp': re.compile(r'^(?P<g>\d)'
34+
r'(?P<yy>\d{2})(?P<mm>\d{2})(?P<dd>\d{2})'
35+
r'(?P<sn>\d{3})'
36+
r'(?P<checksum>\d)$')
37+
})
38+
39+
@staticmethod
40+
def validate(id_number: str) -> bool:
41+
"""
42+
Validate the Italy fiscal code
43+
"""
44+
if not id_number:
45+
return False
46+
47+
if not isinstance(id_number, str):
48+
id_number = repr(id_number)
49+
return PersonalCode.parse(id_number) is not None
50+
51+
@staticmethod
52+
def parse(id_number: str) -> Optional[ParseResult]:
53+
"""
54+
parse the id number
55+
"""
56+
match_obj = PersonalCode.METADATA.regexp.match(id_number)
57+
if not match_obj:
58+
return None
59+
60+
checksum = PersonalCode.checksum(id_number)
61+
if checksum is None or str(checksum) != match_obj.group('checksum'):
62+
return None
63+
year_base, gender = PersonalCode.extract_year_base_gender(int(match_obj.group('g')))
64+
yy = int(match_obj.group('yy'))
65+
mm = int(match_obj.group('mm'))
66+
dd = int(match_obj.group('dd'))
67+
try:
68+
return {
69+
'yyyymmdd': date(year_base + yy, mm, dd),
70+
'gender': gender,
71+
'sn': match_obj.group('sn'),
72+
'checksum': checksum
73+
}
74+
except ValueError:
75+
return None
76+
77+
@staticmethod
78+
def checksum(id_number) -> Optional[CHECK_DIGIT]:
79+
"""
80+
algorithm https://en.wikipedia.org/wiki/National_identification_number#Lithuania
81+
"""
82+
if not validate_regexp(id_number, PersonalCode.METADATA.regexp):
83+
return None
84+
b = 1
85+
c = 3
86+
d = 0
87+
e = 0
88+
numbers = [int(char) for char in id_number]
89+
for number in numbers[:-1]:
90+
d += number * b
91+
e += number * c
92+
b = b + 1 if b < 9 else 1
93+
c = c + 1 if c < 9 else 1
94+
d %= 11
95+
e %= 11
96+
if d < 10:
97+
return d
98+
elif e < 10:
99+
return e
100+
else:
101+
return 0
102+
103+
@staticmethod
104+
def extract_year_base_gender(g: CHECK_DIGIT) -> Optional[Tuple[int, Gender]]:
105+
"""
106+
algorithm: G = floor(year / 100) * 2 - 34 - gender while gender = {female: 0, male: 1}
107+
if the value is odd -> male, if the value is even -> female
108+
"""
109+
gender = Gender.FEMALE if g % 2 == 0 else Gender.MALE
110+
# remove the affect of gender, -1 if it is male otherwise it should be the original value
111+
year_g = (g // 2) * 2
112+
year_base = floor((year_g + 34) / 2) * 100
113+
return year_base, gender
114+
115+
116+
NationalID = PersonalCode
117+
"""alias of FiscalCode"""

tests/nationalid/test_LTU.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from unittest import TestCase, main
2+
3+
from idnumbers.nationalid import LTU
4+
5+
6+
class TestLTUValidation(TestCase):
7+
def test_normal_case(self):
8+
self.assertTrue(LTU.PersonalCode.validate('48310031084'))
9+
self.assertTrue(LTU.PersonalCode.validate('46411231034'))
10+
self.assertTrue(LTU.PersonalCode.validate('37311221319'))
11+
12+
def test_error_case(self):
13+
self.assertFalse(LTU.PersonalCode.validate('48310031083'))
14+
15+
def test_parse(self):
16+
result = LTU.PersonalCode.parse('48310031084')
17+
self.assertEqual(1983, result['yyyymmdd'].year)
18+
self.assertEqual(10, result['yyyymmdd'].month)
19+
self.assertEqual(3, result['yyyymmdd'].day)
20+
self.assertEqual('108', result['sn'])
21+
self.assertEqual(4, result['checksum'])
22+
23+
24+
if __name__ == '__main__':
25+
main()

0 commit comments

Comments
 (0)