Skip to content

Commit 9e7a84e

Browse files
authored
Personal Identification Number provider for Norway (NO) (#1681)
* Personal Identification Number provider for Norway (NO) * [code review] add integration tests with Norwegian locale * [code review] simplify generation of sequence number instead of having CENTURIES Map of Intervals, just use the old good "if-else" :)
1 parent 1081815 commit 9e7a84e

5 files changed

Lines changed: 211 additions & 0 deletions

File tree

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package net.datafaker.idnumbers;
2+
3+
import net.datafaker.providers.base.BaseProviders;
4+
import net.datafaker.providers.base.IdNumber.IdNumberRequest;
5+
import net.datafaker.providers.base.PersonIdNumber;
6+
import net.datafaker.providers.base.PersonIdNumber.Gender;
7+
8+
import java.time.LocalDate;
9+
import java.time.format.DateTimeFormatter;
10+
11+
import static net.datafaker.idnumbers.Utils.birthday;
12+
import static net.datafaker.idnumbers.Utils.gender;
13+
14+
/**
15+
* The Norwegian Individual ID Number is 11 digits log
16+
* Format: ddMMyyCCGkk
17+
* <ul>
18+
* <li>dd - day of birth</li>
19+
* <li>MM - month of birth</li>
20+
* <li>yy - year of birth</li>
21+
* <li>CC - sequence (range depends on century)</li>
22+
* <li>G - a random digit (even for females, odd for males)</li>
23+
* <li>kk - checksum</li>
24+
* </ul>
25+
* <p>
26+
* Example: 11077941012
27+
* </p>
28+
*
29+
* @see <a href="https://www.skatteetaten.no/en/person/national-registry/identitetsnummer-og-elektronisk-id/fodselsnummer/">Overview</a>
30+
* @see <a href="https://taxid.pro/">Online generator</a>
31+
* @see <a href="https://www.oecd.org/content/dam/oecd/en/topics/policy-issue-focus/aeoi/norway-tin.pdf">More on centuries</a>
32+
*/
33+
public class NorwegianIdNumber implements IdNumberGenerator {
34+
private static final DateTimeFormatter BIRTHDAY_FORMAT = DateTimeFormatter.ofPattern("ddMMyy");
35+
36+
private static final int[] CHECKSUM_COEFFICIENTS_K1 = {3, 7, 6, 1, 8, 9, 4, 5, 2};
37+
private static final int[] CHECKSUM_COEFFICIENTS_K2 = {5, 4, 3, 2, 7, 6, 5, 4, 3, 2};
38+
39+
@Override
40+
public String countryCode() {
41+
return "NO";
42+
}
43+
44+
@Override
45+
public PersonIdNumber generateValid(BaseProviders faker, IdNumberRequest request) {
46+
LocalDate birthday = birthday(faker, request);
47+
Gender gender = gender(faker, request);
48+
49+
String basePart = basePart(faker, birthday, gender);
50+
String idNumber = "%s%02d".formatted(basePart, checksum(basePart));
51+
return new PersonIdNumber(idNumber, birthday, gender);
52+
}
53+
54+
@Override
55+
public String generateInvalid(BaseProviders faker) {
56+
String valid = generateValid(faker);
57+
String basePart = valid.substring(0, valid.length() - 2);
58+
int invalidChecksum = (checksum(basePart) + 1) % 100;
59+
return "%s%02d".formatted(basePart, invalidChecksum);
60+
}
61+
62+
String basePart(BaseProviders faker, LocalDate birthday, Gender gender) {
63+
String birthdayDigits = BIRTHDAY_FORMAT.format(birthday);
64+
int sequenceNumber = generateSequenceNumber(faker, birthday.getYear());
65+
int genderDigit = genderDigit(faker, gender);
66+
67+
return "%s%02d%s".formatted(birthdayDigits, sequenceNumber, genderDigit);
68+
}
69+
70+
/**
71+
* <ul>
72+
* <li>born 1854-1899: allocated from series 749-500</li>
73+
* <li>born 1900-1999: allocated from series 499-000</li>
74+
* <li>born 1940-1999: also allocated from series 999-900</li>
75+
* <li>born 2000-2039: allocated from series 999-500</li>
76+
* </ul>
77+
*
78+
* @see <a href="https://www.oecd.org/content/dam/oecd/en/topics/policy-issue-focus/aeoi/norway-tin.pdf">TIN Structure</a>
79+
*/
80+
private int generateSequenceNumber(BaseProviders faker, int year) {
81+
if (year >= 1854 && year <= 1899) {
82+
return faker.random().nextInt(50, 74);
83+
} else if (year >= 1940 && year <= 1999) {
84+
return faker.random().nextInt(90, 99);
85+
} else if (year >= 1900 && year <= 1999) {
86+
return faker.random().nextInt(0, 49);
87+
} else if (year >= 2000 && year <= 2039) {
88+
return faker.random().nextInt(50, 99);
89+
} else {
90+
throw new IllegalArgumentException("Birthday is not in range of supported in Norway");
91+
}
92+
}
93+
94+
private int genderDigit(BaseProviders faker, Gender gender) {
95+
return switch (gender) {
96+
case FEMALE -> faker.options().option(0, 2, 4, 6, 8);
97+
case MALE -> faker.options().option(1, 3, 5, 7, 9);
98+
};
99+
}
100+
101+
/**
102+
* k1 = 11 - ((3 × d1 + 7 × d2 + 6 × m1 + 1 × m2 + 8 × å1 + 9 × å2 + 4 × i1 + 5 × i2 + 2 × i3) mod 11),
103+
* k2 = 11 - ((5 × d1 + 4 × d2 + 3 × m1 + 2 × m2 + 7 × å1 + 6 × å2 + 5 × i1 + 4 × i2 + 3 × i3 + 2 × k1) mod 11).
104+
*/
105+
int checksum(String numbers) {
106+
int k1 = modulo11(numbers, CHECKSUM_COEFFICIENTS_K1);
107+
int k2 = modulo11(numbers + k1, CHECKSUM_COEFFICIENTS_K2);
108+
return k1 * 10 + k2;
109+
}
110+
111+
private int modulo11(String numbers, int[] checksumCoefficients) {
112+
int checkSum = 0;
113+
for (int i = 0; i < numbers.length(); i++) {
114+
int digit = Character.getNumericValue(numbers.charAt(i));
115+
checkSum += checksumCoefficients[i] * digit;
116+
}
117+
118+
return (11 - checkSum % 11) % 10;
119+
}
120+
121+
}

