From abd905dc708fb2d05d9330a20cb29362e09c5419 Mon Sep 17 00:00:00 2001 From: Fredrik Forsmo Date: Thu, 9 Mar 2023 20:27:32 +0100 Subject: [PATCH 1/3] Add getDate and interim number --- lib/personnummer.dart | 82 ++++++++++++++++++++++++++++++++++-------- test/personnummer.dart | 67 +++++++++++++++++++++++++++------- 2 files changed, 122 insertions(+), 27 deletions(-) diff --git a/lib/personnummer.dart b/lib/personnummer.dart index b8d8b89..0b366da 100644 --- a/lib/personnummer.dart +++ b/lib/personnummer.dart @@ -4,6 +4,20 @@ class PersonnummerException implements Exception { [this.cause = 'Invalid swedish personal identity number']); } +class PersonnummerOptions { + // Allow coordination number. + bool allowCoordinationNumber = true; + + /// Allow interim number. + bool allowInterimNumber = false; + + PersonnummerOptions( + {bool? allowCoordinationNumber, bool? allowInterimNumber}) { + allowCoordinationNumber = allowCoordinationNumber; + allowInterimNumber = allowInterimNumber; + } +} + class Personnummer { /// Personnummer age. String age = ''; @@ -33,8 +47,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 @@ -57,13 +74,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(); } @@ -116,6 +139,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. @@ -129,7 +162,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); @@ -154,15 +191,18 @@ 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(); @@ -170,7 +210,7 @@ class Personnummer { 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. @@ -179,6 +219,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() { @@ -199,17 +245,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; } } diff --git a/test/personnummer.dart b/test/personnummer.dart index 919de42..be30f3f 100644 --- a/test/personnummer.dart +++ b/test/personnummer.dart @@ -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) { @@ -42,10 +44,9 @@ void runTests(dynamic list) { 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)); } } } @@ -75,8 +76,9 @@ void runTests(dynamic list) { } 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()); } } }); @@ -105,4 +107,45 @@ void runTests(dynamic list) { } } }); + + test('should test personnummer date', () { + for (var item in list) { + if (!item['valid']) { + return; + } + + 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']) { + return; + } + + 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)); + } + } + } + }); } From e0923fcae96720d56730c88878333b89abfddb8e Mon Sep 17 00:00:00 2001 From: Fredrik Forsmo Date: Thu, 9 Mar 2023 20:28:00 +0100 Subject: [PATCH 2/3] Remove personnummer options --- lib/personnummer.dart | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/lib/personnummer.dart b/lib/personnummer.dart index 0b366da..89df549 100644 --- a/lib/personnummer.dart +++ b/lib/personnummer.dart @@ -4,20 +4,6 @@ class PersonnummerException implements Exception { [this.cause = 'Invalid swedish personal identity number']); } -class PersonnummerOptions { - // Allow coordination number. - bool allowCoordinationNumber = true; - - /// Allow interim number. - bool allowInterimNumber = false; - - PersonnummerOptions( - {bool? allowCoordinationNumber, bool? allowInterimNumber}) { - allowCoordinationNumber = allowCoordinationNumber; - allowInterimNumber = allowInterimNumber; - } -} - class Personnummer { /// Personnummer age. String age = ''; From a45470c0e8af2e6075e35cb85a4b9628a77b7a57 Mon Sep 17 00:00:00 2001 From: Fredrik Forsmo Date: Sun, 12 Mar 2023 16:33:22 +0100 Subject: [PATCH 3/3] Add test for invalid interim numbers --- test/personnummer.dart | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/test/personnummer.dart b/test/personnummer.dart index be30f3f..0cd9ee9 100644 --- a/test/personnummer.dart +++ b/test/personnummer.dart @@ -39,7 +39,7 @@ void runTests(dynamic list, dynamic interim) { test('should format personnummer', () { for (var item in list) { if (!item['valid']) { - return; + continue; } for (var format in availableListFormats) { @@ -55,7 +55,7 @@ void runTests(dynamic list, dynamic interim) { test('should throw personnummer error', () { for (var item in list) { if (item["valid"]) { - return; + continue; } for (var format in availableListFormats) { @@ -72,7 +72,7 @@ void runTests(dynamic list, dynamic interim) { test('should test personnummer sex', () { for (var item in list) { if (!item["valid"]) { - return; + continue; } for (var format in availableListFormats) { @@ -86,7 +86,7 @@ void runTests(dynamic list, dynamic interim) { test('should test personnummer age', () { for (var item in list) { if (!item['valid']) { - return; + continue; } for (var format in availableListFormats) { @@ -111,7 +111,7 @@ void runTests(dynamic list, dynamic interim) { test('should test personnummer date', () { for (var item in list) { if (!item['valid']) { - return; + continue; } for (var format in availableListFormats) { @@ -136,7 +136,7 @@ void runTests(dynamic list, dynamic interim) { test('should test interim numbers', () { for (var item in interim) { if (!item['valid']) { - return; + continue; } for (var format in availableListFormats) { @@ -148,4 +148,20 @@ void runTests(dynamic list, dynamic interim) { } } }); + + 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); + } + } + } + }); }