-
Notifications
You must be signed in to change notification settings - Fork 0
/
estnin.py
executable file
·437 lines (347 loc) · 13.2 KB
/
estnin.py
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
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
# coding: utf-8
import datetime
from datetime import date
from collections import namedtuple
__author__ = "Anti Räis"
class _estnin(namedtuple('ESTNIN', 'century date sequence checksum')):
def __str__(self):
return str(int(self))
def __int__(self):
date = self.date
return (
self.century * 10**10
+ date.year % 100 * 10**8
+ date.month * 10**6
+ date.day * 10**4
+ self.sequence * 10
+ self.checksum
)
class estnin(object):
"""
Provides an representation for Estonian national identity number.
"""
#: First valid value (minimum as a number).
MIN = 10001010002
#: Last valid value (maximum as a number).
MAX = 89912319991
#: Value used by :class:`estnin.create <estnin.create>` method to indicate that the new EstNIN should be created for a male.
MALE = 0
#: Value used by :class:`estnin.create <estnin.create>` method to indicate that the new EstNIN should be created is for a female.
FEMALE = 1
def __init__(self, estnin, set_checksum=False):
"""
Create a new instance from given value.
:param estnin: value to create an EstNIN representation for.
:type estnin: :py:func:`str` or :py:func:`int`
:param set_checksum: if set to :py:const:`True` then recalculate and set the checksum value.
:type set_checksum: :py:const:`bool`
:return: :class:`estnin <estnin>` object
:rtype: estnin.estnin
:raises: :py:exc:`ValueError <ValueError>` if invalid value is given.
**Usage:**
>>> from estnin import estnin
>>> estnin(37001011233)
37001011233
>>> estnin("37001011230", set_checksum=True)
37001011233
"""
self._estnin = self._validate_format(estnin, set_checksum=set_checksum)
@classmethod
def create(cls, sex, birth_date, sequence):
"""
Create a new instance by providing the sex, birth date and sequence.
:param sex: use *falsy* for male and *truthy* value for female
:type sex: :class:`estnin.MALE <estnin.MALE>` or :class:`estnin.FEMALE <estnin.FEMALE>`
:param birth_date: date of birth
:type birth_date: :py:func:`datetime.date`
:param sequence: value in ``[0 - 999]`` specifing the sequence number on given day
:type sequence: :py:func:`int`
:return: :class:`estnin.estnin <estnin.estnin>` object
:rtype: estnin.estnin
:raises: :py:exc:`ValueError <ValueError>` if invalid value is provided
**Usage:**
>>> from estnin import estnin
>>> from datetime import date
>>> estnin.create(estnin.MALE, date(1970, 1, 1), 123)
37001011233
"""
cls._validate_year(birth_date.year)
cls._validate_sequence(sequence)
century = ((birth_date.year - 1800) // 100) * 2 + 1 + bool(sex)
return cls(_estnin(century, birth_date, sequence, 0), set_checksum=True)
def __repr__(self):
return str(self._estnin)
def __int__(self):
return int(self._estnin)
def __lt__(self, other):
return int(self) < int(other)
def __le__(self, other):
return int(self) <= int(other)
def __eq__(self, other):
return int(self) == int(other)
def __neg__(self):
if self.is_male:
self.century += 1
else:
self.century -= 1
return self
def __add__(self, other):
days, sequence = divmod(self.sequence + other, 1000)
date = self.date + datetime.timedelta(days=days)
self._validate_year(date.year)
century = self._calculate_century(date.year)
self._estnin = self._estnin._replace(century=century, date=date, sequence=sequence)
self._update_checksum()
return self
def __sub__(self, other):
return self + (-other)
def __iter__(self):
return self
def __next__(self):
try:
value = estnin(self)
self += 1
return value
except ValueError:
raise StopIteration
def __reversed__(self):
try:
while True:
value = estnin(self)
self -= 1
yield value
except ValueError:
return
@classmethod
def _validate_year(self, year):
if not 1800 <= year <= 2199:
raise ValueError('year not in range [1800..2199]')
@classmethod
def _validate_sequence(self, sequence):
if not 0 <= sequence <= 999:
raise ValueError('sequence not in range [0..999]')
@classmethod
def _validate_century(self, century):
if not 1 <= century <= 8:
raise ValueError('century not in range [1..8]')
def _calculate_century(self, year):
century = (year - 1800) // 100 * 2 + 1
return century if self.is_male else century + 1
@classmethod
def _calculate_year(self, century, year):
return 1800 + 100 * ((century - 1) // 2) + year % 100
def _validate_format(self, estnin, set_checksum=False):
estnin = int(estnin)
if set_checksum:
if not self.MIN // 10 * 10 <= estnin <= self.MAX // 10 * 10 + 9:
raise ValueError('value is out of range')
checksum = self._calculate_checksum(estnin)
else:
if not self.MIN <= estnin <= self.MAX:
raise ValueError('value is out of range')
checksum = self._validate_checksum(estnin)
return _estnin(
estnin // 10**10,
self._validate_date(estnin),
(estnin // 10) % 1000,
checksum,
)
def _validate_date(self, estnin):
century = estnin // 10**10
birth_year = self._calculate_year(century, (estnin % 10**10) // 10**8)
birth_month = (estnin % 10**8) // 10**6
birth_day = (estnin % 10**6) // 10**4
return datetime.date(birth_year, birth_month, birth_day)
def _validate_checksum(self, checksum):
calculated = self._calculate_checksum(checksum)
if checksum % 10 != calculated:
raise ValueError('invalid checksum')
return calculated
def _update_checksum(self):
checksum = self._calculate_checksum(self._estnin)
self._estnin = self._estnin._replace(checksum=checksum)
@classmethod
def _calculate_checksum(self, estnin):
_estnin = str(estnin)
checksum = sum(int(k) * v for k, v in zip(_estnin, [1, 2, 3, 4, 5, 6, 7, 8, 9, 1])) % 11
if checksum == 10:
checksum = sum(int(k) * v for k, v in zip(_estnin, [3, 4, 5, 6, 7, 8, 9, 1, 2, 3])) % 11
checksum = 0 if checksum == 10 else checksum
return checksum
@property
def is_male(self):
"""
Returns :py:const:`True` if the EstNIN represents a male.
:rtype: :py:const:`bool`
"""
return self._estnin.century % 2 == 1
@property
def is_female(self):
"""
Returns :py:const:`True` if the EstNIN represents a female.
:rtype: :py:const:`bool`
"""
return self._estnin.century % 2 == 0
@property
def century(self):
"""
Century property that returns the century digit in the EstNIN or sets it accordingly.
:getter: return the century digit as :py:func:`int`.
:setter: update the century digit given as :py:func:`int` or :py:func:`str`.
:modifies: checksum
:raises: :py:exc:`ValueError <ValueError>` if century value is not in range ``[1..8]``
**Usage:**
>>> from estnin import estnin
>>> person = estnin(37001011233)
>>> person.century
3
>>> person.century = 5
>>> person
57001011235
"""
return self._estnin.century
@century.setter
def century(self, value):
century = int(value)
self._validate_century(century)
year = self._calculate_year(century, self._estnin.date.year)
date = self._estnin.date.replace(year=year)
self._estnin = self._estnin._replace(century=century, date=date)
self._update_checksum()
@property
def year(self):
"""
Year property that returns the year in the EstNIN or sets it accordingly.
:getter: return the year as :py:func:`int` in the format of ``YYYY``.
:setter: update the year given as :py:func:`int` or :py:func:`str` in the format of ``YYYY``.
:modifies: century, checksum
:raises: :py:exc:`ValueError <ValueError>` if year value is not in range ``[1800..2199]``
**Usage:**
>>> from estnin import estnin
>>> person = estnin(37001011233)
>>> person.year
1970
>>> person.year = 2001
>>> person
50101011235
"""
return self._estnin.date.year
@year.setter
def year(self, value):
year = int(value)
self._validate_year(year)
date = self._estnin.date.replace(year=year)
century = self._calculate_century(date.year)
self._estnin = self._estnin._replace(century=century, date=date)
self._update_checksum()
@property
def month(self):
"""
Month property that returns the month in the EstNIN or sets it accordingly.
:getter: return the month as :py:func:`int` in the format of ``MM``.
:setter: update the month given as :py:func:`int` or :py:func:`str` in the format of ``MM``.
:modifies: checksum
:raises: :py:exc:`ValueError <ValueError>` if month value is not in range ``[1..12]``
**Usage:**
>>> from estnin import estnin
>>> person = estnin(37001011233)
>>> person.month
1
>>> person.month = 12
>>> person
30112011231
"""
return self._estnin.date.month
@month.setter
def month(self, value):
month = int(value)
date = self._estnin.date.replace(month=month)
self._estnin = self._estnin._replace(date=date)
self._update_checksum()
@property
def day(self):
"""
Day property that returns the day in the EstNIN or sets it accordingly.
:getter: return the day as :py:func:`int` in the format of ``DD``.
:setter: update the day given as :py:func:`int` or :py:func:`str` in the format of ``DD``.
:modifies: checksum
:raises: :py:exc:`ValueError <ValueError>` if day value is not valid for given month.
**Usage:**
>>> from estnin import estnin
>>> person = estnin(37001011233)
>>> person.day
1
>>> person.day = 31
>>> person
37001311233
"""
return self._estnin.date.day
@day.setter
def day(self, value):
day = int(value)
date = self._estnin.date.replace(day=day)
self._estnin = self._estnin._replace(date=date)
self._update_checksum()
@property
def sequence(self):
"""
Sequence property that returns the sequence in the EstNIN or sets it accordingly.
:getter: return the sequence as :py:func:`int`.
:setter: update the sequence given as :py:func:`int` or :py:func:`str`.
:modifies: checksum
:raises: :py:exc:`ValueError <ValueError>` if sequence value is not in range ``[0..999]``.
**Usage:**
>>> from estnin import estnin
>>> person = estnin(37001011233)
>>> person.sequence
123
>>> person.sequence = 42
>>> person
37001010421
"""
return self._estnin.sequence
@sequence.setter
def sequence(self, value):
sequence = int(value)
self._validate_sequence(sequence)
self._estnin = self._estnin._replace(sequence=sequence)
self._update_checksum()
@property
def checksum(self):
"""
Checksum property that returns the checksum digit in the EstNIN.
:getter: return the checksum as :py:func:`int`.
**Usage:**
>>> from estnin import estnin
>>> person = estnin(37001011233)
>>> person.checksum
3
"""
return self._estnin.checksum
@property
def date(self):
"""
Date property that returns the date representated in the EstNIN.
:getter: return the date as :py:func:`datetime.date`.
:setter: update the date given as :py:func:`datetime.date`.
:modifies: century, checksum
:raises: :py:exc:`ValueError <ValueError>` if invalid date is given.
**Usage:**
>>> from estnin import estnin
>>> person = estnin(37001011233)
>>> person.date
datetime.date(1970, 1, 1)
>>> person.date = person.date.replace(year=1972, day=22)
>>> person.date
datetime.date(1972, 1, 22)
>>> person
37201221236
"""
return self._estnin.date
@date.setter
def date(self, value):
if not isinstance(value, date):
raise ValueError('invalid date object')
self.year = value.year
self.month = value.month
self.day = value.day