Skip to content

Commit feccaff

Browse files
bblazarthurdejong
authored andcommitted
Add support for Slovenian EMŠO (Unique Master Citizen Number)
Closes arthurdejong#338
1 parent 74cc981 commit feccaff

File tree

3 files changed

+196
-0
lines changed

3 files changed

+196
-0
lines changed

stdnum/si/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# coding: utf-8
33
#
44
# Copyright (C) 2012 Arthur de Jong
5+
# Copyright (C) 2022 Blaž Bregar
56
#
67
# This library is free software; you can redistribute it and/or
78
# modify it under the terms of the GNU Lesser General Public
@@ -22,3 +23,4 @@
2223

2324
# provide vat as an alias
2425
from stdnum.si import ddv as vat # noqa: F401
26+
from stdnum.si import emso as personalid # noqa: F401

stdnum/si/emso.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# emso.py - functions for handling Slovenian Unique Master Citizen Numbers
2+
# coding: utf-8
3+
#
4+
# Copyright (C) 2022 Blaž Bregar
5+
#
6+
# This library is free software; you can redistribute it and/or
7+
# modify it under the terms of the GNU Lesser General Public
8+
# License as published by the Free Software Foundation; either
9+
# version 2.1 of the License, or (at your option) any later version.
10+
#
11+
# This library is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
# Lesser General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public
17+
# License along with this library; if not, write to the Free Software
18+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19+
# 02110-1301 USA
20+
21+
"""Enotna matična številka občana (Unique Master Citizen Number).
22+
23+
The EMŠO is used for uniquely identify persons including foreign citizens
24+
living in Slovenia, It is issued by Centralni Register Prebivalstva CRP
25+
(Central Citizen Registry).
26+
27+
The number consists of 13 digits and includes the person's date of birth, a
28+
political region of birth and a unique number that encodes a person's gender
29+
followed by a check digit.
30+
31+
More information:
32+
33+
* https://en.wikipedia.org/wiki/Unique_Master_Citizen_Number
34+
* https://sl.wikipedia.org/wiki/Enotna_matična_številka_občana
35+
36+
>>> validate('0101006500006')
37+
'0101006500006'
38+
>>> validate('0101006500007') # invalid check digit
39+
Traceback (most recent call last):
40+
...
41+
InvalidChecksum: ...
42+
"""
43+
44+
import datetime
45+
46+
from stdnum.exceptions import *
47+
from stdnum.util import clean, isdigits
48+
49+
50+
def compact(number):
51+
"""Convert the number to the minimal representation. This strips the
52+
number of any valid separators and removes surrounding whitespace."""
53+
return clean(number, ' ').strip()
54+
55+
56+
def calc_check_digit(number):
57+
"""Calculate the check digit."""
58+
weights = (7, 6, 5, 4, 3, 2, 7, 6, 5, 4, 3, 2)
59+
total = sum(int(n) * w for n, w in zip(number, weights))
60+
return str(-total % 11 % 10)
61+
62+
63+
def get_birth_date(number):
64+
"""Return date of birth from valid EMŠO."""
65+
number = compact(number)
66+
day = int(number[:2])
67+
month = int(number[2:4])
68+
year = int(number[4:7])
69+
if year < 800:
70+
year += 2000
71+
else:
72+
year += 1000
73+
try:
74+
return datetime.date(year, month, day)
75+
except ValueError:
76+
raise InvalidComponent()
77+
78+
79+
def get_gender(number):
80+
"""Get the person's birth gender ('M' or 'F')."""
81+
number = compact(number)
82+
if int(number[9:12]) < 500:
83+
return 'M'
84+
else:
85+
return 'F'
86+
87+
88+
def get_region(number):
89+
"""Return (political) region from valid EMŠO."""
90+
return number[7:9]
91+
92+
93+
def validate(number):
94+
"""Check if the number is a valid EMŠO number. This checks the length,
95+
formatting and check digit."""
96+
number = compact(number)
97+
if len(number) != 13:
98+
raise InvalidLength()
99+
if not isdigits(number):
100+
raise InvalidFormat()
101+
get_birth_date(number)
102+
if calc_check_digit(number) != number[-1]:
103+
raise InvalidChecksum()
104+
return number
105+
106+
107+
def is_valid(number):
108+
"""Check if the number provided is a valid ID. This checks the length,
109+
formatting and check digit."""
110+
try:
111+
return bool(validate(number))
112+
except ValidationError:
113+
return False
114+
115+
116+
def format(number):
117+
"""Reformat the number to the standard presentation format."""
118+
return compact(number)

tests/test_si_emso.doctest

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
test_si_emso.doctest - more detailed doctests for the stdnum.si.emso module
2+
3+
Copyright (C) 2022 Blaž Bregar
4+
5+
This library is free software; you can redistribute it and/or
6+
modify it under the terms of the GNU Lesser General Public
7+
License as published by the Free Software Foundation; either
8+
version 2.1 of the License, or (at your option) any later version.
9+
10+
This library is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
Lesser General Public License for more details.
14+
15+
You should have received a copy of the GNU Lesser General Public
16+
License along with this library; if not, write to the Free Software
17+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18+
02110-1301 USA
19+
20+
21+
This file contains more detailed doctests for the stdnum.si.emso. It
22+
tries to validate a number of numbers that have been found online.
23+
24+
>>> from stdnum.si import emso
25+
>>> from stdnum.exceptions import *
26+
27+
28+
Tests for some corner cases.
29+
30+
>>> emso.validate('0101006500006')
31+
'0101006500006'
32+
>>> emso.format(' 0101006 50 000 6 ')
33+
'0101006500006'
34+
>>> emso.validate('12345')
35+
Traceback (most recent call last):
36+
...
37+
InvalidLength: ...
38+
>>> emso.validate('3202006500008')
39+
Traceback (most recent call last):
40+
...
41+
InvalidComponent: ...
42+
>>> emso.validate('0101006500007')
43+
Traceback (most recent call last):
44+
...
45+
InvalidChecksum: ...
46+
>>> emso.validate('010100650A007')
47+
Traceback (most recent call last):
48+
...
49+
InvalidFormat: ...
50+
51+
52+
Tests helper functions.
53+
54+
>>> emso.get_gender('0101006500006')
55+
'M'
56+
>>> emso.get_gender('2902932505526')
57+
'F'
58+
>>> emso.get_region('0101006500006')
59+
'50'
60+
>>> emso.get_birth_date('0101006500006')
61+
datetime.date(2006, 1, 1)
62+
63+
64+
These have been found online and should all be valid numbers.
65+
66+
>>> numbers = '''
67+
...
68+
... 0101006500006
69+
... 1211981500126
70+
... 1508995500237
71+
... 2001939010010
72+
... 2902932505526
73+
...
74+
... '''
75+
>>> [x for x in numbers.splitlines() if x and not emso.is_valid(x)]
76+
[]

0 commit comments

Comments
 (0)