Arthur de Jong

Open Source / Free Software developer

summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--stdnum/it/codicefiscale.py152
-rw-r--r--tests/test_it_codicefiscale.doctest129
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'