Source code for estnin

# 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
        )


[docs]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)
[docs] @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