Source code for dk.identifiers.persnr

# -*- coding: utf-8 -*-

"""Norwegian 'Personnummer' module.

"""

import datetime


[docs]class PersnrException(ValueError): "Base exception for persnr module." pass
[docs]def multiply_reduce(avec, bvec): """Multiply each item in a with corresponding item in b, then sum the result. """ return sum((aval * bval) for (aval, bval) in zip(avec, bvec))
[docs]def is_persnr(pnr, country='NO'): "Return True if ``pnr`` is a valid persnr." if not country=='NO' and pnr and len(pnr)>7: return True try: return check_pnr_structure(pnr) except PersnrException: return False except ValueError: return False
[docs]def splitpnr(pnr): "Split the personnummer into it's parts." if len(pnr) != 11: raise PersnrException( "Persnr: incorrect length (%s): %s" % (len(pnr), pnr)) res = { 'day': pnr[:2], 'month': pnr[2:4], 'year': pnr[4:6], 'individ': pnr[6:9], 'gender': 'F' if (int(pnr[8]) % 2 == 0) else 'M', 'k1': pnr[9:10], 'k2': pnr[10:11], 'dnr': False, } if pnr[0] >= '4': res['dnr'] = True res['day'] = str(int(res['day']) - 40) return res
[docs]def calc_year(yr2, inr): "Find the 4-digit year, following all the rules." if 900 <= inr <= 999: if yr2 >= 40: return 1900 + yr2 else: return 2000 + yr2 if 500 <= inr <= 999: if yr2 <= 39: return 2000 + yr2 if 500 <= inr <= 749: if yr2 >= 55: return 1800 + yr2 if 0 <= inr <= 499: return 1900 + yr2 raise PersnrException('Persnr: individ nr mismatch')
[docs]def year(pnr): "Extract the year from pnr." pdata = splitpnr(pnr) inr = int(pdata['individ']) yr2 = int(pdata['year']) return calc_year(yr2, inr)
[docs]def date(pnr): "Find the birth date and return as datetime.date() object." pdata = splitpnr(pnr) day = int(pdata['day']) month = int(pdata['month']) yr4 = year(pnr) return datetime.date(yr4, month, day)
[docs]def gender(pnr): "Extract the gender as 'M' for male and 'F' for female." return splitpnr(pnr)['gender']
[docs]def check_individnr(inr, year4): "Hopelessly inefficient way of checking the individnr." # http://no.wikipedia.org/wiki/Personnummer # 000-499: 1900-1999 # 500-749: 1855-1899 # 500-999: 2000-2039 # 900-999: 1940-1999 yrange = set() if 0 <= inr < 500: yrange |= set(range(1900, 2000)) if 500 <= inr < 750: yrange |= set(range(1855, 1900)) if 500 <= inr < 1000: yrange |= set(range(2000, 2039)) if 900 <= inr < 1000: yrange |= set(range(1940, 2000)) if year4 not in set(yrange): raise PersnrException('Persnr: individ nr mismatch') return True
[docs]def check_pnr_structure(pnr): "Raise exception if the structure of the personnummer is incorrect." splitpnr(pnr) # throws on error date(pnr) # throws on error check_parity(pnr) # throws on error return True
[docs]def check_pnr(pnr, birthday, sex): """ >>> check_pnr('02057035768', datetime.date(1970, 5, 2), 'm') True """ # check that the date is valid (birthday argument is valid # by construction). data = splitpnr(pnr) day = int(data['day']) if day != birthday.day: raise PersnrException('Persnr: Invalid Date') month = int(data['month']) if month != birthday.month: raise PersnrException('Persnr: Invalid Date') yr2 = int(data['year']) # the two digit year if yr2 != birthday.year % 100: raise PersnrException('Persnr: Invalid Date') individnr = int(data['individ']) check_individnr(individnr, birthday.year) # males have an odd digit in position 8, females an even digit. if sex not in 'mMfF': raise PersnrException("Persnr: unknown gender (%r)" % sex) if sex.upper() != data['gender']: raise PersnrException({ 'M': 'Persnr: even male', 'F': 'Persnr: odd female' }.get(sex, 'Persnr: unknown gender (%r)' % sex)) return check_parity(pnr)
VEKT1 = [3, 7, 6, 1, 8, 9, 4, 5, 2, 1, 0] VEKT2 = [5, 4, 3, 2, 7, 6, 5, 4, 3, 2, 1]
[docs]def check_parity(pnr): "Check the last two digits, which are parity controls." try: pnr = [int(v) for v in pnr] except ValueError: raise PersnrException('Kun tall i fødselsnr.') if multiply_reduce(pnr, VEKT1) % 11 != 0: raise PersnrException('Dette er ikke et gyldig fødselsnr. [ktrl-1]') if multiply_reduce(pnr, VEKT2) % 11 != 0: raise PersnrException('Dette er ikke et gyldig fødselsnr. [ktrl-2]') return True
[docs]def calc_par1(ppnr): "Calculate the first parity digit." for i in range(1, 10): tmp = [int(v) for v in ppnr[:9]] + [i] val = multiply_reduce(tmp, VEKT1) if val % 11 == 0: return i raise PersnrException(ppnr)
[docs]def calc_par2(ppnr): "Calculate the second parity digit." for i in range(1, 10): tmp = [int(v) for v in ppnr[:10]] + [i] val = multiply_reduce(tmp, VEKT2) if val % 11 == 0: return i raise PersnrException(ppnr)
[docs]def calc_parity(ppnr): "Calculate parity digits." tmp = ppnr + str(calc_par1(ppnr)) val = tmp + str(calc_par2(tmp)) return val
[docs]def generate_pnr(day, gndr): "Generate all persnrs for a given gender on a given day." datepart = day.strftime('%d%m%y') if gndr == 'M': inrs = range(1, 1000, 2) else: inrs = range(0, 1000, 2) for inr in inrs: try: _pnr = calc_parity('%s%03d' % (datepart, inr)) if check_pnr_structure(_pnr): yield _pnr else: print _pnr, 'error' except PersnrException: pass
[docs]def list_pnr(day=None, gender='M'): # pylint: disable=W0621 "List all persnrs for a given gender on a given day." if day is None: day = datetime.date.today() return list(generate_pnr(day, gender))
[docs]class TestingPersnr(object): "Class to generate personnummer." def __init__(self): self.last_used = -1 self.persnrs = list_pnr(datetime.date(2050, 1, 2))
[docs] def next_persnr(self): self.last_used += 1 return self.persnrs[self.last_used]
_persnr = TestingPersnr()
[docs]def testing_persnr(n=0): """Create a persnr for use in unit tests. If different tests need separate persnr, pass a unique small integer as a parameter. """ # return _persnr.next_persnr() # Keeping old code, some tests fails because of the new one. return list_pnr(datetime.date(2050, 1, 2))[n]
if __name__ == "__main__": # invoke with python -m core.persnr for _pnr in list_pnr(datetime.date(1975, 6, 24), 'F'): print _pnr