diff --git a/src/main/java/net/datafaker/idnumbers/ItalianIdNumber.java b/src/main/java/net/datafaker/idnumbers/ItalianIdNumber.java
new file mode 100644
index 000000000..ffee56188
--- /dev/null
+++ b/src/main/java/net/datafaker/idnumbers/ItalianIdNumber.java
@@ -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 Codice fiscale
+ */
+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 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 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 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);
+ }
+
+}
diff --git a/src/main/java/net/datafaker/idnumbers/Utils.java b/src/main/java/net/datafaker/idnumbers/Utils.java
index 156e84a2d..54120101c 100644
--- a/src/main/java/net/datafaker/idnumbers/Utils.java
+++ b/src/main/java/net/datafaker/idnumbers/Utils.java
@@ -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());
@@ -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;
+ }
+
}
diff --git a/src/main/resources/META-INF/services/net.datafaker.idnumbers.IdNumberGenerator b/src/main/resources/META-INF/services/net.datafaker.idnumbers.IdNumberGenerator
index 4a78d7fb0..4f5be0476 100644
--- a/src/main/resources/META-INF/services/net.datafaker.idnumbers.IdNumberGenerator
+++ b/src/main/resources/META-INF/services/net.datafaker.idnumbers.IdNumberGenerator
@@ -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
diff --git a/src/test/java/net/datafaker/helpers/IdNumberPatterns.java b/src/test/java/net/datafaker/helpers/IdNumberPatterns.java
index 7cfce2c60..874d21d7c 100644
--- a/src/test/java/net/datafaker/helpers/IdNumberPatterns.java
+++ b/src/test/java/net/datafaker/helpers/IdNumberPatterns.java
@@ -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}");
}
diff --git a/src/test/java/net/datafaker/idnumbers/ItalianIdNumberTest.java b/src/test/java/net/datafaker/idnumbers/ItalianIdNumberTest.java
new file mode 100644
index 000000000..aeb665c68
--- /dev/null
+++ b/src/test/java/net/datafaker/idnumbers/ItalianIdNumberTest.java
@@ -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 web
+ */
+ @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));
+ }
+
+}
diff --git a/src/test/java/net/datafaker/providers/base/IdNumberTest.java b/src/test/java/net/datafaker/providers/base/IdNumberTest.java
index 627ebd90d..eb694071e 100644
--- a/src/test/java/net/datafaker/providers/base/IdNumberTest.java
+++ b/src/test/java/net/datafaker/providers/base/IdNumberTest.java
@@ -28,6 +28,7 @@ class IdNumberTest extends BaseFakerTest {
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() {
@@ -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));