Skip to content

Commit

Permalink
Merge pull request #12 from personnummer/spec-v3-1
Browse files Browse the repository at this point in the history
Add getDate and interim number
  • Loading branch information
frozzare authored Mar 12, 2023
2 parents 2ed2fcf + a45470c commit 58bc519
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 31 deletions.
68 changes: 53 additions & 15 deletions lib/personnummer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,11 @@ class Personnummer {
String check = '';

/// Personnummer constructor.
Personnummer(String ssn, [dynamic options]) {
_parse(ssn);
Personnummer(String pin,
{bool allowCoordinationNumber = true, bool allowInterimNumber = false}) {
_parse(pin,
allowCoordinationNumber: allowCoordinationNumber,
allowInterimNumber: allowInterimNumber);
}

/// Luhn/mod10 algorithm. Used to calculate a checksum from the passed value
Expand All @@ -57,13 +60,19 @@ class Personnummer {
}

/// Parse Swedish personal identity numbers and set properties.
void _parse(String ssn) {
void _parse(String pin,
{bool allowCoordinationNumber = true, bool allowInterimNumber = false}) {
if (pin.length < 10 || pin.length > 13) {
throw PersonnummerException(
"Input value too ${pin.length > 13 ? "long" : "short"}");
}

var reg = RegExp(
r'^(\d{2}){0,1}(\d{2})(\d{2})(\d{2})([\+\-]?)((?!000)\d{3})(\d)$');
r'^(\d{2}){0,1}(\d{2})(\d{2})(\d{2})([+-]?)((?!000)\d{3}|[TRSUWXJKLMN]\d{2})(\d)$');
RegExpMatch? match;

try {
match = reg.firstMatch(ssn);
match = reg.firstMatch(pin);
} catch (e) {
throw PersonnummerException();
}
Expand Down Expand Up @@ -116,6 +125,16 @@ class Personnummer {
if (!_valid()) {
throw PersonnummerException();
}

// throw error if coordination numbers is not allowed.
if (!allowCoordinationNumber && isCoordinationNumber()) {
throw PersonnummerException();
}

// throw error if interim numbers is not allowed.
if (!allowInterimNumber && isInterimNumber()) {
throw PersonnummerException();
}
}

/// Test year, month and day as date and see if it's the same.
Expand All @@ -129,7 +148,11 @@ class Personnummer {
/// Returns `true` if the input value is a valid Swedish personal identity number.
bool _valid() {
try {
var valid = _luhn(year + month + day + num) == int.parse(check);
var valid = _luhn(year +
month +
day +
num.replaceFirst(RegExp(r'[TRSUWXJKLMN]'), '1')) ==
int.parse(check);

var localYear = int.parse(year);
var localMonth = int.parse(month);
Expand All @@ -154,23 +177,26 @@ class Personnummer {
return year + month + day + sep + num + check;
}

// Get age from a Swedish personal identity number.
int getAge() {
/// Get date from a Swedish personal identity number.
DateTime getDate() {
var ageDay = int.parse(day);
if (isCoordinationNumber()) {
ageDay -= 60;
}

var pnrDate = DateTime(int.parse(century + year), int.parse(month), ageDay);
return DateTime(int.parse(century + year), int.parse(month), ageDay);
}

/// Get age from a Swedish personal identity number.
int getAge() {
DateTime dt;
if (dateTimeNow == null) {
dt = DateTime.now();
} else {
dt = dateTimeNow!;
}

return (dt.difference(pnrDate).inMilliseconds / 3.15576e+10).floor();
return (dt.difference(getDate()).inMilliseconds / 3.15576e+10).floor();
}

/// Check if a Swedish personal identity number is a coordination number or not.
Expand All @@ -179,6 +205,12 @@ class Personnummer {
return _testDate(int.parse(year), int.parse(month), int.parse(day) - 60);
}

/// Check if a Swedish personal identity number is a interim number or not.
/// Returns `true` if it's a interim number.
bool isInterimNumber() {
return RegExp(r'[TRSUWXJKLMN]').hasMatch(num[0]);
}

/// Check if a Swedish personal identity number is for a female.
/// Returns `true` if it's a female.
bool isFemale() {
Expand All @@ -199,17 +231,23 @@ class Personnummer {

/// Parse Swedish personal identity numbers.
/// Returns `Personnummer` class.
static Personnummer parse(String ssn, [dynamic options]) {
return Personnummer(ssn, options);
static Personnummer parse(String pin,
{bool allowCoordinationNumber = true, bool allowInterimNumber = false}) {
return Personnummer(pin,
allowCoordinationNumber: allowCoordinationNumber,
allowInterimNumber: allowInterimNumber);
}

/// Validates Swedish personal identity numbers.
/// Returns `true` if the input value is a valid Swedish personal identity number
static bool valid(String ssn, [dynamic options]) {
static bool valid(String pin,
{bool allowCoordinationNumber = true, bool allowInterimNumber = false}) {
try {
parse(ssn, options);
Personnummer(pin,
allowCoordinationNumber: allowCoordinationNumber,
allowInterimNumber: allowInterimNumber);
return true;
} catch (e) {
} catch (_) {
return false;
}
}
Expand Down
91 changes: 75 additions & 16 deletions test/personnummer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@ var availableListFormats = [
];

void main() async {
final url =
'https://raw.githubusercontent.com/personnummer/meta/master/testdata/list.json';
String body = await fetchUrlBodyAsString(url);
dynamic list = jsonDecode(body);
runTests(list);
String listBody = await fetchUrlBodyAsString(
'https://raw.githubusercontent.com/personnummer/meta/master/testdata/list.json');

String interimBody = await fetchUrlBodyAsString(
'https://raw.githubusercontent.com/personnummer/meta/master/testdata/interim.json');

runTests(jsonDecode(listBody), jsonDecode(interimBody));
}

void runTests(dynamic list) {
void runTests(dynamic list, dynamic interim) {
test('should validate personnummer with control digit', () {
for (var item in list) {
for (var format in availableListFormats) {
Expand All @@ -37,15 +39,14 @@ void runTests(dynamic list) {
test('should format personnummer', () {
for (var item in list) {
if (!item['valid']) {
return;
continue;
}

for (var format in availableListFormats) {
if (format != 'short_format') {
expect(item["separated_format"],
Personnummer.parse(item[format]).format());
expect(item["long_format"],
Personnummer.parse(item[format]).format(true));
var p = Personnummer.parse(item[format]);
expect(item["separated_format"], p.format());
expect(item["long_format"], p.format(true));
}
}
}
Expand All @@ -54,7 +55,7 @@ void runTests(dynamic list) {
test('should throw personnummer error', () {
for (var item in list) {
if (item["valid"]) {
return;
continue;
}

for (var format in availableListFormats) {
Expand All @@ -71,20 +72,21 @@ void runTests(dynamic list) {
test('should test personnummer sex', () {
for (var item in list) {
if (!item["valid"]) {
return;
continue;
}

for (var format in availableListFormats) {
expect(item["isMale"], Personnummer.parse(item[format]).isMale());
expect(item["isFemale"], Personnummer.parse(item[format]).isFemale());
var p = Personnummer.parse(item[format]);
expect(item["isMale"], p.isMale());
expect(item["isFemale"], p.isFemale());
}
}
});

test('should test personnummer age', () {
for (var item in list) {
if (!item['valid']) {
return;
continue;
}

for (var format in availableListFormats) {
Expand All @@ -105,4 +107,61 @@ void runTests(dynamic list) {
}
}
});

test('should test personnummer date', () {
for (var item in list) {
if (!item['valid']) {
continue;
}

for (var format in availableListFormats) {
if (format != 'short_format') {
var pin = item["separated_long"];
var year = int.parse(pin.substring(0, 4));
var month = int.parse(pin.substring(4, 6));
var day = int.parse(pin.substring(6, 8));

if (item["type"] == 'con') {
day = day - 60;
}

var date = DateTime(year, month, day);
// Personnummer.dateTimeNow = date;
expect(date, Personnummer.parse(item[format]).getDate());
}
}
}
});

test('should test interim numbers', () {
for (var item in interim) {
if (!item['valid']) {
continue;
}

for (var format in availableListFormats) {
if (format != 'short_format') {
var p = Personnummer.parse(item[format], allowInterimNumber: true);
expect(item['separated_format'], p.format());
expect(item['long_format'], p.format(true));
}
}
}
});

test('should test invalid interim numbers', () {
for (var item in interim) {
if (item['valid']) {
continue;
}

for (var format in availableListFormats) {
if (format != 'short_format') {
expect(
() => Personnummer.parse(item[format], allowInterimNumber: true),
throwsException);
}
}
}
});
}

0 comments on commit 58bc519

Please sign in to comment.