Skip to content

Instantly share code, notes, and snippets.

@mcshaz
Last active August 31, 2022 03:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mcshaz/b41dc6bd4aa3104d54da677e2b4f6b45 to your computer and use it in GitHub Desktop.
Save mcshaz/b41dc6bd4aa3104d54da677e2b4f6b45 to your computer and use it in GitHub Desktop.
NZ NHI validator
using NHIValidation;
using System.Collections.Generic;
using Xunit;
using Xunit.Abstractions;
namespace TestNhi
{
public class NHIValidationUnitTests
{
private readonly ITestOutputHelper _output;
public NHIValidationUnitTests(ITestOutputHelper output)
{
_output = output;
}
[Theory]
[MemberData(nameof(NHIs))]
public void TestNHIsPass(string nhi)
{
Assert.True(NHIValidator.IsValid(nhi));
}
[Theory]
[MemberData(nameof(NHIs))]
public void TestOtherNHIChecksumsFail(string nhi)
{
char checksum = nhi[6];
string baseNHI = nhi[0..6];
char[] possibleChecksums = char.IsDigit(checksum)
? new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }
: new[] { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' };
foreach (var c in possibleChecksums)
{
if (c != checksum)
{
var n = baseNHI + c;
Assert.False(NHIValidator.IsValid(n), $"NHI: {n} incorrectly found valid");
_output.WriteLine($"NHI: {n} correctly found invalid");
}
}
}
[Theory]
[InlineData("ZZZ0044")] // no digit can be added to "ZZZ004\d"
[InlineData("ZZZZ000")] // 3 letters followed by either 4 numbers OR 2 numbers + 2 letters
[InlineData("ZZZ?000")]
public void TestNHIsFail(string nhi)
{
Assert.False(NHIValidator.IsValid(nhi));
}
public static IEnumerable<object[]> NHIs =>
new List<object[]>
{
new object[] { "ZZZ0016" },
new object[] { "ZZZ0024" },
new object[] { "ZZZ00AX" },
new object[] { "ALU18KZ" }
};
}
}
using System;
using System.Text.RegularExpressions;
namespace NHIValidation
{
public static class NHIValidator
{
// The first 3 characters of an NHI number must be alphabetic, but not ‘I’ or ‘O’, to avoid confusion with one and zero. The 4th to 6th characters must be numeric. The 7th character is also numeric, and is a check digit based on modulus 11.
// Each alphabet character is assigned a number based on the following table, plus 1
private static readonly char[] LetterToNumberMap = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' };
public static bool IsValid(string NHIString)
{
if (!Regex.IsMatch(NHIString, @"^([A-HJ-NP-Z]{3}\d{4}|[A-HJ-NP-Z]{3}\d{2}[A-HJ-NP-Z]{2})$"))
{
return false;
}
int cumulative = 0;
for (int i = 0; i < 6; ++i)
{
cumulative += GetCharNumberValue(NHIString[i]) * (7 - i);
}
char checksumRaw = NHIString[6];
if (Char.IsDigit(checksumRaw)) // old NHI
{
int modulus = cumulative % 11;
if (modulus == 0) return false;
// Subtract checksum from 11 to create check digit.
// If the check digit equals ‘10’, convert to ‘0’
return modulus == 1
? checksumRaw == '0'
: 11 - modulus == (int)Char.GetNumericValue(checksumRaw);
}
else // new NHI format
{
int modulus = cumulative % 24;
// note in the old NHI format, a modulus of 0 fails, but NOT in the new format
return LetterToNumberMap[23 - modulus] == checksumRaw;
}
}
private static int GetCharNumberValue(char character)
{
return Char.IsDigit(character)
? (int)Char.GetNumericValue(character)
: Array.IndexOf(LetterToNumberMap, character) + 1;
}
}
}
function isValidNHI(nhiString) {
if (!/([A-HJ-NP-Z]{3}\d{4}|[A-HJ-NP-Z]{3}\d{2}[A-HJ-NP-Z]{2})/i.test(nhiString)) {
return false;
}
const alphaLookup = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
const checksumRaw = nhiString.slice(-1);
nhiString = nhiString.slice(0, -1).toUpperCase();
let cum = 0;
let multiplier = 7;
for (const c of nhiString) {
let val = parseInt(c, 10);
if (isNaN(val)) {
val = alphaLookup.indexOf(c) + 1;
if (val === 0) { return false; }
}
cum += val * multiplier--;
}
const checksumVal = parseInt(checksumRaw, 10);
if (isNaN(checksumVal)) { // newer NHI format
const modulus = cum % 24;
return alphaLookup[23 - modulus] === checksumRaw.toUpperCase();
} else { // old NHI format
const modulus = cum % 11;
if (modulus === 0) { return false; }
return modulus === 1
? checksumVal === 0
: (checksumVal === 11 - modulus);
}
}
@Nice-Mark
Copy link

Hi Brent. Thanks for this code. Could you check "ALU18KZ" please? The Ministry of Health lists it as a sample NHI but it fails your validator (because modulus = 0). Thanks.

@mcshaz
Copy link
Author

mcshaz commented Aug 31, 2022

@Nice-Mark Thank you very much for noticing and providing a concrete example - you are absolutely correct that the modulus of 0 fails the old version of NHIs, but provides a valid checksum in the new NHI format (equating to Z). The NHI validation code and the unit test is now updated.

@Nice-Mark
Copy link

@mcshaz Wow that was fast, thank you! I finally signed up to GitHub after all these years to ask this question. 😁

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment