Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add italian id number generator #1450

Merged
merged 1 commit into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions src/main/java/net/datafaker/idnumbers/ItalianIdNumber.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package net.datafaker.idnumbers;

import static java.util.Locale.ROOT;
import static java.util.Map.entry;
import static net.datafaker.idnumbers.LatinLetters.isConsonant;
import static net.datafaker.idnumbers.LatinLetters.removeNonLatinLetters;
import static net.datafaker.idnumbers.Utils.birthday;
import static net.datafaker.idnumbers.Utils.gender;
import static net.datafaker.idnumbers.Utils.join;

import java.time.LocalDate;
import java.util.Map;
import java.util.stream.IntStream;

import net.datafaker.providers.base.BaseProviders;
import net.datafaker.providers.base.IdNumber.IdNumberRequest;
import net.datafaker.providers.base.PersonIdNumber;
import net.datafaker.providers.base.PersonIdNumber.Gender;
import net.datafaker.providers.base.Text.TextSymbolsBuilder;

/**
* See <a href="https://codicefiscale.com/#calcolo-completato">Italian national id numbers</a>
*/
public class ItalianIdNumber implements IdNumberGenerator {

private static final String REGION_CODE_FIRST_LETTERS = "ABCDEFGHIJKLM";
private static final String MONTH_LETTER = "_ABCDEHLMPRST";

private static final Map<Character, Integer> ODD_CHARACTERS = Map.ofEntries(
entry('0', 1), entry('C', 5), entry('O', 11),
entry('1', 0), entry('D', 7), entry('P', 3),
entry('2', 5), entry('E', 9), entry('Q', 6),
entry('3', 7), entry('F', 13), entry('R', 8),
entry('4', 9), entry('G', 15), entry('S', 12),
entry('5', 13), entry('H', 17), entry('T', 14),
entry('6', 15), entry('I', 19), entry('U', 16),
entry('7', 17), entry('J', 21), entry('V', 10),
entry('8', 19), entry('K', 2), entry('W', 22),
entry('9', 21), entry('L', 4), entry('X', 25),
entry('A', 1), entry('M', 18), entry('Y', 24),
entry('B', 0), entry('N', 20), entry('Z', 23)
);

private static final Map<Character, Integer> EVEN_CHARACTERS = Map.ofEntries(
entry('0', 0), entry('C', 2), entry('O', 14),
entry('1', 1), entry('D', 3), entry('P', 15),
entry('2', 2), entry('E', 4), entry('Q', 16),
entry('3', 3), entry('F', 5), entry('R', 17),
entry('4', 4), entry('G', 6), entry('S', 18),
entry('5', 5), entry('H', 7), entry('T', 19),
entry('6', 6), entry('I', 8), entry('U', 20),
entry('7', 7), entry('J', 9), entry('V', 21),
entry('8', 8), entry('K', 10), entry('W', 22),
entry('9', 9), entry('L', 11), entry('X', 23),
entry('A', 0), entry('M', 12), entry('Y', 24),
entry('B', 1), entry('N', 13), entry('Z', 25)
);

private static final Map<Integer, Character> DIVISION_REST = Map.ofEntries(
entry(0, 'A'), entry(10, 'K'), entry(20, 'U'),
entry(1, 'B'), entry(11, 'L'), entry(21, 'V'),
entry(2, 'C'), entry(12, 'M'), entry(22, 'W'),
entry(3, 'D'), entry(13, 'N'), entry(23, 'X'),
entry(4, 'E'), entry(14, 'O'), entry(24, 'Y'),
entry(5, 'F'), entry(15, 'P'), entry(25, 'Z'),
entry(6, 'G'), entry(16, 'Q'),
entry(7, 'H'), entry(17, 'R'),
entry(8, 'I'), entry(18, 'S'),
entry(9, 'J'), entry(19, 'T')
);

@Override
public String countryCode() {
return "IT";
}

@Override
public PersonIdNumber generateValid(BaseProviders faker, IdNumberRequest request) {
LocalDate birthday = birthday(faker, request);
Gender gender = gender(faker, request);
String firstName = faker.name().firstName().toUpperCase(ROOT);
String lastName = faker.name().lastName().toUpperCase(ROOT);
String basePart = encodeName(firstName) + encodeName(lastName) + encodeYear(birthday) + encodeMonth(birthday) + encodeDayAndGender(birthday, gender) + encodeRegion(faker);
return new PersonIdNumber(basePart + checksum(basePart), birthday, gender);
}

/**
* The first three letters are taken from the consonants of the name.
* In case of insufficient consonants the vowels are also taken, but they always come after the consonants.
* In case of too short names, "X" letter is appended.
*
* @param name first name of last name, UPPER CASE!
* @return 3 letters from the name
*/
String encodeName(String name) {
String latinLetters = removeNonLatinLetters(name);
IntStream consonants = latinLetters.chars().filter(c -> isConsonant(c));
IntStream vowels = latinLetters.chars().filter(c -> !isConsonant(c));
IntStream placeholder = "XXX".chars();
return join(consonants, vowels, placeholder, 3);
}

private String encodeYear(LocalDate birthday) {
return String.valueOf(birthday.getYear()).substring(2);
}

private char encodeMonth(LocalDate birthday) {
return MONTH_LETTER.charAt(birthday.getMonthValue());
}

private String encodeDayAndGender(LocalDate birthday, Gender gender) {
int day = switch (gender) {
case FEMALE -> 40 + birthday.getDayOfMonth();
case MALE -> birthday.getDayOfMonth();
};
return "%02d".formatted(day);
}

private String encodeRegion(BaseProviders faker) {
String regionLetter = faker.text().text(TextSymbolsBuilder.builder().len(1).with(REGION_CODE_FIRST_LETTERS).build());
return "%s%03d".formatted(regionLetter, faker.number().numberBetween(1, 1000));
}

@Override
public String generateInvalid(BaseProviders faker) {
String valid = generateValid(faker);
return valid.substring(0, valid.length() - 1) + '9';
}

char checksum(String basePart) {
int sum = 0;
for (int i = 0; i < basePart.length(); i += 2) {
sum += ODD_CHARACTERS.get(basePart.charAt(i));
}
for (int i = 1; i < basePart.length(); i += 2) {
sum += EVEN_CHARACTERS.get(basePart.charAt(i));
}
return DIVISION_REST.get(sum % 26);
}

}
21 changes: 21 additions & 0 deletions src/main/java/net/datafaker/idnumbers/LatinLetters.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package net.datafaker.idnumbers;

