-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathjmbg.py
More file actions
197 lines (156 loc) · 5.94 KB
/
jmbg.py
File metadata and controls
197 lines (156 loc) · 5.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
from __future__ import annotations
import datetime
from typing import Optional
from gender import Gender
from jmbg_error import JmbgError
from regions import _REGIONS
_WEIGHTS = [7, 6, 5, 4, 3, 2, 7, 6, 5, 4, 3, 2]
class Jmbg:
"""
Represents a parsed and validated JMBG number.
Attributes:
original (str): The raw 13-digit input string.
day (int): Day of birth.
month (int): Month of birth.
year (int): Full year of birth (e.g. 1990, 2001).
region (int): Two-digit region code.
unique (int): Three-digit unique number.
checksum (int): Checksum digit.
day_original (str): Zero-padded day string from JMBG (e.g. "05").
month_original (str): Zero-padded month string (e.g. "09").
year_original (str): Three-digit year string from JMBG (e.g. "990").
region_original (str): Two-digit region string (e.g. "71").
unique_original (str): Three-digit unique string (e.g. "001").
region_text (str): Human-readable region name (e.g. "Belgrade").
country (str): Country name (e.g. "Serbia").
"""
def __init__(self, input: str) -> None:
"""
Parse and validate a JMBG string.
Args:
input: A 13-digit JMBG string.
Raises:
JmbgError: If the string is not a valid JMBG.
"""
if len(input) != 13:
raise JmbgError(
f"JMBG string must have exactly 13 digits, got {len(input)}"
)
if not input.isdigit():
raise JmbgError("JMBG must contain only numeric characters")
d = [int(c) for c in input]
dd = d[0] * 10 + d[1]
mm = d[2] * 10 + d[3]
yyy = d[4] * 100 + d[5] * 10 + d[6]
rr = d[7] * 10 + d[8]
bbb = d[9] * 100 + d[10] * 10 + d[11]
c = d[12]
# Full year
year = 2000 + yyy if yyy < 800 else 1000 + yyy
# Validate date
try:
birth_date = datetime.date(year, mm, dd)
except ValueError:
raise JmbgError(f"Date '{dd:02d}/{mm:02d}/{year}' is not valid")
# Validate region
region = _REGIONS.get(rr)
if region is None:
raise JmbgError(f"Region '{rr}' is not valid for JMBG")
# Validate checksum (modulo 11)
total = sum(d[i] * _WEIGHTS[i] for i in range(12))
remainder = total % 11
if remainder == 1:
raise JmbgError("Checksum is not valid")
expected = 0 if remainder == 0 else 11 - remainder
if c != expected:
raise JmbgError("Checksum is not valid")
self.original: str = input
self.day: int = dd
self.month: int = mm
self.year: int = year
self.region: int = rr
self.unique: int = bbb
self.checksum: int = c
self.day_original: str = input[0:2]
self.month_original: str = input[2:4]
self.year_original: str = input[4:7]
self.region_original: str = input[7:9]
self.unique_original: str = input[9:12]
self.region_text: str = region.name
self.country: str = region.country
self._gender: Gender = Gender.FEMALE if bbb >= 500 else Gender.MALE
self._birth_date: datetime.date = birth_date
# ------------------------------------------------------------------
# Class methods
# ------------------------------------------------------------------
@classmethod
def parse(cls, input: str) -> "Jmbg":
"""Parse and return a Jmbg instance. Alias for the constructor."""
return cls(input)
# ------------------------------------------------------------------
# Date / Age
# ------------------------------------------------------------------
def get_date(self) -> datetime.date:
"""Return the birth date as a datetime.date object."""
return self._birth_date
def get_age(self) -> int:
"""Return the current age in full years."""
today = datetime.date.today()
years = today.year - self._birth_date.year
# Check if birthday hasn't occurred yet this year
if (today.month, today.day) < (self._birth_date.month, self._birth_date.day):
years -= 1
return years
# ------------------------------------------------------------------
# Gender
# ------------------------------------------------------------------
def gender(self) -> Gender:
"""Return the Gender enum value."""
return self._gender
def is_male(self) -> bool:
"""Return True if the person is male."""
return self._gender == Gender.MALE
def is_female(self) -> bool:
"""Return True if the person is female."""
return self._gender == Gender.FEMALE
# ------------------------------------------------------------------
# Misc
# ------------------------------------------------------------------
def is_adult(self) -> bool:
"""Return True if the person is 18 or older."""
return self.get_age() >= 18
def format(self) -> str:
"""Return the JMBG as a 13-digit string."""
return self.original
def __str__(self) -> str:
return self.original
def __repr__(self) -> str:
return f"Jmbg('{self.original}')"
def __eq__(self, other: object) -> bool:
if isinstance(other, Jmbg):
return self.original == other.original
return NotImplemented
def __hash__(self) -> int:
return hash(self.original)
def parse(input: str) -> Jmbg:
"""
Parse and validate a JMBG string.
Args:
input: A 13-digit JMBG string.
Returns:
A Jmbg instance.
Raises:
JmbgError: If the string is not a valid JMBG.
"""
return Jmbg(input)
def valid(input: str) -> bool:
"""
Return True if the given string is a valid JMBG, False otherwise.
Args:
input: String to validate.
"""
try:
Jmbg(input)
return True
except JmbgError:
return False