Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/interim numbers #83

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/main/java/dev/personnummer/Options.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,20 @@
public class Options {
public Options(boolean allowCoordinationNumber) {
this.allowCoordinationNumber = allowCoordinationNumber;
this.allowInterimNumbers = false;
}

public Options(boolean allowCoordinationNumber, boolean allowInterimNumbers) {
this.allowCoordinationNumber = allowCoordinationNumber;
this.allowInterimNumbers = allowInterimNumbers;
}

public Options() {
this.allowInterimNumbers = false;
this.allowCoordinationNumber = true;
}

boolean allowCoordinationNumber = true;
final boolean allowInterimNumbers;

final boolean allowCoordinationNumber;
}
32 changes: 29 additions & 3 deletions src/main/java/dev/personnummer/Personnummer.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@
*/
public final class Personnummer implements Comparable<Personnummer> {
private static final Pattern regexPattern;
private static final Pattern interimPatternTest;
private static final String interimTestStr = "(?![-+])\\D";

static {
regexPattern = Pattern.compile("^(\\d{2})?(\\d{2})(\\d{2})(\\d{2})([-+]?)?((?!000)\\d{3})(\\d?)$");
regexPattern = Pattern.compile("^(\\d{2})?(\\d{2})(\\d{2})(\\d{2})([-+]?)?((?!000)\\d{3}|[TRSUWXJKLMN]\\d{2})(\\d?)$");
interimPatternTest = Pattern.compile(interimTestStr);
}

/**
Expand Down Expand Up @@ -111,6 +114,13 @@ public Personnummer(String personnummer, Options options) throws PersonnummerExc
throw new PersonnummerException("Failed to parse personal identity number. Invalid input.");
}

if (!options.allowInterimNumbers && interimPatternTest.matcher(personnummer).find()) {
throw new PersonnummerException(
personnummer +
" contains non-integer characters and options are set to not allow interim numbers"
);
}

Matcher matches = regexPattern.matcher(personnummer);
if (!matches.find()) {
throw new PersonnummerException("Failed to parse personal identity number. Invalid input.");
Expand Down Expand Up @@ -153,9 +163,14 @@ public Personnummer(String personnummer, Options options) throws PersonnummerExc

this.isMale = Integer.parseInt(Character.toString(this.numbers.charAt(2))) % 2 == 1;

String nums = matches.group(6);
if (options.allowInterimNumbers) {
nums = nums.replaceFirst(interimTestStr, "1");
}

// The format passed to Luhn method is supposed to be YYmmDDNNN
// Hence all numbers that are less than 10 (or in last case 100) will have leading 0's added.
if (luhn(String.format("%s%s%s%s", this.year, this.month, this.day, matches.group(6))) != Integer.parseInt(this.controlNumber)) {
if (luhn(String.format("%s%s%s%s", this.year, this.month, this.day, nums)) != Integer.parseInt(this.controlNumber)) {
throw new PersonnummerException("Invalid personal identity number.");
}
}
Expand Down Expand Up @@ -204,8 +219,19 @@ public String format(boolean longFormat) {
* @return True if valid.
*/
public static boolean valid(String personnummer) {
return valid(personnummer, new Options());
}

/**
* Validate a Swedish personal identity number.
*
* @param personnummer personal identity number to validate, as string.
* @param options options object.
* @return True if valid.
*/
public static boolean valid(String personnummer, Options options) {
try {
parse(personnummer);
parse(personnummer, options);
return true;
} catch (PersonnummerException ex) {
return false;
Expand Down
36 changes: 36 additions & 0 deletions src/test/java/DataProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
public class DataProvider {
private static final List<PersonnummerData> all = new ArrayList<>();
private static final List<PersonnummerData> orgNr = new ArrayList<>();
private static final List<PersonnummerData> interimNr = new ArrayList<>();

public static void initialize() throws IOException {
InputStream in = new URL("https://raw.githubusercontent.com/personnummer/meta/master/testdata/list.json").openStream();
Expand Down Expand Up @@ -55,8 +56,43 @@ public static void initialize() throws IOException {
current.getString("type")
));
}


in = new URL("https://raw.githubusercontent.com/personnummer/meta/master/testdata/interim.json").openStream();
reader = new BufferedReader(new InputStreamReader(in));
json = "";
while ((line = reader.readLine()) != null) {
json = json.concat(line);
}

in.close();
rootObject = new JSONArray(json);
for (int i = 0; i < rootObject.length(); i++) {
JSONObject current = rootObject.getJSONObject(i);
interimNr.add(new PersonnummerData(
current.getLong("integer"),
current.getString("long_format"),
current.getString("short_format"),
current.getString("separated_format"),
current.getString("separated_long"),
current.getBoolean("valid"),
current.getString("type"),
false, // ignore
false // ignore
)
);
}
}

public static List<PersonnummerData> getInterimNumbers() {
return interimNr;
}
public static List<PersonnummerData> getValidInterimNumbers() {
return interimNr.stream().filter(o -> o.valid).collect(Collectors.toList());
}
public static List<PersonnummerData> getInvalidInterimNumbers() {
return interimNr.stream().filter(o -> !o.valid).collect(Collectors.toList());
}
public static List<PersonnummerData> getCoordinationNumbers() {
return all.stream().filter(o -> !o.type.equals("ssn")).collect(Collectors.toList());
}
Expand Down
67 changes: 67 additions & 0 deletions src/test/java/InterimnummerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import java.io.IOException;

import dev.personnummer.*;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import static org.junit.jupiter.api.Assertions.*;

public class InterimnummerTest {
private static Options opts = new Options(true, true);

@BeforeAll
public static void setup() throws IOException {
DataProvider.initialize();
}

@ParameterizedTest
@MethodSource("DataProvider#getValidInterimNumbers")
public void testValidateInterim(PersonnummerData ssn) {
assertTrue(Personnummer.valid(ssn.longFormat, opts));
assertTrue(Personnummer.valid(ssn.shortFormat, opts));
}

@ParameterizedTest
@MethodSource("DataProvider#getInvalidInterimNumbers")
public void testValidateInvalidInterim(PersonnummerData ssn) {
assertFalse(Personnummer.valid(ssn.longFormat, opts));
assertFalse(Personnummer.valid(ssn.shortFormat, opts));
}

@ParameterizedTest
@MethodSource("DataProvider#getValidInterimNumbers")
public void testFormatLongInterim(PersonnummerData ssn) throws PersonnummerException {
Personnummer pnr = Personnummer.parse(ssn.longFormat, opts);

assertEquals(pnr.format(false), ssn.separatedFormat);
assertEquals(pnr.format(true), ssn.longFormat);
}

@ParameterizedTest
@MethodSource("DataProvider#getValidInterimNumbers")
public void testFormatShortInterim(PersonnummerData ssn) throws PersonnummerException {
Personnummer pnr = Personnummer.parse(ssn.shortFormat, opts);

assertEquals(pnr.format(false), ssn.separatedFormat);
assertEquals(pnr.format(true), ssn.longFormat);
}

@ParameterizedTest
@MethodSource("DataProvider#getInvalidInterimNumbers")
public void testInvalidInterimThrows(PersonnummerData ssn) {
assertThrows(PersonnummerException.class, () -> Personnummer.parse(ssn.shortFormat, opts));
assertThrows(PersonnummerException.class, () -> Personnummer.parse(ssn.longFormat, opts));
assertThrows(PersonnummerException.class, () -> Personnummer.parse(ssn.separatedLong, opts));
assertThrows(PersonnummerException.class, () -> Personnummer.parse(ssn.separatedFormat, opts));
}

@ParameterizedTest
@MethodSource("DataProvider#getValidInterimNumbers")
public void testInterimThrowsIfNotActive(PersonnummerData ssn) {
assertThrows(PersonnummerException.class, () -> Personnummer.parse(ssn.shortFormat));
assertThrows(PersonnummerException.class, () -> Personnummer.parse(ssn.longFormat));
assertThrows(PersonnummerException.class, () -> Personnummer.parse(ssn.separatedLong));
assertThrows(PersonnummerException.class, () -> Personnummer.parse(ssn.separatedFormat));
}
}