-
Notifications
You must be signed in to change notification settings - Fork 176
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add italian id number generator
- Loading branch information
Andrei Solntsev
committed
Nov 21, 2024
1 parent
e7151cc
commit c6cb512
Showing
7 changed files
with
262 additions
and
5 deletions.
There are no files selected for viewing
141 changes: 141 additions & 0 deletions
141
src/main/java/net/datafaker/idnumbers/ItalianIdNumber.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 input string | ||
* @return first n constants | ||
*/ | ||
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); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package net.datafaker.idnumbers; | ||
|
||
import java.util.Arrays; | ||
|
||
public class LatinLetters { | ||
|
||
private static final char[] VOWELS = {'A', 'E', 'I', 'O', 'U', 'Y'}; | ||
|
||
static boolean isConsonant(int c) { | ||
return isConsonant((char) c); | ||
} | ||
|
||
static boolean isConsonant(char c) { | ||
return Arrays.binarySearch(VOWELS, c) < 0; | ||
} | ||
|
||
static String removeNonLatinLetters(String text) { | ||
return text.replaceAll("[^A-Z]", ""); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
69 changes: 69 additions & 0 deletions
69
src/test/java/net/datafaker/idnumbers/ItalianIdNumberTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters