diff --git a/README.md b/README.md index 6f857ba..d1a1b56 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,9 @@ codicefiscale.decode("CCCFBA85D03L219P") # "province": "TO", # "code": "L219", # }, +# "firstname_options": [ +# "Fabio", +# ], # "omocodes": [ # "CCCFBA85D03L219P", # "CCCFBA85D03L21VE", @@ -86,6 +89,9 @@ codicefiscale.decode("CCCFBA85D03L219P") # } ``` +> [!TIP] +> **Name suggestions**: The `firstname_options` field contains a list of possible first names matching the encoded firstname code. For Italian birthplaces, in approximately **60% of cases**, it returns a single name, providing near-certain identification. In other cases, it returns a list of possible names. For foreign birthplaces, the list is empty. + #### Check ```python codicefiscale.is_valid("CCCFBA85D03L219P") diff --git a/src/codicefiscale/__init__.py b/src/codicefiscale/__init__.py index 046a082..9ed17a4 100644 --- a/src/codicefiscale/__init__.py +++ b/src/codicefiscale/__init__.py @@ -1,3 +1,16 @@ +from codicefiscale.codicefiscale import ( + decode, + decode_firstname, + decode_raw, + encode, + encode_birthdate, + encode_birthplace, + encode_cin, + encode_firstname, + encode_lastname, + is_omocode, + is_valid, +) from codicefiscale.metadata import ( __author__, __copyright__, @@ -14,4 +27,15 @@ "__license__", "__title__", "__version__", + "decode", + "decode_firstname", + "decode_raw", + "encode", + "encode_birthdate", + "encode_birthplace", + "encode_cin", + "encode_firstname", + "encode_lastname", + "is_omocode", + "is_valid", ] diff --git a/src/codicefiscale/codicefiscale.py b/src/codicefiscale/codicefiscale.py index 248a3fa..d5fd7b2 100644 --- a/src/codicefiscale/codicefiscale.py +++ b/src/codicefiscale/codicefiscale.py @@ -5,7 +5,7 @@ from datetime import datetime, timedelta from itertools import combinations from re import Pattern -from typing import Any, Literal +from typing import Any, Literal, cast from dateutil import parser as date_parser from slugify import slugify @@ -82,7 +82,15 @@ _OMOCODIA_SUBS_INDEXES_COMBINATIONS.append(list(combo)) -_DATA: dict[str, dict[str, list[dict[str, Any]]]] = get_indexed_data() +_DATA: dict[str, Any] | None = None + + +def _get_data() -> dict[str, Any]: + global _DATA + if _DATA is None: + _DATA = get_indexed_data() + return _DATA + CODICEFISCALE_RE: Pattern[str] = re.compile( r"^" @@ -144,17 +152,18 @@ def _get_date( def _get_birthplace( birthplace: str, birthdate: datetime | str | None = None, -) -> dict[str, dict[str, Any]] | None: +) -> dict[str, Any] | None: birthplace_unicode_slug = slugify(birthplace, allow_unicode=True) birthplace_slug = slugify(birthplace) birthplace_code = birthplace_slug.upper() - birthplaces_options = _DATA["municipalities"].get( + data = _get_data() + birthplaces_options = data["municipalities"].get( birthplace_unicode_slug, - _DATA["municipalities"].get( + data["municipalities"].get( birthplace_slug, - _DATA["countries"].get( + data["countries"].get( birthplace_slug, - _DATA["codes"].get( + data["codes"].get( birthplace_code, ), ), @@ -165,7 +174,7 @@ def _get_birthplace( birthdate_date = _get_date(birthdate) if not birthdate_date: - return birthplaces_options[0].copy() + return cast(dict[str, Any], birthplaces_options[0].copy()) # search birthplace that has been created before / deleted after birthdate for birthplace_option in birthplaces_options: @@ -173,7 +182,7 @@ def _get_birthplace( date_deleted = _get_date(birthplace_option["date_deleted"]) or datetime.max # print(birthdate_date, date_created, date_deleted) if birthdate_date >= date_created and birthdate_date <= date_deleted: - return birthplace_option.copy() + return cast(dict[str, Any], birthplace_option.copy()) return _get_birthplace_fallback(birthplaces_options, birthdate_date) @@ -181,7 +190,7 @@ def _get_birthplace( def _get_birthplace_fallback( birthplaces_options: list[dict[str, Any]], birthdate_date: datetime, -) -> dict[str, dict[str, Any]] | None: +) -> dict[str, Any] | None: # avoid wrong birthplace code error when birthdate falls in # missing date-range in the data-source even if birthplace code is valid birthplaces_options_count = len(birthplaces_options) @@ -280,6 +289,43 @@ def encode_firstname(firstname: str) -> str: return firstname_code +def decode_firstname( + firstname_code: str, gender: Literal["m", "M", "f", "F"] | None = None +) -> list[str] | None: + """ + Decodes firstname code to possible italian first names. + + Returns a list of possible names that encode to the given code. + Only works for common italian names. + + :param firstname_code: The 3-character firstname code + :type firstname_code: string + :param gender: Optional gender filter ('M' or 'F') + :type gender: string | None + + :returns: List of possible first names, or None if not found + :rtype: list[str] | None + """ + firstname_code_upper = firstname_code.upper() + data = _get_data() + names_by_gender = cast( + dict[str, list[str]] | None, data["names"].get(firstname_code_upper) + ) + + if not names_by_gender: + return None + + if gender: + gender_upper = gender.upper() + if gender_upper in ("M", "F"): + gender_names = names_by_gender.get(gender_upper, []) + return gender_names if gender_names else None + + # return all names (both genders) if no gender specified + all_names = names_by_gender.get("M", []) + names_by_gender.get("F", []) + return sorted(set(all_names)) if all_names else None + + def encode_birthdate( birthdate: datetime | str | None, gender: Literal["m", "M", "f", "F"], @@ -448,7 +494,7 @@ def decode_raw(code: str) -> dict[str, str]: return data -def decode(code: str) -> dict[str, Any]: +def decode(code: str) -> dict[str, Any]: # noqa: C901 """ Decodes the italian fiscal code. @@ -466,11 +512,10 @@ def decode(code: str) -> dict[str, Any]: birthdate_month = _MONTHS.index(raw["birthdate_month"]) + 1 birthdate_day = int(raw["birthdate_day"].translate(_OMOCODIA_DECODE_TRANS)) + gender: Literal["M", "F"] = "M" if birthdate_day > 40: birthdate_day -= 40 gender = "F" - else: - gender = "M" current_year = datetime.now().year current_year_century_prefix = str(current_year)[0:-2] @@ -517,12 +562,19 @@ def decode(code: str) -> dict[str, Any]: f"expected {cin_check!r}, found {cin!r}" ) + # add possible first names if birthplace is in Italy (not foreign country) + firstname_options = None + is_foreign = birthplace and birthplace.get("province") == "EE" + if not is_foreign: + firstname_options = decode_firstname(raw["firstname"], gender) + data = { "code": code, "omocodes": _get_omocodes(code), "gender": gender, "birthdate": birthdate, "birthplace": birthplace, + "firstname_options": firstname_options or [], "raw": raw, } diff --git a/src/codicefiscale/data.py b/src/codicefiscale/data.py index c307ffb..c313292 100644 --- a/src/codicefiscale/data.py +++ b/src/codicefiscale/data.py @@ -2,7 +2,6 @@ import os import sys -from datetime import datetime from typing import Any import fsutil @@ -33,23 +32,31 @@ def get_countries_data() -> Any: return deleted_countries + countries -def get_indexed_data() -> dict[ - str, dict[str, list[dict[str, bool | datetime | str | list[str]]]] -]: +def get_names_data() -> Any: + names = get_data("names.json") + return names + + +def get_indexed_data() -> dict[str, Any]: + from codicefiscale.codicefiscale import encode_firstname + municipalities = get_municipalities_data() countries = get_countries_data() - data: dict[str, dict[str, list[dict[str, bool | datetime | str | list[str]]]]] = { + names = get_names_data() + + data: dict[str, Any] = { "municipalities": {}, "countries": {}, "codes": {}, + "names": {}, } for municipality in municipalities: code = municipality["code"] province = municipality["province"].lower() municipality_unicode_slug = slugify(municipality["name"], allow_unicode=True) - names = [municipality_unicode_slug] + municipality["name_slugs"] - for name in names: + municipality_names = [municipality_unicode_slug] + municipality["name_slugs"] + for name in municipality_names: name_and_province = f"{name}-{province}" data["municipalities"].setdefault(name, []) data["municipalities"].setdefault(name_and_province, []) @@ -60,11 +67,21 @@ def get_indexed_data() -> dict[ for country in countries: code = country["code"] - names = country["name_slugs"] - for name in names: + country_names = country["name_slugs"] + for name in country_names: data["countries"].setdefault(name, []) data["countries"][name].append(country) data["codes"].setdefault(code, []) data["codes"][code].append(country) + for gender, gender_names in names.items(): + for name in gender_names: + code = encode_firstname(name) + data["names"].setdefault(code, {"M": set(), "F": set()}) + data["names"][code][gender].add(name) + + for code in data["names"]: + data["names"][code]["M"] = sorted(data["names"][code]["M"]) + data["names"][code]["F"] = sorted(data["names"][code]["F"]) + return data diff --git a/src/codicefiscale/data/names.json b/src/codicefiscale/data/names.json new file mode 100644 index 0000000..f15c7be --- /dev/null +++ b/src/codicefiscale/data/names.json @@ -0,0 +1,164 @@ +{ + "M": [ + "Aaron", "Abbondanzio", "Abbondio", "Abdone", "Abelardo", "Abele", "Abramo", "Acacio", + "Accursio", "Achille", "Adalberto", "Adalgisio", "Adamo", "Adelfo", "Adelmo", "Adeolato", + "Adolfo", "Adone", "Adriano", "Agapito", "Agazio", "Agostino", "Aiace", "Aimone", + "Alan", "Albano", "Alberico", "Alberto", "Albino", "Alboino", "Alceo", "Alcide", + "Aldo", "Aleandro", "Aleardo", "Alessandro", "Alessio", "Alex", "Alfeo", "Alfio", "Alfonzo", + "Alfredo", "Algiso", "Aloisio", "Alvaro", "Amedeo", "Amerigo", "Amilcare", "Ambrogio", "Anacleto", "Anastasio", + "Andrea", "Angelo", "Aniceto", "Annibale", "Anselmo", "Antonino", "Antonio", + "Apollinare", "Arcangelo", "Archimede", "Arduino", "Aristide", "Armando", "Arnaldo", + "Arnoldo", "Arrigo", "Artemio", "Arturo", "Ascanio", "Assunto", "Attilio", "Augusto", + "Aurelio", "Aureliano", "Baldassarre", "Baldo", "Baldomero", "Baldovino", "Bardo", + "Barnaba", "Bartolomeo", "Basilio", "Bassiano", "Bastiano", "Battista", "Baudolino", + "Beda", "Belisario", "Beniamino", "Benigno", "Benito", "Benvenuto", "Berardo", + "Berengario", "Bernardo", "Bertoldo", "Berto", "Bertrand", "Biagio", "Bindo", "Bino", + "Bonagiunta", "Bonaldo", "Bonifacio", "Bonomo", "Boris", "Bortolo", "Brando", "Brian", + "Bruno", "Bryan", + "Caligero", "Calogero", "Calcedonio", "Calimero", "Callisto", "Camillo", "Candido", + "Canio", "Cantidio", "Carino", "Carlo", "Carmelo", "Carmine", "Casimiro", "Cassiano", + "Cassio", "Castro", "Cecilio", "Celestino", "Celso", "Cesare", "Cesario", "Cherubino", + "Cino", "Ciriaco", "Cirillo", "Cirino", "Ciro", "Claudio", "Clemente", "Cleto", + "Colombo", "Concetto", "Corrado", "Cosimo", "Costante", "Costantino", "Costanzo", + "Crescenzo", "Crisogono", "Cristiano", "Cristoforo", "Dacio", "Dagoberto", "Dalmazio", + "Damaso", "Damiano", "Daniele", "Danilo", "Danio", "Dante", "Dario", "Davide", + "Delio", "Delfino", "Demetrio", "Denis", "Dennis", "Deodato", "Desiderato", "Desiderio", "Diego", + "Dimitri", "Diodoro", "Diomede", "Dionigi", "Dionisio", "Domenico", "Donatello", + "Donato", "Dylan", "Edgardo", "Edmondo", "Edoardo", "Egidio", "Eligio", "Elio", "Eliodoro", "Eliseo", + "Elmo", "Elvio", "Emanuele", "Emidio", "Emiliano", "Emilio", "Ennio", "Enrico", + "Enzo", "Eraclio", "Eraldo", "Ercole", "Eric", "Erik", "Ermanno", "Ermenegildo", "Ermes", "Ermete", + "Erminio", "Ernesto", "Eros", "Errico", "Ethan", "Ettore", "Eugenio", "Eustachio", "Eusebio", + "Evaristo", "Ezio", "Fabiano", "Fabio", "Fabrizio", "Fausto", "Fedele", "Federico", + "Felice", "Feliciano", "Ferdinando", "Fernando", "Fermo", "Ferruccio", "Festo", "Fidel", "Firmino", + "Filiberto", "Filippo", "Filomeno", "Fiorenzo", "Flaminio", "Flaviano", "Flavio", + "Floriano", "Florindo", "Folco", "Fortunato", "Fosco", "Francesco", "Franco", + "Fulvio", "Furio", "Gabino", "Gabriele", "Gaetano", "Gaiaffo", "Galdino", "Galeazzo", + "Galileo", "Galliano", "Gandolfo", "Garibaldo", "Gaspare", "Gastone", "Gaudenzio", + "Gaudioso", "Gavino", "Gedeone", "Geminiano", "Gennaro", "Gerardo", "Gerasimo", "Geremia", + "Germano", "Gerolamo", "Geronimo", "Gerson", "Gervasio", "Gesualdo", "Getulio", + "Ghino", "Giacinto", "Giacobbe", "Giacomo", "Giambattista", "Gianbattista", + "Giancarlo", "Giandomenico", "Gianfranco", "Gianluca", "Gianmarco", "Gianmaria", + "Gianni", "Gianpaolo", "Gianpiero", "Gianpietro", "Gilberto", "Gino", "Gioacchino", + "Giobbe", "Gioele", "Giorgio", "Giosue", "Giovanni", "Giuliano", "Giulio", "Giuseppe", + "Giustino", "Giusto", "Goffredo", "Graziano", "Grazioso", "Gregorio", "Gualtiero", "Guglielmo", + "Guido", "Gustavo", "Iacopo", "Iago", "Icaro", "Idio", "Ignazio", "Igor", "Ilario", + "Ilvano", "Indro", "Innocenzo", "Ippolito", "Ireneo", "Isacco", "Isaia", "Isidoro", + "Italo", "Ivan", "Ivano", "Ivo", "Ivone", "Jacopo", "Jonathan", "Joshua", "Justin", + "Kevin", "Laerte", "Lamberto", "Lando", + "Landolfo", "Lanfranco", "Lauro", "Lazzaro", "Leandro", "Lelio", "Leo", "Leonardo", + "Leone", "Leonida", "Leonzio", "Leopoldo", "Letterio", "Libero", "Liberio", "Liborio", "Lidio", + "Lieto", "Lillo", "Lino", "Livio", "Lorenzo", "Loris", "Luca", "Luciano", "Lucio", + "Ludovico", "Luigi", "Macario", "Manfredo", "Manfredi", "Manilo", "Mansueto", + "Manuel", "Manuele", "Marcello", "Marciano", "Marco", "Mariano", "Marino", "Mario", + "Massimiliano", "Massimo", "Matteo", "Mattia", "Maurilio", "Maurizio", "Mauro", + "Max", "Medardo", "Melchiorre", "Melezio", "Menelao", "Michael", "Michele", "Michelangelo", + "Mirco", "Modesto", "Monaldo", "Nabile", "Nando", "Nanni", "Napoleone", "Narcisio", + "Nardo", "Nathan", "Nazzareno", "Nazzaro", "Nazario", "Nearco", "Nelio", "Nemesio", "Neopol", "Nereo", + "Neri", "Nerino", "Nestore", "Niccio", "Nicholas", "Nicodemo", "Nicola", "Nicolo", "Nilo", + "Nino", "Noah", "Noe", "Norberto", "Novello", "Nunzio", "Oberon", "Oberto", "Obizzo", + "Oddo", "Odilo", "Odino", "Odoacre", "Odoardo", "Olaf", "Olimpio", "Olindo", + "Olinto", "Omar", "Omero", "Onofrio", "Onorato", "Onorio", "Orazio", "Oreste", + "Orfeo", "Orio", "Orlando", "Ornaldo", "Orso", "Oscar", "Osvaldo", "Otello", + "Ottavio", "Ottone", "Pacifico", "Pacomio", "Palatino", "Palamede", "Palmerio", + "Palmiro", "Pancrazio", "Panfilo", "Pantaleo", "Paolo", "Paride", "Pasquale", + "Patrick", "Patrizio", "Patroclo", "Pellegrino", "Pierangelo", "Pierantonio", "Pierfrancesco", + "Piergiorgio", "Pierino", "Pierluigi", "Piero", "Pierpaolo", "Pietro", "Pio", + "Pippo", "Placido", "Platone", "Plinio", "Policarpo", "Polidorio", "Pompeo", + "Pompilio", "Pomponio", "Ponziano", "Ponzio", "Porfirio", "Primo", "Prospero", "Quarto", + "Quentino", "Querino", "Quintiliano", "Quintilio", "Quintino", "Quinto", "Quirino", + "Radames", "Raffaele", "Raimondo", "Rainerio", "Ramiro", "Raniele", "Raniero", + "Raoul", "Redento", "Reginaldo", "Regolo", "Remigio", "Remo", "Renato", "Renzo", + "Riccardo", "Rinaldo", "Rino", "Roberto", "Rocco", "Rodolfo", "Rodrigo", "Rolando", + "Romano", "Romeo", "Romolo", "Romualdo", "Rosario", "Ruben", "Ruggero", "Ruggiero", "Ryan", "Sabatino", + "Sabato", "Sabazio", "Sabino", "Salomone", "Salvatore", "Salverio", "Salvino", + "Salvo", "Samuele", "Sandro", "Sansone", "Santo", "Santorre", "Saro", "Sasha", "Sauro", + "Saverio", "Savino", "Sebastiano", "Secondiano", "Secondino", "Secondo", "Serafino", + "Sergio", "Sereno", "Settimio", "Severino", "Severo", "Sesto", "Sigismondo", + "Silvano", "Silverio", "Silvestro", "Silvio", "Simeone", "Simone", "Sinibaldo", + "Siro", "Sisto", "Sirio", "Socrate", "Stanislao", "Stefano", "Taddeo", "Tancredi", + "Tarquinio", "Tarsilio", "Tazio", "Ted", "Telemaco", "Telesforo", "Teo", "Teobaldo", + "Teodoro", "Teodosio", "Teofilo", "Terenzio", "Teresio", "Teseo", "Thomas", "Tiberio", + "Timoteo", "Tito", "Tobia", "Tolomeo", "Tommaso", "Toni", "Tony", "Tosco", "Tristano", + "Ubaldo", "Uberto", "Ugo", "Ugolino", "Ulderico", "Ulisse", "Ulpiano", "Ultimo", + "Umberto", "Urbano", "Uriele", "Valdemiro", "Valdo", "Valentino", "Valeriano", + "Valerio", "Valfredo", "Valter", "Vanni", "Vasco", "Varo", "Venanzio", "Venceslao", + "Venerando", "Veniero", "Ventura", "Vespasiano", "Vezio", "Vico", "Vid", "Vidal", + "Viden", "Vieri", "Vilfredo", "Vincenzo", "Vindonio", "Virgilio", "Virginio", + "Viscardo", "Vitale", "Vitaliano", "Vito", "Vittorio", "Walter", "Werner", "William", + "Willy", "Wladimiro", "Xeno", "Yago", "Yuri", "Zaccaria", "Zanetto", "Zeno", "Zenobio", + "Zoilo" + ], + "F": [ + "Aba", "Adelaide", "Addolorata", "Agata", "Agostina", "Agnese", "Aida", "Alba", "Albina", "Alberta", "Alda", "Alessandra", + "Alessia", "Alex", "Alfreda", "Alice", "Alida", "Alma", "Amalia", "Amanda", "Amber", "Ambrosia", "Amelia", + "Andrea", "Andreina", "Angela", + "Angelica", "Angiola", "Anita", "Anna", "Annunziata", "Antonella", "Antonia", "Antonina", + "Apollonia", "Armida", "Ashley", "Asia", "Assunta", "Assuntina", "Aurora", "Barbara", "Bartolomea", "Basilia", + "Beatrice", "Benedetta", "Benigna", "Benita", "Benvenuta", "Berta", "Bertilla", + "Betta", "Bethany", "Biagia", "Bianca", "Bibiana", "Bice", "Bonaria", "Brittany", "Bruna", "Calogera", + "Camelia", "Camilla", "Candida", "Capitolina", "Carina", "Carla", "Carmela", + "Carmen", "Carmina", "Carola", "Carolina", "Cassandra", "Caterina", "Catia", "Cecelia", + "Cecilia", "Celeste", "Celestina", "Cesarina", "Cherubina", "Chiara", "Cinzia", + "Cirilla", "Clara", "Claudia", "Clelia", "Clementina", "Clotilde", "Colomba", + "Concetta", "Concettina", "Consolata", "Cordelia", "Corinna", "Cornelia", "Costanza", "Crescenza", + "Cristiana", "Cristina", "Crystal", "Dacia", "Dafne", "Dalida", "Dalila", "Damiana", "Daniela", + "Debora", "Deborah", "Delfina", "Delia", "Delinda", "Demetria", "Desdemona", "Desiderata", + "Devota", "Diamante", "Diana", "Diletta", "Dilia", "Dina", "Dionisia", "Diva", + "Divina", "Dolores", "Domenica", "Donata", "Donatella", "Dorotea", "Edda", + "Edelberga", "Eden", "Editta", "Edvige", "Egle", "Elena", "Eleonora", "Elettra", + "Eliana", "Elisa", "Elisabetta", "Eloisa", "Elsa", "Elva", "Elvia", "Elvira", + "Emanuela", "Emerenziana", "Emilia", "Emma", "Emily", "Enrichetta", "Enza", "Erika", + "Ermenegilda", "Erminia", "Ernesta", "Ester", "Eugenia", "Eva", "Evelina", "Fabia", "Fabiana", + "Fabiola", "Fatima", "Fausta", "Faustina", "Fedele", "Federica", "Fedra", "Felicia", + "Feliciana", "Felicita", "Ferdinanda", "Fiamma", "Fiammetta", "Filippa", "Filomena", + "Fiona", "Fiorella", "Fiorenza", "Flaminia", "Flavia", "Flaviana", "Flora", + "Floriana", "Florinda", "Fosca", "Fortunata", "Franca", "Francesca", "Fulvia", "Gabriella", + "Gaia", "Galla", "Gaudenzia", "Gea", "Gelsomina", "Geltrude", "Gemma", "Gennara", + "Genoveffa", "Genziana", "Gerarda", "Germana", "Giacinta", "Giacomina", "Giada", + "Ginevra", "Gioconda", "Gioele", "Giorgia", "Giovanna", "Giovannina", "Giuditta", "Giulia", + "Giuliana", "Giulietta", "Giuseppa", "Giuseppina", "Giustina", "Grazia", "Graziella", + "Greta", "Guendalina", "Heather", "Hebe", "Helga", "Hersilia", "Hilaria", "Hilda", "Hilde", + "Honoria", "Ida", "Ifigenia", "Igina", "Ilaria", "Ilda", "Ilde", "Ildefonza", + "Ilenia", "Ilia", "Illuminata", "Ilva", "Imelda", "Immacolata", "Ines", "Innocenza", + "Iolanda", "Iole", "Ione", "Ippolita", "Irene", "Iris", "Irma", "Irmina", "Isa", + "Isabella", "Isotta", "Ivana", "Ivonne", "Jane", "Janet", "Jasmine", "Jennifer", + "Jenny", "Jessica", "Jole", "Jolanda", "Judith", "Julia", "Julian", "Julie", + "July", "Karen", "Kasia", "Katia", "Katrin", "Kati", "Kelly", "Kety", "Kimberly", + "Kira", "Krizia", "Lamberta", "Lara", "Laura", "Lauretta", "Laurenzia", "Laurina", "Lea", + "Leda", "Lelia", "Lella", "Lena", "Leonarda", "Leonella", "Leonilde", "Leontina", + "Leopolda", "Letizia", "Letteria", "Lia", "Liana", "Liberata", "Libera", "Liboria", + "Lidia", "Lidiana", "Lietta", "Ligeia", "Lilia", "Lilian", "Liliana", "Lina", + "Linda", "Lisa", "Livia", "Liviana", "Loredana", "Loreta", "Loretta", "Lorenza", + "Luana", "Luciana", "Lucia", "Lucilla", "Lucrezia", "Ludovica", "Luigia", "Luigina", "Luisa", + "Luna", "Maddalena", "Mafalda", "Magda", "Maia", "Maida", "Manuela", "Mara", + "Marcella", "Marzia", "Margherita", "Maria", "Mariangela", "Mariantonia", "Mariagrazia", "Marianna", "Marica", "Marilu", + "Marina", "Marisa", "Marta", "Martina", "Massima", "Matilde", "Maurilia", "Maura", + "Medea", "Melania", "Melissa", "Mercedes", "Mia", "Michela", "Michelina", "Michelle", "Milena", + "Mina", "Minerva", "Mira", "Mirella", "Miriam", "Monia", "Monica", "Morena", + "Nada", "Narcisa", "Nazzarena", "Neda", "Nedda", "Neera", "Nelia", "Nella", + "Nerea", "Nerina", "Nicla", "Nicole", "Nicoletta", "Nicolina", "Nilde", "Nilla", "Nimea", "Nina", "Ninetta", "Ninfa", + "Nives", "Noemi", "Norma", "Norvegia", "Novella", "Nubia", "Nunzia", + "Nunziata", "Odette", "Odilia", "Ofelia", "Olga", "Olimpia", "Olivia", "Onofria", + "Onorata", "Onorina", "Orchidea", "Oria", "Oriana", "Oriele", "Orietta", "Ornella", + "Orsola", "Ortensia", "Osvalda", "Ottavia", "Palmira", "Pamela", "Pandora", + "Paola", "Pasqua", "Pasqualina", "Patrizia", "Piera", "Pierangela", "Pierina", + "Pipa", "Placida", "Porzia", "Prassede", "Prisca", "Priscilla", "Prudenza", + "Queen", "Querina", "Quintilia", "Quintina", "Rachel", "Rachele", "Raffaella", "Raide", + "Raimonda", "Raisa", "Rebecca", "Redenta", "Regina", "Renata", "Resi", "Rina", + "Rita", "Roberta", "Romana", "Romilda", "Romina", "Rosilda", "Rosamunda", "Rosalba", "Rosalia", + "Rosalinda", "Rossana", "Rosanna", "Rosaria", "Rosa", "Rosetta", "Rosina", "Rossella", + "Sabata", "Sabatina", "Sabele", "Sabina", "Sabrina", "Saffo", "Saffira", "Salvatrice", + "Salve", "Salvia", "Salvina", "Samanta", "Samira", "Sandra", "Santa", "Santina", + "Sara", "Sarah", "Sasha", "Saveria", "Savina", "Scilla", "Scolastica", "Sebastiana", "Seconda", + "Secondina", "Sefora", "Selene", "Selena", "Selvaggia", "Serafina", "Serena", "Sharon", + "Silvana", "Silvia", "Simona", "Sofia", "Sonia", "Stefania", "Stephanie", "Susanna", "Sveva", + "Taide", "Tamara", "Tania", "Tarsilla", "Tarsilia", "Tea", "Teda", "Teresa", + "Teresina", "Teresita", "Tiffany", "Tilde", "Timotea", "Tina", "Tita", "Tiziana", "Tommasa", "Tommasina", + "Tosca", "Trinita", "Ubalda", "Uberta", "Ugolina", "Ulfa", "Ulpia", "Umberta", + "Urania", "Valda", "Valentina", "Valeria", "Valeriana", "Vanda", "Vanessa", "Vanna", + "Venera", "Veneranda", "Venere", "Ventura", "Vera", "Verena", "Veridiana", "Veronica", + "Vilma", "Vincenza", "Viola", "Violante", "Violetta", "Virginia", "Virna", "Vita", + "Vitaliana", "Vittoria", "Viviana", "Wanda", "Wera", "Wilma", "Xenia", "Yara", + "Ylenia", "Yonne", "Zaira", "Zenobia", "Zita", "Zoe", "Zosima" + ] +} diff --git a/tests/issues/test_issue_0079.py b/tests/issues/test_issue_0079.py index b2ad946..5d431cd 100644 --- a/tests/issues/test_issue_0079.py +++ b/tests/issues/test_issue_0079.py @@ -18,6 +18,7 @@ def test_issue_0079(): assert code == "RSSMRA04S29L219G" code_data = codicefiscale.decode("RSSMRA00S29L219C") code_data.pop("omocodes") + code_data.pop("firstname_options") expected_code_data = { "code": "RSSMRA00S29L219C", "gender": "M", diff --git a/tests/issues/test_issue_0162.py b/tests/issues/test_issue_0162.py index c14547c..f3fa991 100644 --- a/tests/issues/test_issue_0162.py +++ b/tests/issues/test_issue_0162.py @@ -11,6 +11,7 @@ def test_issue_0162(): assert codicefiscale.is_valid(code) code_data = codicefiscale.decode(code) code_data.pop("omocodes") + code_data.pop("firstname_options") expected_code_data = { "code": "DFLNTN42T20B860H", "gender": "M", diff --git a/tests/issues/test_issue_0203.py b/tests/issues/test_issue_0203.py index 42b6d19..950bbfc 100644 --- a/tests/issues/test_issue_0203.py +++ b/tests/issues/test_issue_0203.py @@ -13,6 +13,7 @@ def test_issue_0203(): code_data = codicefiscale.decode(code) code_data.pop("omocodes", None) + code_data.pop("firstname_options") expected_code_data = { "code": "MCHFDA07A01D877A", "gender": "M", diff --git a/tests/issues/test_issue_0210_0213.py b/tests/issues/test_issue_0210_0213.py index 13c2658..933215c 100644 --- a/tests/issues/test_issue_0210_0213.py +++ b/tests/issues/test_issue_0210_0213.py @@ -12,6 +12,7 @@ def test_issue_0210(): assert codicefiscale.is_valid(code) code_data = codicefiscale.decode(code) code_data.pop("omocodes") + code_data.pop("firstname_options") expected_code_data = { "code": "CCCFBA30H66F991T", "gender": "F", @@ -52,6 +53,7 @@ def test_issue_0213(): assert codicefiscale.is_valid(code) code_data = codicefiscale.decode(code) code_data.pop("omocodes") + code_data.pop("firstname_options") expected_code_data = { "code": "DBRGRZ53R66F059C", "gender": "F", diff --git a/tests/issues/test_issue_0246.py b/tests/issues/test_issue_0246.py index 1f73d97..b123c87 100644 --- a/tests/issues/test_issue_0246.py +++ b/tests/issues/test_issue_0246.py @@ -21,6 +21,7 @@ def test_issue_0246(): code_data = codicefiscale.decode(code) code_data.pop("omocodes", None) + code_data.pop("firstname_options") expected_code_data = { "code": "DGRFNC84T31G371E", "gender": "M", diff --git a/tests/test_cli.py b/tests/test_cli.py index dade7ed..2b42adc 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -125,6 +125,10 @@ def test_decode_without_omocodes(): "name_trans": "Roma", "province": "RM" }, + "firstname_options": [ + "Mario", + "Mauro" + ], "raw": { "code": "RSSMRA90A01H501W", "lastname": "RSS", @@ -168,6 +172,7 @@ def test_decode_without_omocodes(): "birthplace": "H501", "cin": "W", }, + "firstname_options": ["Mario", "Mauro"], } @@ -192,6 +197,10 @@ def test_decode_without_omocodes_from_command_line(): "name_trans": "Roma", "province": "RM" }, + "firstname_options": [ + "Mario", + "Mauro" + ], "raw": { "code": "RSSMRA90A01H501W", "lastname": "RSS", @@ -223,6 +232,7 @@ def test_decode_without_omocodes_from_command_line(): "name_trans": "Roma", "province": "RM", }, + "firstname_options": ["Mario", "Mauro"], "raw": { "code": "RSSMRA90A01H501W", "lastname": "RSS", @@ -397,6 +407,10 @@ def test_decode_with_omocodes(): "name_trans": "Roma", "province": "RM" }, + "firstname_options": [ + "Mario", + "Mauro" + ], "raw": { "code": "RSSMRA90A01H501W", "lastname": "RSS", @@ -558,6 +572,7 @@ def test_decode_with_omocodes(): "name_trans": "Roma", "province": "RM", }, + "firstname_options": ["Mario", "Mauro"], "raw": { "code": "RSSMRA90A01H501W", "lastname": "RSS", diff --git a/tests/test_decode.py b/tests/test_decode.py index 54dcbf0..9c14498 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -390,3 +390,63 @@ def test_decode_with_invalid_birthplace(): code = "FRTMXM74L15D354A" valid = codicefiscale.is_valid(code) assert not valid, "Expected invalid fiscal code" + + +def test_decode_firstname_options(): + """Test decoding fiscal code with firstname_options verification.""" + code = "CCCFBA85D03L219P" + decoded = codicefiscale.decode(code) + + firstname_options = decoded.get("firstname_options") + assert firstname_options is not None + assert "Fabio" in firstname_options + + +def test_decode_firstname_options_foreign_birthplace(): + """Test that firstname_options is empty for foreign birthplaces.""" + # foreign birthplace (Marocco - Z330) + foreign_code = "THDSDA95P08Z330H" + foreign_decoded = codicefiscale.decode(foreign_code) + + assert "firstname_options" in foreign_decoded + assert foreign_decoded["firstname_options"] == [] + + # italian birthplace (Torino - L219) + italian_code = "CCCFBA85D03L219P" + italian_decoded = codicefiscale.decode(italian_code) + + assert "firstname_options" in italian_decoded + assert len(italian_decoded["firstname_options"]) > 0 + assert isinstance(italian_decoded["firstname_options"], list) + + +def test_decode_firstname(): + """Test decode_firstname function with different parameters.""" + # test with gender M + names_m = codicefiscale.decode_firstname("FBA", "M") + assert names_m is not None + assert isinstance(names_m, list) + assert "Fabio" in names_m + + # test with gender F + names_f = codicefiscale.decode_firstname("FBA", "F") + assert names_f is not None + assert isinstance(names_f, list) + assert "Fabia" in names_f or "Fabiana" in names_f + + # test without gender (should return both M and F, deduplicated) + names_all = codicefiscale.decode_firstname("FBA") + assert names_all is not None + assert isinstance(names_all, list) + assert len(names_all) == len(set(names_all)) # no duplicates + assert len(names_all) >= max(len(names_m), len(names_f)) + + # test unisex name (Alex) - should appear only once + names_alex = codicefiscale.decode_firstname("LXA") + assert names_alex is not None + assert len(names_alex) == len(set(names_alex)) # no duplicates + assert names_alex.count("Alex") == 1 + + # test with invalid code + names_invalid = codicefiscale.decode_firstname("XXX") + assert names_invalid is None