src/main/resources/META-INF/services/net.datafaker.idnumbers.IdNumberGenerator

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ net.datafaker.idnumbers.LatvianIdNumber
1212
net.datafaker.idnumbers.MacedonianIdNumber
1313
net.datafaker.idnumbers.MexicanIdNumber
1414
net.datafaker.idnumbers.MoldovanIdNumber
15+
net.datafaker.idnumbers.NorwegianIdNumber
1516
net.datafaker.idnumbers.PolishIdNumber
1617
net.datafaker.idnumbers.PortugueseIdNumber
1718
net.datafaker.idnumbers.RomanianIdNumber

src/test/java/net/datafaker/helpers/IdNumberPatterns.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ public class IdNumberPatterns {
1414
public static final Pattern HUNGARIAN = Pattern.compile("[1-4]\\d{9}");
1515
public static final Pattern ESTONIAN = Pattern.compile("[1-6][0-9]{10}");
1616
public static final Pattern BRAZILIAN = Pattern.compile("\\d{3}\\.\\d{3}\\.\\d{3}-\\d{2}");
17+
public static final Pattern NORWEGIAN = Pattern.compile("\\d{11}");
1718

1819
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package net.datafaker.idnumbers;
2+
3+
import net.datafaker.Faker;
4+
import net.datafaker.providers.base.PersonIdNumber;
5+
import org.junit.jupiter.api.RepeatedTest;
6+
import org.junit.jupiter.params.ParameterizedTest;
7+
import org.junit.jupiter.params.provider.CsvSource;
8+
9+
import java.time.LocalDate;
10+
11+
import static java.lang.Integer.parseInt;
12+
import static org.assertj.core.api.Assertions.assertThat;
13+
14+
class NorwegianIdNumberTest {
15+
private final NorwegianIdNumber generator = new NorwegianIdNumber();
16+
private final Faker faker = new Faker();
17+
18+
@RepeatedTest(100)
19+
void valid() {
20+
String pin = generator.generateValid(faker);
21+
assertThat(pin)
22+
.as(() -> "Presumably valid PIN: '%s'".formatted(pin))
23+
.hasSize(11)
24+
.matches("\\d{11}");
25+
}
26+
27+
@ParameterizedTest
28+
@CsvSource({
29+
"110779410, 12",
30+
"010203987, 67",
31+
})
32+
void checksum(String basePart, int expectedChecksum) {
33+
assertThat(generator.checksum(basePart)).isEqualTo(expectedChecksum);
34+
}
35+
36+
@ParameterizedTest
37+
@CsvSource({
38+
"1854-12-31, MALE, 311254..ODD, 50, 74",
39+
"1854-12-31, FEMALE, 311254..EVEN, 50, 74",
40+
"1900-01-01, MALE, 010100..ODD, 0, 49 ",
41+
"1940-12-31, MALE, 311240..ODD, 90, 99",
42+
"2010-12-31, MALE, 311210..ODD, 50, 99",
43+
})
44+
void basePart(
45+
LocalDate birthday,
46+
PersonIdNumber.Gender gender,
47+
String expectedRegex,
48+
int expectedI12Min,
49+
int expectedI12Max
50+
) {
51+
String pin = generator.basePart(faker, birthday, gender);
52+
assertThat(pin)
53+
.as(() -> "PIN '%s' does not match %s".formatted(pin, expectedRegex))
54+
.hasSize(9)
55+
.matches(expectedRegex.replace("ODD", "[13579]").replace("EVEN", "[02468]").replace(".", "\\d"));
56+
57+
assertThat(parseInt(pin.substring(6, 8)))
58+
.isBetween(expectedI12Min, expectedI12Max);
59+
}
60+
61+
@RepeatedTest(100)
62+
void invalid() {
63+
String generated = generator.generateInvalid(faker);
64+
String basePart = generated.substring(0, generated.length() - 2);
65+
int checksum = parseInt(generated.substring(generated.length() - 2));
66+
int validChecksum = generator.checksum(basePart);
67+
68+
assertThat(generated)
69+
.describedAs("Invalid ID number should have a valid length")
70+
.hasSize(11)
71+
.describedAs("Invalid ID number should consist from numbers only")
72+
.matches("\\d+");
73+
assertThat(checksum)
74+
.describedAs("Checksum should be broken")
75+
.isNotEqualTo(validChecksum);
76+
}
77+
}

src/test/java/net/datafaker/providers/base/IdNumberTest.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class IdNumberTest {
4141
private static final Faker ITALIAN = new Faker(new Locale("it", "IT"));
4242
private static final Faker HUNGARIAN = new Faker(new Locale("hu", "HU"));
4343
private static final Faker BRAZILIAN = new Faker(new Locale("pt", "BR"));
44+
private static final Faker NORWEGIAN = new Faker(new Locale("no", "NO"));
4445

4546
@Test
4647
void testValid() {
@@ -267,6 +268,16 @@ void hungarianIdNumber_invalid() {
267268
assertThatPin(HUNGARIAN.idNumber().invalid()).matches(IdNumberPatterns.HUNGARIAN);
268269
}
269270

271+
@RepeatedTest(100)
272+
void norwegianIdNumber_valid() {
273+
assertThatPin(NORWEGIAN.idNumber().valid()).matches(IdNumberPatterns.NORWEGIAN);
274+
}
275+
276+
@RepeatedTest(100)
277+
void norwegianIdNumber_invalid() {
278+
assertThatPin(NORWEGIAN.idNumber().invalid()).matches(IdNumberPatterns.NORWEGIAN);
279+
}
280+
270281
private static AbstractStringAssert<?> assertThatPin(String pin) {
271282
return assertThat(pin)
272283
.as(() -> "PIN: %s".formatted(pin));

0 commit comments

Comments
 (0)