diff --git a/README b/README index c0953cf9..15a3e7c6 100644 --- a/README +++ b/README @@ -170,6 +170,7 @@ Currently this package supports the following formats: * PESEL (Polish national identification number) * REGON (Rejestr Gospodarki Narodowej, Polish register of economic units) * NIF (Número de identificação fiscal, Portuguese VAT number) + * CC (Número de Cartão de Cidadão, Portuguese Identity number) * RUC number (Registro Único de Contribuyentes, Paraguay tax number) * CF (Cod de înregistrare în scopuri de TVA, Romanian VAT number) * CNP (Cod Numeric Personal, Romanian Numerical Personal Code) diff --git a/docs/index.rst b/docs/index.rst index 03e1cfc7..9cac2d71 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -267,6 +267,7 @@ Available formats pl.nip pl.pesel pl.regon + pt.cc pt.nif py.ruc ro.cf diff --git a/docs/stdnum.pt.cc.rst b/docs/stdnum.pt.cc.rst new file mode 100644 index 00000000..52d23bae --- /dev/null +++ b/docs/stdnum.pt.cc.rst @@ -0,0 +1,5 @@ +stdnum.pt.cc +============= + +.. automodule:: stdnum.pt.cc + :members: diff --git a/stdnum/pt/cc.py b/stdnum/pt/cc.py new file mode 100644 index 00000000..a8e4b1e7 --- /dev/null +++ b/stdnum/pt/cc.py @@ -0,0 +1,110 @@ +# cc.py - functions for handling Portuguese Identity numbers +# coding: utf-8 +# +# Copyright (C) 2021 David Vaz +# +# 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 + +"""CC (Número de Cartão de Cidadão, Portuguese Identity number). + +The Portuguese Identity Number is alfanumeric with the following format: + +DDDDDDDD C AAT + +with: +D - Civil Identity Number [0-9] +C - Civil Identity Number Check Digit [0-9] +A - Version [A-Z0-9] +T - Identity Number Check Digit [0-9] + +Full definition in Portuguese can be found here: +https://www.autenticacao.gov.pt/documents/20126/115760/Valida%C3%A7%C3%A3o+de+N%C3%BAmero+de+Documento+do+Cart%C3%A3o+de+Cidad%C3%A3o.pdf + +>>> validate('00000000 0 ZZ4') +'000000000ZZ4' +>>> validate('00000000 A ZZ4') # invalid format +Traceback (most recent call last): + ... +InvalidFormat: ... +>>> validate('00000000 0 ZZ3') # invalid check digits +Traceback (most recent call last): + ... +InvalidChecksum: ... +""" +import re + +from stdnum.exceptions import * +from stdnum.util import clean, isdigits + +_cc_re = re.compile(r'^\d*[A-Z0-9]{2}\d$') + + +def compact(number): + """Convert the number to the minimal representation. This strips the + number of any valid separators and removes surrounding whitespace.""" + number = clean(number, ' ').upper().strip() + return number + + +def _alphabet_convert(char): + """ + alphabet table conversion: + 0: 0, .. 9: 9 + A:10, B: 11 .. Z:35 + """ + if isdigits(char): + return ord(char) - ord('0') + return ord(char) - ord('A') + 10 + + +def calc_check_digit(number): + """Calculate the check digits for the number.""" + value_sum = 0 + for i in range(len(number) - 1, -1, -1): + value = _alphabet_convert(number[i]) + if i % 2 == 0: + value *= 2 + if value > 9: + value -= 9 + value_sum += value + return str(10 - value_sum % 10) + + +def validate(number): + """checks if the number is a valid cartao de cidadao number""" + number = compact(number) + + if not _cc_re.match(number): + raise InvalidFormat() + + if calc_check_digit(number[:-1]) != number[-1]: + raise InvalidChecksum() + + return number + + +def is_valid(number): + """Check if the number is a valid cartao de cidadao number""" + try: + return bool(validate(number)) + except ValidationError: + return False + + +def format(number): + """Reformat the number to the standard presentation format.""" + number = compact(number) + return ' '.join([number[:-4], number[-4], number[-3:]])