Skip to content

Commit

Permalink
feat: add italian id number generator
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrei Solntsev committed Nov 21, 2024
1 parent e7151cc commit f588cf4
Show file tree
Hide file tree
Showing 5 changed files with 253 additions and 5 deletions.
144 changes: 144 additions & 0 deletions src/main/java/net/datafaker/idnumbers/ItalianIdNumber.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package net.datafaker.idnumbers;

import static java.lang.Integer.parseInt;
import static java.util.Locale.ROOT;
import static java.util.Map.entry;
import static net.datafaker.idnumbers.Utils.birthday;
import static net.datafaker.idnumbers.Utils.gender;
import static net.datafaker.idnumbers.Utils.isConsonant;
import static net.datafaker.idnumbers.Utils.join;
import static net.datafaker.providers.base.PersonIdNumber.Gender.FEMALE;

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">Codice fiscale</a>
*/
public class ItalianIdNumber implements IdNumberGenerator {

private static final int CHECKSUM_DIVIDER = 26;
private static final String REGION_CODE_FIRST_LETTERS = "ABCDEFGHIJKLM";

private static final String MONTH_LETTER = "_ABCDEHLMPRST";

private static final Map<Character, Integer> ODD_ALPHANUMERIC_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_ALPHANUMERIC_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> CONVERSION_RESULT_CHARACTERS = 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 input string
* @return first n constants
*/
String encodeName(String name) {
String latinLetters = name.replaceAll("[^A-Z]", "");
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) {
String birthdayValue = "%02d".formatted(birthday.getDayOfMonth());
return "" + (gender.equals(FEMALE) ? parseInt(birthdayValue) + 40 : birthdayValue);
}

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) {
char[] chars = basePart.toCharArray();

int sum = 0;
for (int i = 0; i < chars.length; i += 2) {
sum += ODD_ALPHANUMERIC_CHARACTERS.get(chars[i]);
}
for (int i = 1; i < chars.length; i += 2) {
sum += EVEN_ALPHANUMERIC_CHARACTERS.get(chars[i]);
}

return CONVERSION_RESULT_CHARACTERS.get(sum % CHECKSUM_DIVIDER);
}

}
32 changes: 27 additions & 5 deletions src/main/java/net/datafaker/idnumbers/Utils.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
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.Arrays;
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 {
private static final char[] VOWELS = {'A', 'E', 'I', 'O', 'U', 'Y'};

static LocalDate birthday(BaseProviders faker, IdNumberRequest request) {
return faker.timeAndDate().birthday(request.minAge(), request.maxAge());
Expand Down Expand Up @@ -44,4 +49,21 @@ 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());
}

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

static boolean isConsonant(char c) {
return Arrays.binarySearch(VOWELS, c) < 0;
}

}
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
net.datafaker.idnumbers.MacedonianIdNumber
net.datafaker.idnumbers.MexicanIdNumber
net.datafaker.idnumbers.MoldovanIdNumber
Expand Down
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));
}

}
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

0 comments on commit f588cf4

Please sign in to comment.