diff --git a/LICENSE b/LICENSE index 2e08098..46a61ed 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017-2019 - Personnummer and Contributors +Copyright (c) 2017-2020 - Personnummer and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index c3f39bb..ea496da 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,91 @@ # Personnummer -[![Build Status](https://travis-ci.org/personnummer/java.svg?branch=master)](https://travis-ci.org/personnummer/java) +[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/personnummer/java/Test)](https://github.com/personnummer/java/actions) -Validate Swedish personal identity numbers +Validate Swedish personal identity numbers. -## Example +## Installation -```java -class Test { - public void main(String[] args){ - Personnummer.valid(6403273813L); // => True - Personnummer.valid("19130401+2931"); // => True +Add the github repository as a Maven or Gradle repository: + +```xml + + dev.personnummer + personnummer + 3.*.* + +``` + +```groovy +plugins { + id 'maven' +} + +repositories { + maven { + url "https://github.com/personnummer/java:personnummer" } } + +dependencies { + configuration("dev.personnummer:personnummer") +} +``` + +For more information on how to install and authenticate with github packages, check [this link](https://help.github.com/en/packages/using-github-packages-with-your-projects-ecosystem/configuring-apache-maven-for-use-with-github-packages). + +## Examples + +### Validation + +```java +import dev.personnummer.*; + +class Test +{ + public void TestValidation() + { + Personnummer.valid("191212121212"); // => True + Personnummer.valid("12121+21212"); // => True + Personnummer.valid("2012121-21212"); // => True + } +} +``` + +### Format + +```java +// Short format (YYMMDD-XXXX) +(new Personnummer("1212121212")).format(); +// => 121212-1212 + +// Short format for 100+ years old +(new Personnummer("191212121212")).format(); +//=> 121212+1212 + +// Long format (YYYYMMDDXXXX) +Personnummer.parse("1212121212").format(true); +//=> 201212121212 +``` + +### Age + +```java +(new Personnummer("1212121212")).getAge(); +//=> 7 +``` + +### Get sex + +```java +(new Personnummer("1212121212")).isMale(); +//=> true +Personnummer.parse("1212121212").isFemale(); +//=> false ``` -See [`src/test/java/PersonnummerTest.java`](src/test/java/PersonnummerTest.java) for more examples. +See `src/test//PersonnummerTest.java` for more examples. ## License -[MIT](LICENSE) +[MIT](https://github.com/personnummer/java/blob/master/LICENSE) diff --git a/build.gradle b/build.gradle index 26cf9f9..f5b7c23 100644 --- a/build.gradle +++ b/build.gradle @@ -1,20 +1,19 @@ plugins { - id("maven-publish") + id 'maven-publish' + id 'java-library' + id 'java' } -apply plugin: 'java-library' -apply plugin: 'java' - sourceCompatibility = 1.8 targetCompatibility = 1.8 repositories { - jcenter() + mavenCentral() } dependencies { - testImplementation 'junit:junit:4.12' - testCompile ("junit:junit:4.12", "org.json:json:20200518") + testImplementation('org.junit.jupiter:junit-jupiter:5.6.2') + testCompile('org.junit.jupiter:junit-jupiter:5.6.2', "org.json:json:20200518") } @@ -27,6 +26,7 @@ jar { } test { + useJUnitPlatform() testLogging { events "passed", "skipped", "failed" exceptionFormat "full" diff --git a/src/main/java/Personnummer.java b/src/main/java/Personnummer.java deleted file mode 100644 index ac717bb..0000000 --- a/src/main/java/Personnummer.java +++ /dev/null @@ -1,97 +0,0 @@ -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Class used to validate Swedish personal identity numbers. - * - * @author Johannes Tegnér - */ -public final class Personnummer { - private static final Pattern regexPattern; - - static { - regexPattern = Pattern.compile("^(\\d{2})?(\\d{2})(\\d{2})(\\d{2})([-|+]?)?((?!000)\\d{3})(\\d?)$"); - } - - private Personnummer() { - throw new AssertionError("Class cannot be instantiated"); - } - - /** - * Validate a Swedish personal identity number. - * - * @param value personal identity number to validate, as string. - * @return True if valid. - */ - public static boolean valid(String value) { - if (value == null) { - return false; - } - - Matcher matches = regexPattern.matcher(value); - if (!matches.find()) { - return false; - } - - int year, month, day, control, number; - try { - String y = matches.group(2); - year = Integer.parseInt((y.length() == 4 ? y.substring(2) : y)); - month = Integer.parseInt(matches.group(3)); - day = Integer.parseInt(matches.group(4)); - control = Integer.parseInt(matches.group(7)); - number = Integer.parseInt(matches.group(6)); - } catch (NumberFormatException e) { - return false; - } - - // 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. - int luhn = luhn(String.format("%02d%02d%02d%03d0", year, month, day, number)); - return (luhn == control) && (testDate(year, month, day) || testDate(year, month, day - 60)); - } - - /** - * Validate a Swedish personal identity number. - * - * @param value personal identity number to validate, as long. - * @return True if valid. - */ - public static boolean valid(long value) { - return valid(Long.toString(value)); - } - - private static int luhn(String value) { - // Luhn/mod10 algorithm. Used to calculate a checksum from the - // passed value. The checksum is returned and tested against the control number - // in the personal identity number to make sure that it is a valid number. - - int temp; - int sum = 0; - - for (int i = 0; i < value.length(); i++) { - temp = Character.getNumericValue(value.charAt(i)); - temp *= 2 - (i % 2); - if (temp > 9) - temp -= 9; - - sum += temp; - } - - return (int)(Math.ceil((double)sum / 10.0) * 10.0 - (double)sum); - } - - private static boolean testDate(int year, int month, int day) { - try { - DateFormat df = new SimpleDateFormat("yy-MM-dd"); - df.setLenient(false); - df.parse(String.format("%02d-%02d-%02d", year, month, day)); - return true; - } catch (Exception ex) { - return false; - } - } - -} diff --git a/src/main/java/dev/personnummer/Options.java b/src/main/java/dev/personnummer/Options.java new file mode 100644 index 0000000..7d8fef1 --- /dev/null +++ b/src/main/java/dev/personnummer/Options.java @@ -0,0 +1,12 @@ +package dev.personnummer; + +public class Options { + public Options(boolean allowCoordinationNumber) { + this.allowCoordinationNumber = allowCoordinationNumber; + } + + public Options() { + } + + boolean allowCoordinationNumber = true; +} diff --git a/src/main/java/dev/personnummer/Personnummer.java b/src/main/java/dev/personnummer/Personnummer.java new file mode 100644 index 0000000..cc638ec --- /dev/null +++ b/src/main/java/dev/personnummer/Personnummer.java @@ -0,0 +1,222 @@ +package dev.personnummer; + +import java.time.LocalDate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Class used to validate Swedish personal identity numbers. + * + * @author Johannes Tegnér + */ +public final class Personnummer { + private static final Pattern regexPattern; + + static { + regexPattern = Pattern.compile("^(\\d{2})?(\\d{2})(\\d{2})(\\d{2})([-|+]?)?((?!000)\\d{3})(\\d?)$"); + } + + /** + * Create a new Personnummber object from a string. + * In case options is not passed, they will default to accept any personal and coordination numbers. + * + * @param personnummer Personal identity number as a string to create the object from. + * @param options Options to use when creating the object. + * @throws PersonnummerException On parse error. + */ + public static Personnummer parse(String personnummer, Options options) throws PersonnummerException { + return new Personnummer(personnummer, options); + } + + /** + * Create a new Personnummber object from a string. + * In case options is not passed, they will default to accept any personal and coordination numbers. + * + * @param personnummer Personal identity number as a string to create the object from. + * @throws PersonnummerException On parse error. + */ + public static Personnummer parse(String personnummer) throws PersonnummerException { + return parse(personnummer, new Options()); + } + + + private final int realDay; + private final String fullYear; + private final String century; + private final String year; + private final String month; + private final String day; + private final String numbers; + private final String controlNumber; + private final boolean isMale; + private final boolean isFemale; + + public Boolean isMale() { + return this.isMale; + } + + public Boolean isFemale() { + return this.isFemale; + } + + public String separator() { + return this.getAge() >= 100 ? "+" : "-"; + } + + public String getFullYear() { + return fullYear; + } + + public String getCentury() { + return century; + } + + public String getYear() { + return year; + } + + public String getMonth() { + return month; + } + + public String getDay() { + return day; + } + + public String getNumbers() { + return numbers; + } + + public String getControlNumber() { + return controlNumber; + } + + public int getAge() { + return (LocalDate.of(Integer.parseInt(this.fullYear), Integer.parseInt(this.month), this.realDay).until(LocalDate.now())).getYears(); + } + + /** + * Create a new Personnummber object from a string. + * In case options is not passed, they will default to accept any personal and coordination numbers. + * + * @param personnummer Personal identity number as a string to create the object from. + * @param options Options to use when creating the object. + * @throws PersonnummerException On parse error. + */ + public Personnummer(String personnummer, Options options) throws PersonnummerException { + if (personnummer == null) { + throw new PersonnummerException("Failed to parse personal identity number. Invalid input."); + } + + Matcher matches = regexPattern.matcher(personnummer); + if (!matches.find()) { + throw new PersonnummerException("Failed to parse personal identity number. Invalid input."); + } + + String century; + String decade = matches.group(2); + if (matches.group(1) != null && !matches.group(1).isEmpty()) { + century = matches.group(1); + } else { + int born = LocalDate.now().getYear() - Integer.parseInt(decade); + if (!matches.group(5).isEmpty() && matches.group(5).equals("+")) { + born -= 100; + } + + century = Integer.toString(born).substring(0, 2); + } + + int day = Integer.parseInt(matches.group(4)); + if (options.allowCoordinationNumber) { + day = day > 60 ? day - 60 : day; + } else if(day > 60) { + throw new PersonnummerException("Invalid personal identity number."); + } + + this.realDay = day; + this.century = century; + this.year = decade; + this.fullYear = century + decade; + this.month = matches.group(3); + this.day = matches.group(4); + this.numbers = matches.group(6) + matches.group(7); + this.controlNumber = matches.group(7); + + this.isMale = Integer.parseInt(Character.toString(this.numbers.charAt(2))) % 2 == 1; + this.isFemale = !this.isMale; + + // 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)) { + throw new PersonnummerException("Invalid personal identity number."); + } + } + + /** + * Create a new Personnummber object from a string. + * In case options is not passed, they will default to accept any personal and coordination numbers. + * + * @param personnummer Personal identity number as a string to create the object from. + * @throws PersonnummerException On parse error. + */ + public Personnummer(String personnummer) throws PersonnummerException { + this(personnummer, new Options()); + } + + /** + * Format the personal identity number into a valid string (YYMMDD-/+XXXX) + * If longFormat is true, it will include the century (YYYYMMDD-/+XXXX) + * + * @return Formatted personal identity number. + */ + public String format() { + return format(false); + } + + /** + * Format the personal identity number into a valid string (YYMMDD-/+XXXX) + * If longFormat is true, it will include the century (YYYYMMDD-/+XXXX) + * + * @param longFormat If century should be included. + * @return Formatted personal identity number. + */ + public String format(boolean longFormat) { + return (longFormat ? this.fullYear : this.year) + this.month + this.day + (longFormat ? "" : separator()) + numbers; + } + + /** + * Validate a Swedish personal identity number. + * + * @param personnummer personal identity number to validate, as string. + * @return True if valid. + */ + public static boolean valid(String personnummer) { + try { + parse(personnummer); + return true; + } catch (PersonnummerException ex) { + return false; + } + } + + private static int luhn(String value) { + // Luhn/mod10 algorithm. Used to calculate a checksum from the + // passed value. The checksum is returned and tested against the control number + // in the personal identity number to make sure that it is a valid number. + + int temp; + int sum = 0; + + for (int i = 0; i < value.length(); i++) { + temp = Character.getNumericValue(value.charAt(i)); + temp *= 2 - (i % 2); + if (temp > 9) + temp -= 9; + + sum += temp; + } + + return (int)(Math.ceil((double)sum / 10.0) * 10.0 - (double)sum); + } + +} diff --git a/src/main/java/dev/personnummer/PersonnummerException.java b/src/main/java/dev/personnummer/PersonnummerException.java new file mode 100644 index 0000000..be13965 --- /dev/null +++ b/src/main/java/dev/personnummer/PersonnummerException.java @@ -0,0 +1,7 @@ +package dev.personnummer; + +public class PersonnummerException extends Exception { + PersonnummerException(String message) { + super(message); + } +} diff --git a/src/test/java/DataProvider.java b/src/test/java/DataProvider.java new file mode 100644 index 0000000..65302e0 --- /dev/null +++ b/src/test/java/DataProvider.java @@ -0,0 +1,57 @@ +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.*; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class DataProvider { + private static final List all = new ArrayList<>(); + + public static void initialize() throws IOException { + InputStream in = new URL("https://raw.githubusercontent.com/personnummer/meta/master/testdata/list.json").openStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(in)); + String json = ""; + String line; + while ((line = reader.readLine()) != null) { + json = json.concat(line); + } + JSONArray rootObject = new JSONArray(json); + for (int i = 0; i < rootObject.length(); i++) { + JSONObject current = rootObject.getJSONObject(i); + all.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"), + current.getBoolean("isMale"), + current.getBoolean("isFemale") + )); + } + } + + public static List getCoordinationNumbers() { + return all.stream().filter(o -> !o.type.equals("ssn")).collect(Collectors.toList()); + } + public static List getPersonnummer() { + return all.stream().filter(o -> o.type.equals("ssn")).collect(Collectors.toList()); + } + public static List getInvalidCoordinationNumbers() { + return getCoordinationNumbers().stream().filter(o -> !o.valid).collect(Collectors.toList()); + } + public static List getInvalidPersonnummer() { + return getPersonnummer().stream().filter(o -> !o.valid).collect(Collectors.toList()); + } + public static List getValidCoordinationNumbers() { + return getCoordinationNumbers().stream().filter(o -> o.valid).collect(Collectors.toList()); + } + public static List getValidPersonnummer() { + return getPersonnummer().stream().filter(o -> o.valid).collect(Collectors.toList()); + } + +} diff --git a/src/test/java/PersonnummerData.java b/src/test/java/PersonnummerData.java new file mode 100644 index 0000000..b86e6b3 --- /dev/null +++ b/src/test/java/PersonnummerData.java @@ -0,0 +1,25 @@ +public class PersonnummerData { + + public PersonnummerData(Long integer, String longFormat, String shortFormat, String separatedFormat, String separatedLong, Boolean valid, String type, Boolean isMale, Boolean isFemale) { + this.integer = integer; + this.longFormat = longFormat; + this.shortFormat = shortFormat; + this.separatedFormat = separatedFormat; + this.separatedLong = separatedLong; + this.valid = valid; + this.type = type; + this.isMale = isMale; + this.isFemale = isFemale; + } + + public Long integer; + public String longFormat; + public String shortFormat; + public String separatedFormat; + public String separatedLong; + public Boolean valid; + public String type; + public Boolean isMale; + public boolean isFemale; + +} diff --git a/src/test/java/PersonnummerTest.java b/src/test/java/PersonnummerTest.java index 29c39d1..4cd4598 100644 --- a/src/test/java/PersonnummerTest.java +++ b/src/test/java/PersonnummerTest.java @@ -1,136 +1,178 @@ -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Test; import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.util.ArrayList; -import java.util.List; -import org.json.*; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; -public class PersonnummerTest { - private static Boolean fileLoaded = false; +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.*; - private static List validSsnInt = new ArrayList<>(); - private static List validSsnString = new ArrayList<>(); - private static List invalidSsnInt = new ArrayList<>(); - private static List invalidSsnString = new ArrayList<>(); - private static List validConInt = new ArrayList<>(); - private static List validConString = new ArrayList<>(); - private static List invalidConInt = new ArrayList<>(); - private static List invalidConString = new ArrayList<>(); +public class PersonnummerTest { - @AfterClass - public static void deleteTestData() throws IOException { - Files.delete(Paths.get("temp.json")); + @BeforeAll + public static void setup() throws IOException { + DataProvider.initialize(); } - @BeforeClass - public static void loadTestData() throws IOException { - if (fileLoaded) { - return; - } + @ParameterizedTest + @MethodSource("DataProvider#getValidPersonnummer") + public void testConstructor(PersonnummerData ssn) { + assertDoesNotThrow(() -> new Personnummer(ssn.longFormat, new Options(false))); + assertDoesNotThrow(() -> new Personnummer(ssn.shortFormat, new Options(false))); + assertDoesNotThrow(() -> new Personnummer(ssn.separatedFormat, new Options(false))); + assertDoesNotThrow(() -> new Personnummer(ssn.separatedFormat, new Options(false))); + } - if (!Files.exists(Paths.get("temp.json"))) { - InputStream in = new URL("https://raw.githubusercontent.com/personnummer/meta/master/testdata/structured.json").openStream(); - Files.copy(in, Paths.get("temp.json"), StandardCopyOption.REPLACE_EXISTING); - fileLoaded = true; - } + @ParameterizedTest + @MethodSource("DataProvider#getValidCoordinationNumbers") + public void testConstructorCoord(PersonnummerData ssn) { + assertDoesNotThrow(() -> new Personnummer(ssn.longFormat, new Options(true))); + assertDoesNotThrow(() -> new Personnummer(ssn.shortFormat, new Options(true))); + assertDoesNotThrow(() -> new Personnummer(ssn.separatedFormat, new Options(true))); + assertDoesNotThrow(() -> new Personnummer(ssn.separatedFormat, new Options(true))); + } - String jsonString = new String(Files.readAllBytes(Paths.get("temp.json"))); - JSONObject json = new JSONObject(jsonString); + @ParameterizedTest + @MethodSource({"DataProvider#getInvalidPersonnummer", "DataProvider#getValidCoordinationNumbers"}) + public void testConstructorInvalid(PersonnummerData ssn) { + assertThrows(PersonnummerException.class, () -> new Personnummer(ssn.longFormat, new Options(false))); + assertThrows(PersonnummerException.class, () -> new Personnummer(ssn.shortFormat, new Options(false))); + assertThrows(PersonnummerException.class, () -> new Personnummer(ssn.separatedFormat, new Options(false))); + assertThrows(PersonnummerException.class, () -> new Personnummer(ssn.separatedFormat, new Options(false))); + } - JSONObject ssn = json.getJSONObject("ssn"); - JSONObject con = json.getJSONObject("con"); + @ParameterizedTest + @MethodSource({"DataProvider#getInvalidCoordinationNumbers"}) + public void testConstructorCoordInvalid(PersonnummerData ssn) { + assertThrows(PersonnummerException.class, () -> new Personnummer(ssn.longFormat, new Options(true))); + assertThrows(PersonnummerException.class, () -> new Personnummer(ssn.shortFormat, new Options(true))); + assertThrows(PersonnummerException.class, () -> new Personnummer(ssn.separatedFormat, new Options(true))); + assertThrows(PersonnummerException.class, () -> new Personnummer(ssn.separatedFormat, new Options(true))); + } - validSsnInt = getIntList(ssn, "integer", "valid"); - invalidSsnInt = getIntList(ssn, "integer", "invalid"); - validSsnString = getStringList(ssn, "string", "valid"); - invalidSsnString = getStringList(ssn, "string", "invalid"); + @ParameterizedTest + @MethodSource("DataProvider#getValidPersonnummer") + public void testParse(PersonnummerData ssn) { + assertDoesNotThrow(() -> Personnummer.parse(ssn.longFormat, new Options(false))); + assertDoesNotThrow(() -> Personnummer.parse(ssn.shortFormat, new Options(false))); + assertDoesNotThrow(() -> Personnummer.parse(ssn.separatedFormat, new Options(false))); + assertDoesNotThrow(() -> Personnummer.parse(ssn.separatedFormat, new Options(false))); + } - validConInt = getIntList(con, "integer", "valid"); - invalidConInt = getIntList(con, "integer", "invalid"); - validConString = getStringList(con, "string", "valid"); - invalidConString = getStringList(con, "string", "invalid"); + @ParameterizedTest + @MethodSource({"DataProvider#getValidCoordinationNumbers", "DataProvider#getValidPersonnummer"}) + public void testParseCoord(PersonnummerData ssn) { + assertDoesNotThrow(() -> Personnummer.parse(ssn.longFormat, new Options(true))); + assertDoesNotThrow(() -> Personnummer.parse(ssn.shortFormat, new Options(true))); + assertDoesNotThrow(() -> Personnummer.parse(ssn.separatedFormat, new Options(true))); + assertDoesNotThrow(() -> Personnummer.parse(ssn.separatedFormat, new Options(true))); } - private static ArrayList getStringList(JSONObject root, String dataType, String valid) { - JSONArray arr = root.getJSONObject(dataType).getJSONArray(valid); - ArrayList result = new ArrayList<>(); - for (int i=0; i Personnummer.parse(ssn.longFormat, new Options(false))); + assertThrows(PersonnummerException.class, () -> Personnummer.parse(ssn.shortFormat, new Options(false))); + assertThrows(PersonnummerException.class, () -> Personnummer.parse(ssn.separatedFormat, new Options(false))); + assertThrows(PersonnummerException.class, () -> Personnummer.parse(ssn.separatedFormat, new Options(false))); } - private static ArrayList getIntList(JSONObject root, String dataType, String valid) { - JSONArray arr = root.getJSONObject(dataType).getJSONArray(valid); - ArrayList result = new ArrayList<>(); - for (int i=0; i Personnummer.parse(ssn.longFormat, new Options(true))); + assertThrows(PersonnummerException.class, () -> Personnummer.parse(ssn.shortFormat, new Options(true))); + assertThrows(PersonnummerException.class, () -> Personnummer.parse(ssn.separatedFormat, new Options(true))); + assertThrows(PersonnummerException.class, () -> Personnummer.parse(ssn.separatedFormat, new Options(true))); } - @Test - public void testPersonnNummerWithInvalidIntegerValues() { - for (Long ssn: invalidSsnInt) { - assertFalse(Personnummer.valid(ssn)); - } + + @ParameterizedTest + @MethodSource({"DataProvider#getValidPersonnummer"}) + public void testAge(PersonnummerData ssn) throws PersonnummerException { + LocalDate date = LocalDate.parse(ssn.longFormat.substring(0, ssn.longFormat.length() - 4), DateTimeFormatter.ofPattern("yyyyMMdd")); + int years = (date.until(LocalDate.now())).getYears(); + + assertEquals(years, Personnummer.parse(ssn.separatedLong, new Options(false)).getAge()); + assertEquals(years, Personnummer.parse(ssn.separatedFormat, new Options(false)).getAge()); + assertEquals(years, Personnummer.parse(ssn.longFormat, new Options(false)).getAge()); + assertEquals(years > 99 ? years - 100 : years, Personnummer.parse(ssn.shortFormat, new Options(false)).getAge()); } - @Test - public void testCoordinationNummerWithInvalidIntegerValues() { - for (Long ssn: invalidConInt) { - assertFalse(Personnummer.valid(ssn)); - } + @ParameterizedTest + @MethodSource({"DataProvider#getValidCoordinationNumbers"}) + public void testAgeCn(PersonnummerData ssn) throws PersonnummerException { + String strDay = ssn.longFormat.substring(ssn.longFormat.length() - 6, ssn.longFormat.length() - 4); + int day = Integer.parseInt(strDay) - 60; + strDay = day < 10 ? "0" + Integer.toString(day) : Integer.toString(day); + + LocalDate date = LocalDate.parse(ssn.longFormat.substring(0, ssn.longFormat.length() - 6) + strDay, DateTimeFormatter.ofPattern("yyyyMMdd")); + int years = (date.until(LocalDate.now())).getYears(); + + assertEquals(years, Personnummer.parse(ssn.separatedLong, new Options(true)).getAge()); + assertEquals(years, Personnummer.parse(ssn.separatedFormat, new Options(true)).getAge()); + assertEquals(years, Personnummer.parse(ssn.longFormat, new Options(true)).getAge()); + assertEquals(years > 99 ? years - 100 : years, Personnummer.parse(ssn.shortFormat, new Options(true)).getAge()); } - @Test - public void testPersonnNummerWithInvalidStringValues() { - for (String ssn: invalidSsnString) { - assertFalse(Personnummer.valid(ssn)); - } + @ParameterizedTest + @MethodSource({"DataProvider#getValidPersonnummer", "DataProvider#getValidCoordinationNumbers"}) + public void testFormat(PersonnummerData ssn) throws PersonnummerException { + assertEquals(ssn.separatedFormat, Personnummer.parse(ssn.separatedLong, new Options(true)).format()); + assertEquals(ssn.separatedFormat, Personnummer.parse(ssn.separatedFormat, new Options(true)).format()); + assertEquals(ssn.separatedFormat, Personnummer.parse(ssn.longFormat, new Options(true)).format()); } - @Test - public void testCoordinationNummerWithInvalidStringValues() { - for (String ssn: invalidConString) { - assertFalse(Personnummer.valid(ssn)); - } + @ParameterizedTest + @MethodSource({"DataProvider#getValidPersonnummer", "DataProvider#getValidCoordinationNumbers"}) + public void testFormatLong(PersonnummerData ssn) throws PersonnummerException { + assertEquals(ssn.longFormat, Personnummer.parse(ssn.separatedLong, new Options(true)).format(true)); + assertEquals(ssn.longFormat, Personnummer.parse(ssn.separatedFormat, new Options(true)).format(true)); + assertEquals(ssn.longFormat, Personnummer.parse(ssn.longFormat, new Options(true)).format(true)); } - @Test - public void testPersonnNummerWithValidIntegerValues() { - for (Long ssn: validSsnInt) { - assertTrue(Personnummer.valid(ssn)); - } + @ParameterizedTest + @MethodSource({"DataProvider#getValidPersonnummer", "DataProvider#getValidCoordinationNumbers"}) + public void testValid(PersonnummerData ssn) { + assertTrue(Personnummer.valid(ssn.longFormat)); + assertTrue(Personnummer.valid(ssn.separatedLong)); + assertTrue(Personnummer.valid(ssn.separatedFormat)); + assertTrue(Personnummer.valid(ssn.shortFormat)); } - @Test - public void testCoordinationNummerVnvalidIntegerValues() { - for (Long ssn: validConInt) { - assertTrue(Personnummer.valid(ssn)); - } + @ParameterizedTest + @MethodSource({"DataProvider#getInvalidPersonnummer", "DataProvider#getInvalidCoordinationNumbers"}) + public void testValidInvalid(PersonnummerData ssn) { + assertFalse(Personnummer.valid(ssn.longFormat)); + assertFalse(Personnummer.valid(ssn.separatedLong)); + assertFalse(Personnummer.valid(ssn.separatedFormat)); + assertFalse(Personnummer.valid(ssn.shortFormat)); } - @Test - public void testPersonnNummerWithValidStringValues() { - for (String ssn: validSsnString) { - assertTrue(Personnummer.valid(ssn)); - } + @ParameterizedTest + @MethodSource({"DataProvider#getValidPersonnummer", "DataProvider#getValidCoordinationNumbers"}) + public void testMaleFemale(PersonnummerData ssn) throws PersonnummerException { + assertEquals(ssn.isMale, Personnummer.parse(ssn.longFormat, new Options(true)).isMale()); + assertEquals(ssn.isMale, Personnummer.parse(ssn.separatedLong, new Options(true)).isMale()); + assertEquals(ssn.isMale, Personnummer.parse(ssn.separatedFormat, new Options(true)).isMale()); + assertEquals(ssn.isMale, Personnummer.parse(ssn.shortFormat, new Options(true)).isMale()); + + assertEquals(ssn.isFemale, Personnummer.parse(ssn.longFormat, new Options(true)).isFemale()); + assertEquals(ssn.isFemale, Personnummer.parse(ssn.separatedLong, new Options(true)).isFemale()); + assertEquals(ssn.isFemale, Personnummer.parse(ssn.separatedFormat, new Options(true)).isFemale()); + assertEquals(ssn.isFemale, Personnummer.parse(ssn.shortFormat, new Options(true)).isFemale()); } - @Test - public void testCoordinationNummerWithValidStringValues() { - for (String ssn: validConString) { - assertTrue(Personnummer.valid(ssn)); - } + @ParameterizedTest + @MethodSource({"DataProvider#getValidPersonnummer", "DataProvider#getValidCoordinationNumbers"}) + public void testSeparator(PersonnummerData ssn) throws PersonnummerException { + String sep = ssn.separatedFormat.contains("+") ? "+" : "-"; + assertEquals(sep, Personnummer.parse(ssn.longFormat, new Options(true)).separator()); + assertEquals(sep, Personnummer.parse(ssn.separatedLong, new Options(true)).separator()); + assertEquals(sep, Personnummer.parse(ssn.separatedFormat, new Options(true)).separator()); + // Getting the separator from a short formatted none-separated person number is not actually possible if it is intended to be a +. } }