diff --git a/src/main/java/dev/personnummer/Options.java b/src/main/java/dev/personnummer/Options.java index 7d8fef1..30f3197 100644 --- a/src/main/java/dev/personnummer/Options.java +++ b/src/main/java/dev/personnummer/Options.java @@ -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; } diff --git a/src/main/java/dev/personnummer/Personnummer.java b/src/main/java/dev/personnummer/Personnummer.java index f1210e6..02c8e80 100644 --- a/src/main/java/dev/personnummer/Personnummer.java +++ b/src/main/java/dev/personnummer/Personnummer.java @@ -13,9 +13,12 @@ */ public final class Personnummer implements Comparable { 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); } /** @@ -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."); @@ -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."); } } @@ -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; diff --git a/src/test/java/DataProvider.java b/src/test/java/DataProvider.java index 240b332..f880262 100644 --- a/src/test/java/DataProvider.java +++ b/src/test/java/DataProvider.java @@ -10,6 +10,7 @@ public class DataProvider { private static final List all = new ArrayList<>(); private static final List orgNr = new ArrayList<>(); + private static final List interimNr = new ArrayList<>(); public static void initialize() throws IOException { InputStream in = new URL("https://raw.githubusercontent.com/personnummer/meta/master/testdata/list.json").openStream(); @@ -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 getInterimNumbers() { + return interimNr; + } + public static List getValidInterimNumbers() { + return interimNr.stream().filter(o -> o.valid).collect(Collectors.toList()); + } + public static List getInvalidInterimNumbers() { + return interimNr.stream().filter(o -> !o.valid).collect(Collectors.toList()); + } public static List getCoordinationNumbers() { return all.stream().filter(o -> !o.type.equals("ssn")).collect(Collectors.toList()); } diff --git a/src/test/java/InterimnummerTest.java b/src/test/java/InterimnummerTest.java new file mode 100644 index 0000000..c0bac6d --- /dev/null +++ b/src/test/java/InterimnummerTest.java @@ -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)); + } +}