Skip to content

Commit

Permalink
Denmark extensions: Updates CPR number generator with checksum (#496)
Browse files Browse the repository at this point in the history
* Allow for control digit to be 0 and 1, which is valid.
Adds new unit tests file
Updates CPR number generator with optional checksum and adds unit tests.

* File scope DanishExtensionTest.cs;
Fix spelling error in Extension

* Adds includeDash as parameter for formatting final result.

* Use switch statement for better readability; and use actual date range in ArgumentOutOfRangeException message

---------

Co-authored-by: Brian Chavez <bchavez@bitarmory.com>
  • Loading branch information
MunroRaymaker and bchavez authored Dec 31, 2023
1 parent 6d25006 commit fa4d525
Show file tree
Hide file tree
Showing 3 changed files with 308 additions and 20 deletions.
152 changes: 152 additions & 0 deletions Source/Bogus.Tests/ExtensionTests/DanishExtentionTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
using System;
using Bogus.DataSets;
using Bogus.Extensions.Denmark;
using FluentAssertions;
using Xunit;

namespace Bogus.Tests.ExtensionTests;

public class DanishExtensionTest : SeededTest
{
private readonly Faker _faker;

public DanishExtensionTest()
{
_faker = new Faker();
}

[Fact]
public void can_generate_cpr_number_for_denmark()
{
// Act
var obtained = _faker.Person.Cpr(validChecksum: false);

obtained.Dump();

// Assert
obtained.Should().NotBeNullOrWhiteSpace();
ShouldBeLegalDanishCprNumber(obtained);
ShouldBeCorrectGenderCode(_faker.Person.Gender, obtained);
}

[Fact]
public void excludes_dash_cpr_number()
{
var result = _faker.Person.Cpr(includeDash: false);
result.Should().NotContain("-");
}

[Theory]
[InlineData("080165-0058", Name.Gender.Female)]
[InlineData("080165-0066", Name.Gender.Female)]
[InlineData("080165-0074", Name.Gender.Female)]
[InlineData("080165-0082", Name.Gender.Female)]
[InlineData("080165-0090", Name.Gender.Female)]
[InlineData("250665-3595", Name.Gender.Male)]
[InlineData("250665-3617", Name.Gender.Male)]
[InlineData("250665-3633", Name.Gender.Male)]
[InlineData("250665-3641", Name.Gender.Male)]
[InlineData("250665-3749", Name.Gender.Male)]
public void is_valid_danish_cpr_number(string candidate, Name.Gender gender)
{
ShouldBeCorrectGenderCode(gender, candidate);
ShouldBeLegalDanishCprNumber(candidate);
ShouldHaveCorrectChecksum(candidate);
}

[Theory]
[InlineData("000000-0000", Name.Gender.Female)]
[InlineData("111111-1111", Name.Gender.Male)]
[InlineData("999999-9999", Name.Gender.Female)]
[InlineData("AAAAAA-AAAA", Name.Gender.Female)]
[InlineData("241212-1234", Name.Gender.Female)]
public void is_invalid_danish_cpr_number(string candidate, Name.Gender gender)
{
Action action = () =>
{
ShouldBeCorrectGenderCode(gender, candidate);
ShouldBeLegalDanishCprNumber(candidate);
ShouldHaveCorrectChecksum(candidate);
};

action.Should().Throw<Exception>();
}

[Theory]
[InlineData("080165", Name.Gender.Female)]
[InlineData("080166", Name.Gender.Female)]
[InlineData("080167", Name.Gender.Female)]
[InlineData("080168", Name.Gender.Female)]
[InlineData("080169", Name.Gender.Female)]
[InlineData("250665", Name.Gender.Male)]
[InlineData("250607", Name.Gender.Male)]
[InlineData("250608", Name.Gender.Male)]
[InlineData("250609", Name.Gender.Male)]
[InlineData("250610", Name.Gender.Male)]
public void can_generate_valid_danish_cpr_numbers(string birthDate, Name.Gender gender)
{;
int day = int.Parse(birthDate.Substring(0, 2));
int month = int.Parse(birthDate.Substring(2, 2));
int year = int.Parse(birthDate.Substring(4, 2));

year += year < DateTime.Now.Year % 100 ? 2000 : 1900;

var bd = new DateTime(year, month, day);

_faker.Person.DateOfBirth = bd;
_faker.Person.Gender = gender;

var actual = _faker.Person.Cpr(true);

ShouldBeCorrectGenderCode(gender, actual);
ShouldBeLegalDanishCprNumber(actual);
ShouldHaveCorrectChecksum(actual);
}

private void ShouldHaveCorrectChecksum(string candidate)
{
var factors = new[] { 4, 3, 2, 7, 6, 5, 4, 3, 2, 1 };
var digits = candidate.Replace("-", "").Substring(0, 10).ToCharArray();

int cs = 0;
for (int i = 0; i < 10; i++)
{
cs += (digits[i] - '0') * factors[i];
}

(cs % 11).Should().Be(0);
}

private void ShouldBeLegalDanishCprNumber(string candidate)
{
var parts = candidate.Split('-');
parts[0].Should().HaveLength(6);
parts[1].Should().HaveLength(4);

// Check if the first 6 digits represent a valid date.
int day = int.Parse(parts[0].Substring(0, 2));
int month = int.Parse(parts[0].Substring(2, 2));
int year = int.Parse(parts[0].Substring(4, 2));

day.Should().BeInRange(1, 31);
month.Should().BeInRange(1, 12);
year.Should().BeInRange(0, 99);
}

private void ShouldBeCorrectGenderCode(Name.Gender gender, string candidate)
{
var lastPart = int.Parse(candidate.Split('-')[1]);

if (gender == Name.Gender.Female)
{
lastPart.Should()
.Match(x => x % 2 == 0);
}

if (gender == Name.Gender.Male)
{
lastPart.Should()
.Match(x => x % 2 == 1);
}
}
}
16 changes: 0 additions & 16 deletions Source/Bogus.Tests/PersonTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using Bogus.DataSets;
using Bogus.Extensions.Brazil;
using Bogus.Extensions.Canada;
using Bogus.Extensions.Denmark;
using Bogus.Extensions.Finland;
using Bogus.Extensions.UnitedStates;
using FluentAssertions;
Expand Down Expand Up @@ -149,21 +148,6 @@ public void can_generate_numeric_cpf_for_brazil()
obtained.Should().Equal(expect);
}

