Arthur de Jong

Open Source / Free Software developer

summaryrefslogtreecommitdiffstats
path: root/stdnum
diff options
context:
space:
mode:
Diffstat (limited to 'stdnum')
-rw-r--r--stdnum/it/codicefiscale.py152
1 files changed, 152 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