import java.util.Set;

public class LatinLetters {

private static final Set<Character> VOWELS = Set.of('A', 'a', 'E', 'e', 'I', 'i', 'O', 'o', 'U', 'u');

static boolean isConsonant(int c) {
return isConsonant((char) c);
}

static boolean isConsonant(char c) {
return !VOWELS.contains(c);
}

static String removeNonLatinLetters(String text) {
return text.replaceAll("[^a-zA-Z]+", "");
}

}
22 changes: 17 additions & 5 deletions src/main/java/net/datafaker/idnumbers/Utils.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package net.datafaker.idnumbers;

import static java.util.stream.Collectors.joining;
import static java.util.stream.IntStream.concat;
import static net.datafaker.providers.base.PersonIdNumber.Gender.FEMALE;
import static net.datafaker.providers.base.PersonIdNumber.Gender.MALE;

import java.time.LocalDate;
import java.util.stream.IntStream;

import net.datafaker.providers.base.BaseProviders;
import net.datafaker.providers.base.IdNumber;
import net.datafaker.providers.base.IdNumber.IdNumberRequest;
import net.datafaker.providers.base.PersonIdNumber.Gender;

import java.time.LocalDate;

import static net.datafaker.providers.base.PersonIdNumber.Gender.FEMALE;
import static net.datafaker.providers.base.PersonIdNumber.Gender.MALE;