[Fact]
public void can_generate_cpr_number_for_denmark()
{
var p = new Person();
var obtained = p.Cpr();

obtained.Dump();

var a = obtained.Split('-')[0];
var b = obtained.Split('-')[1];

a.Length.Should().Be(6);
b.Length.Should().Be(4);
}

[Fact]
public void can_generate_henkilötunnus_for_finland()
{
Expand Down
160 changes: 156 additions & 4 deletions Source/Bogus/Extensions/Denmark/ExtensionsForDenmark.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
namespace Bogus.Extensions.Denmark;
using static Bogus.DataSets.Name;
using System;

namespace Bogus.Extensions.Denmark;

/// <summary>
/// API extensions specific for a geographical location.
Expand All @@ -8,18 +11,167 @@ public static class ExtensionsForDenmark
/// <summary>
/// Danish Personal Identification number
/// </summary>
public static string Cpr(this Person p)
/// <param name="p">The holder.</param>
/// <param name="validChecksum">
/// Indicates whether the generated CPR number should have a valid checksum or not.
/// </param>
public static string Cpr(this Person p, bool validChecksum = true, bool includeDash = true)
{
const string Key = nameof(ExtensionsForDenmark) + "CPR";
if( p.context.ContainsKey(Key) )
if (p.context.ContainsKey(Key))
{
return p.context[Key] as string;
}

/*
DDMMYY-XXXX
| | | |
| | | |
| | | |
| | | |----> (X)Individual number
| | |-------> (Y)Year (last two digits)
| |---------> (M)Month
|-----------> (D)Day
The individual number has to be even for women and odd for men.
As of 2007 there is no longer a requirement for a checksum with a modulo algorithm.
https://cpr.dk/cpr-systemet/opbygning-af-cpr-nummeret
https://da.wikipedia.org/wiki/CPR-nummer
https://www.cprgenerator.net/metode
*/

var r = p.Random;
var final = $"{p.DateOfBirth:ddMMyy}-{r.Replace("####")}";
string birthDate = $"{p.DateOfBirth:ddMMyy}";
string individualNumber;
string checksum;
bool hasValidChecksum;

if (validChecksum)
{
do
{
individualNumber = GenerateIndividualThreeDigitNumber(r, p.DateOfBirth.Year);
hasValidChecksum = GenerateChecksum(birthDate, p.Gender, individualNumber, out checksum);
} while (!hasValidChecksum);
}
else
{
checksum = string.Empty;
individualNumber = GenerateIndividualFourDigitNumber(r, p.Gender, p.DateOfBirth.Year);
}

string final;
if( includeDash ) {
final = $"{birthDate}-{individualNumber}{checksum}";
}
else
{
final = $"{birthDate}{individualNumber}{checksum}";
}

p.context[Key] = final;
return final;
}

private static string GenerateIndividualFourDigitNumber(Randomizer r, DataSets.Name.Gender gender, int year)
{
int from;
int to;

switch( year )
{
case >= 1858 and <= 1899:
from = 5000;
to = 8999;
break;
case >= 1900 and <= 1936:
from = 0;
to = 3999;
break;
case >= 1937 and <= 1999:
from = 0;
to = 4999;
break;
case >= 2000 and <= 2036:
from = 4000;
to = 9999;
break;
case >= 2037 and <= 2057:
from = 5000;
to = 9999;
break;
default:
throw new ArgumentOutOfRangeException(nameof(year), $"{nameof(year)} must be between 1858 and 2057.");
}

int individualNumber = gender == DataSets.Name.Gender.Female ? r.Even(from, to) : r.Odd(from, to);

return individualNumber.ToString("D4");
}

private static string GenerateIndividualThreeDigitNumber(Randomizer r, int year)
{
int from;
int to;

switch( year )
{
case >= 1858 and <= 1899:
from = 500;
to = 899;
break;
case >= 1900 and <= 1936:
from = 0;
to = 399;
break;
case >= 1937 and <= 1999:
from = 0;
to = 499;
break;
case >= 2000 and <= 2036:
from = 400;
to = 999;
break;
case >= 2037 and <= 2057:
from = 500;
to = 999;
break;
default:
throw new ArgumentOutOfRangeException(nameof(year), $"{nameof(year)} must be between 1858 and 2057.");
}

int individualNumber = r.Int(from, to);

return individualNumber.ToString("D3");
}

private static bool GenerateChecksum(string birthDate, DataSets.Name.Gender gender, string individualNumber, out string checksum)
{
var factors = new[] { 4, 3, 2, 7, 6, 5, 4, 3, 2 };
var digits = (birthDate + individualNumber).ToCharArray();

int cs = 0;
for (int i = 0; i < 9; i++)
{
cs += (digits[i] - '0') * factors[i];
}

cs = 11 - (cs % 11);

if (cs == 11)
{
cs = 0;
}

checksum = $"{cs}";

if (gender == Gender.Female && cs % 2 != 0) return false;
if (gender == Gender.Male && cs % 2 == 0) return false;

return cs < 10;
}
}

0 comments on commit fa4d525

Please sign in to comment.