|
| 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 | +} |
0 commit comments