public class Utils {

static LocalDate birthday(BaseProviders faker, IdNumberRequest request) {
Expand Down Expand Up @@ -44,4 +47,13 @@ static int multiply(String text, int[] weights) {
}
return checksum;
}

static String join(IntStream chars1, IntStream chars2, IntStream chars3, int maxLength) {
return concat(chars1, concat(chars2, chars3))
.limit(maxLength)
.mapToObj(c -> (char) c)
.map(Object::toString)
.collect(joining());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ net.datafaker.idnumbers.ChineseIdNumber
net.datafaker.idnumbers.EstonianIdNumber
net.datafaker.idnumbers.FrenchIdNumber
net.datafaker.idnumbers.GeorgianIdNumber
net.datafaker.idnumbers.ItalianIdNumber
kingthorin marked this conversation as resolved.
Show resolved Hide resolved
net.datafaker.idnumbers.MacedonianIdNumber
net.datafaker.idnumbers.MexicanIdNumber
net.datafaker.idnumbers.MoldovanIdNumber
Expand Down
1 change: 1 addition & 0 deletions src/test/java/net/datafaker/helpers/IdNumberPatterns.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ public class IdNumberPatterns {
public static final Pattern UKRAINIAN = Pattern.compile("\\d{8}-\\d{5}");
public static final Pattern SOUTH_AFRICAN = Pattern.compile("[0-9]{10}([01])8[0-9]");
public static final Pattern FRENCH = Pattern.compile("[12]\\d{2}(1[0-2]|0[1-9])\\d[0-9a-zA-Z]\\d{8}");
public static final Pattern ITALIAN = Pattern.compile("[A-Z]{6}\\d{2}[ABCDEHLMPRST]\\d{2}[\\dA-Z]{5}");

}
69 changes: 69 additions & 0 deletions src/test/java/net/datafaker/idnumbers/ItalianIdNumberTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package net.datafaker.idnumbers;

import static net.datafaker.providers.base.IdNumber.GenderRequest.ANY;
import static org.assertj.core.api.Assertions.assertThat;

import java.util.Locale;

import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Test;

import net.datafaker.Faker;
import net.datafaker.providers.base.IdNumber.IdNumberRequest;
import net.datafaker.providers.base.PersonIdNumber;

class ItalianIdNumberTest {

private static final Locale LOCALE = new Locale("it", "IT");
private final ItalianIdNumber impl = new ItalianIdNumber();

@Test
void consonants() {
assertThat(impl.encodeName("OOOOOP")).isEqualTo("POO");
assertThat(impl.encodeName("HELLO")).isEqualTo("HLL");
assertThat(impl.encodeName("HELLO")).isEqualTo("HLL");
assertThat(impl.encodeName("ANNA")).isEqualTo("NNA");
assertThat(impl.encodeName("AIROLI")).isEqualTo("RLA");
assertThat(impl.encodeName("AIROUE")).isEqualTo("RAI");
assertThat(impl.encodeName("FO")).isEqualTo("FOX");
assertThat(impl.encodeName("OF")).isEqualTo("FOX");
assertThat(impl.encodeName("F")).isEqualTo("FXX");
assertThat(impl.encodeName("D'AMICO")).isEqualTo("DMC");
assertThat(impl.encodeName("D`AMICO")).isEqualTo("DMC");
assertThat(impl.encodeName("D_AMICO")).isEqualTo("DMC");
assertThat(impl.encodeName("DE ROSA")).isEqualTo("DRS");
assertThat(impl.encodeName("ÜÜLAR ÄHO")).isEqualTo("LRH");
assertThat(impl.encodeName("")).isEqualTo("XXX");
}

/**
* Example from <a href="https://fiscomania.com/carattere-controllo/">web</a>
*/
@Test
void checksum() {
assertThat(impl.checksum("BBBTTT20H12X122")).isEqualTo('H');
assertThat(impl.checksum("AAAAAAAAAAAAAAA"))
.as("'A' is 1 on odd positions, 'A' is 0 on even positions. So, 1*8 + 7*0 = 8 -> 'I'")
.isEqualTo('I');
assertThat(impl.checksum("101010101010101"))
.as("'1' is 0 on odd positions, '0' is 0 on even positions. So, 0*8 + 0*0 = 0 -> 'A'")
.isEqualTo('A');
assertThat(impl.checksum("H01010101010101"))
.as("'H' is 17 on odd positions, '0' is 0 on even positions. So, 17 + 0*7 + 0*0 = 17 -> 'R'")
.isEqualTo('R');
}

@RepeatedTest(1000)
void checksumShouldMatchForValidCodes() {
PersonIdNumber personIdNumber = impl.generateValid(new Faker(LOCALE), new IdNumberRequest(1, 200, ANY));
String idNumber = personIdNumber.idNumber();
assertThat(impl.checksum(idNumber.substring(0, 15))).isEqualTo(idNumber.charAt(15));
}

@RepeatedTest(10)
void checksumShouldNotMatchForInvalidCodes() {
String idNumber = impl.generateInvalid(new Faker(LOCALE));
assertThat(impl.checksum(idNumber.substring(0, 15))).isNotEqualTo(idNumber.charAt(15));
}

}
34 changes: 34 additions & 0 deletions src/test/java/net/datafaker/idnumbers/LatinLettersTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package net.datafaker.idnumbers;

import static java.lang.Character.toUpperCase;
import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class LatinLettersTest {

@ParameterizedTest
@ValueSource(chars = {'a', 'e', 'i', 'o', 'u'})
void vowels(char vowel) {
assertThat(LatinLetters.isConsonant(vowel)).isFalse();
assertThat(LatinLetters.isConsonant(toUpperCase(vowel))).isFalse();
}

@ParameterizedTest
@ValueSource(chars = {'b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z'})
void consonants(char consonant) {
assertThat(LatinLetters.isConsonant(consonant)).isTrue();
assertThat(LatinLetters.isConsonant(toUpperCase(consonant))).isTrue();
}

@Test
void keepsOnlyLatinLetters() {
assertThat(LatinLetters.removeNonLatinLetters("John 1 Malkovi4")).isEqualTo("JohnMalkovi");
assertThat(LatinLetters.removeNonLatinLetters("πüäÄöÖ")).isEqualTo("");
assertThat(LatinLetters.removeNonLatinLetters("Ülar")).isEqualTo("lar");
assertThat(LatinLetters.removeNonLatinLetters("Андрей the Творожок")).isEqualTo("the");
}

}
12 changes: 12 additions & 0 deletions src/test/java/net/datafaker/providers/base/IdNumberTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class IdNumberTest extends BaseFakerTest<BaseFaker> {
private static final Faker ROMANIAN = new Faker(new Locale("ro", "RO"));
private static final Faker UKRAINIAN = new Faker(new Locale("uk", "UA"));
private static final Faker FRENCH = new Faker(new Locale("fr", "FR"));
private static final Faker ITALIAN = new Faker(new Locale("it", "IT"));

@Test
void testValid() {
Expand Down Expand Up @@ -198,6 +199,17 @@ void frenchIdNumber() {
assertThat(actual).matches(IdNumberPatterns.FRENCH);
}

@Test
void italianIdNumberSample() {
assertThatPin("PTRJHN89T04Z222B").matches(IdNumberPatterns.ITALIAN);
assertThatPin("BBBTTT20H12X122H").matches(IdNumberPatterns.ITALIAN);
}

@RepeatedTest(100)
void italianIdNumber() {
assertThatPin(ITALIAN.idNumber().valid()).matches(IdNumberPatterns.ITALIAN);
}

private static AbstractStringAssert<?> assertThatPin(String pin) {
return assertThat(pin)
.as(() -> "PIN: %s".formatted(pin));
Expand Down