diff options
-rw-r--r-- | stdnum/it/codicefiscale.py | 152 | ||||
-rw-r--r-- | tests/test_it_codicefiscale.doctest | 129 |
2 files changed, 281 insertions, 0 deletions
diff --git a/stdnum/it/codicefiscale.py b/stdnum/it/codicefiscale.py new file mode 100644 index 0000000..63b7f6e --- /dev/null +++ b/stdnum/it/codicefiscale.py @@ -0,0 +1,152 @@ +# codicefiscale.py - library for Italian fiscal code +# +# This file is based on code from pycodicefiscale, a Python library for +# working with Italian fiscal code numbers officially known as Italy's +# Codice Fiscale. +# https://github.com/baxeico/pycodicefiscale +# +# Copyright (C) 2009-2013 Emanuele Rocca +# Copyright (C) 2014 Augusto Destrero +# Copyright (C) 2014 Arthur de Jong +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 USA + +"""Codice Fiscale (Italian tax code for individuals). + +The Codice Fiscale is an alphanumeric code of 16 characters used to +identify individuals residing in Italy. The number consists of three +characters derived from the person's last name, three from the person's +first name, five that hold information on the person's gender and birth +date, four that represent the person's place of birth and one check digit. + +>>> validate('RCCMNL83S18D969H') +'RCCMNL83S18D969H' +>>> validate('RCCMNL83S18D969') +Traceback (most recent call last): + ... +InvalidLength: ... +>>> calc_check_digit('RCCMNL83S18D969') +'H' +""" + +import re +import datetime + +from stdnum.exceptions import * +from stdnum.util import clean + + +# regular expression for matching fiscal codes +_code_re = re.compile( + '^[A-Z]{6}' + '[0-9LMNPQRSTUV]{2}[ABCDEHLMPRST]{1}[0-9LMNPQRSTUV]{2}' + '[A-Z]{1}[0-9LMNPQRSTUV]{3}[A-Z]{1}$') + +# encoding of birth day and year values (usually numeric but some letters +# may be substituted on clashes) +_date_digits = dict((x, n) for n, x in enumerate('0123456789')) +_date_digits.update(dict((x, n) for n, x in enumerate('LMNPQRSTUV'))) + +# encoding of month values (A = January, etc.) +_month_digits = dict((x, n) for n, x in enumerate('ABCDEHLMPRST')) + +# values of characters in even positions for checksum calculation +_even_values = dict((x, n) for n, x in enumerate('0123456789')) +_even_values.update( + dict((x, n) for n, x in enumerate('ABCDEFGHIJKLMNOPQRSTUVWXYZ'))) + +# values of characters in odd positions for checksum calculation +values = [1, 0, 5, 7, 9, 13, 15, 17, 19, 21, 2, 4, 18, 20, 11, 3, 6, 8, + 12, 14, 16, 10, 22, 25, 24, 23] +_odd_values = dict((x, values[n]) for n, x in enumerate('0123456789')) +_odd_values.update( + dict((x, values[n]) for n, x in enumerate('ABCDEFGHIJKLMNOPQRSTUVWXYZ'))) +del values + + +def compact(number): + """Convert the number to the minimal representation. This strips the + number of any valid separators and removes surrounding whitespace.""" + return clean(number, ' ').strip().upper() + + +def calc_check_digit(number): + """Compute the control code for the given number. The passed number + should be the first 15 characters of a fiscal code.""" + code = sum(_odd_values[x] if n % 2 == 0 else _even_values[x] + for n, x in enumerate(number)) + return 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'[code % 26] + + +def get_birth_date(number, minyear=1920): + """Get the birth date from the person's whose fiscal code. + + Only the last two digits of the year are stured in the number. The + dates will be returned in the range from minyear to minyear + 100. + + >>> get_birth_date('RCCMNL83S18D969H') + datetime.date(1983, 11, 18) + >>> get_birth_date('RCCMNL83S18D969H', minyear=1990) + datetime.date(2083, 11, 18) + """ + number = compact(number) + day = (_date_digits[number[9]] * 10 + _date_digits[number[10]]) % 40 + month = _month_digits[number[8]] + 1 + year = _date_digits[number[6]] * 10 + _date_digits[number[7]] + # find four-digit year + year += (minyear // 100) * 100 + if year < minyear: + year += 100 + try: + return datetime.date(year, month, day) + except ValueError: + raise InvalidComponent() + + +def get_gender(number): + """Get the gender of the person's provided fiscal code. + + >>> get_gender('RCCMNL83S18D969H') + 'M' + >>> get_gender('CNTCHR83T41D969D') + 'F' + """ + number = compact(number) + return 'M' if int(number[9:11]) < 32 else 'F' + + +def validate(number): + """Checks to see if the given fiscal code is valid. This checks the + length and whether the check digit is correct.""" + number = compact(number) + if len(number) != 16: + raise InvalidLength() + if not _code_re.match(number): + raise InvalidFormat() + if calc_check_digit(number[:-1]) != number[-1]: + raise InvalidChecksum() + # check if birth date is valid + birth_date = get_birth_date(number) + return number + + +def is_valid(number): + """Checks to see if the given fiscal code is valid. This checks the + length and whether the check digit is correct.""" + try: + return bool(validate(number)) + except ValidationError: + return False diff --git a/tests/test_it_codicefiscale.doctest b/tests/test_it_codicefiscale.doctest new file mode 100644 index 0000000..0dc033a --- /dev/null +++ b/tests/test_it_codicefiscale.doctest @@ -0,0 +1,129 @@ +test_it_codicefiscale.doctest - tests for the stdnum.it.codicefiscale module + +Copyright (C) 2009-2013 Emanuele Rocca +Copyright (C) 2014 Augusto Destrero +Copyright (C) 2014 Arthur de Jong + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +02110-1301 USA + + +This file contains more detailed doctests for the stdnum.it.codicefiscale +module. It tries to validate a number of tax codes that have been found +online. + +>>> from stdnum.it import codicefiscale +>>> from stdnum.exceptions import * + + +Some valid numbers + +>>> numbers = ''' +... MRTNTN23M02D969P +... RCCMNL83S18D969H +... MRSMSR81D60Z611H +... CNTCHR83T41D969D +... FOXDRA26C24H872Y +... MAILCU91A25F839D +... RSSMRA45C12F205C +... RSSMRA45C12F20RX +... RSSMRA45C12F2L5N +... RSSMRA45C12F2LRI +... RSSMRAQRCMNFNLRG +... ''' +>>> [x for x in numbers.splitlines() if x and not codicefiscale.is_valid(x)] +[] + + +These should be invalid: + +>>> codicefiscale.validate('CSTNGL22I10D086I') # the first 'I' shouldn't be there +Traceback (most recent call last): + ... +InvalidFormat: ... +>>> codicefiscale.validate('FOXDRA26C24H872A') +Traceback (most recent call last): + ... +InvalidChecksum: ... +>>> codicefiscale.validate('CNTCHR83T32D969H') # invalid date +Traceback (most recent call last): + ... +InvalidComponent: ... + + +Test getting the birth date. + +>>> codicefiscale.get_birth_date('MRTNTN23M02D969P') +datetime.date(1923, 8, 2) +>>> codicefiscale.get_birth_date('RCCMNL83S18D969H') +datetime.date(1983, 11, 18) +>>> codicefiscale.get_birth_date('MRSMSR81D60Z611H') +datetime.date(1981, 4, 20) +>>> codicefiscale.get_birth_date('CNTCHR83T41D969D') +datetime.date(1983, 12, 1) +>>> codicefiscale.get_birth_date('FOXDRA26C24H872Y') +datetime.date(1926, 3, 24) +>>> codicefiscale.get_birth_date('MAILCU91A25F839D') +datetime.date(1991, 1, 25) +>>> codicefiscale.get_birth_date('RSSMRA45C12F205C') +datetime.date(1945, 3, 12) +>>> codicefiscale.get_birth_date('RSSMRA45C12F20RX') +datetime.date(1945, 3, 12) +>>> codicefiscale.get_birth_date('RSSMRA45C12F2L5N') +datetime.date(1945, 3, 12) +>>> codicefiscale.get_birth_date('RSSMRA45C12F2LRI') +datetime.date(1945, 3, 12) +>>> codicefiscale.get_birth_date('RSSMRAQRCMNFNLRG') +datetime.date(1945, 3, 12) +>>> codicefiscale.get_birth_date('MRTNTN23M02D969P') +datetime.date(1923, 8, 2) + + +Test getting the gender. + +>>> codicefiscale.get_gender('MRTNTN23M02D969P') +'M' +>>> codicefiscale.get_gender('RCCMNL83S18D969H') +'M' +>>> codicefiscale.get_gender('RCDLSN84S16D969Z') +'M' +>>> codicefiscale.get_gender('MRSMSR81D60Z611H') +'F' +>>> codicefiscale.get_gender('CNTCHR83T41D969D') +'F' +>>> codicefiscale.get_gender('FOXDRA26C24H872Y') +'M' +>>> codicefiscale.get_gender('MAILCU91A25F839D') +'M' + + +Test calculating the check digit. + +>>> codicefiscale.calc_check_digit('MRTNTN23M02D969') +'P' +>>> codicefiscale.calc_check_digit('MRSMSR81D60Z611') +'H' +>>> codicefiscale.calc_check_digit('RCDLSN84S16D969') +'Z' +>>> codicefiscale.calc_check_digit('CNTCHR83T41D969') +'D' +>>> codicefiscale.calc_check_digit('BNCSFN85T58G702') +'W' +>>> codicefiscale.calc_check_digit('RCCMNL83S18D969') +'H' +>>> codicefiscale.calc_check_digit('FOXDRA26C24H872') +'Y' +>>> codicefiscale.calc_check_digit('MAILCU91A25F839') +'D' |