Implementing the Luhn Algorithm in C#

Michael Harges
18 min readSep 18, 2023

--

Photo by Antoine Dautry on Unsplash

Implementing and optimizing a classic check digit algorithm

Check digits are an useful tool for detecting human transcription errors. By embedding a check digit in a piece of information it is possible to detect common data entry errors early, often before performing more extensive and time consuming processing. Depending on the algorithm, a check digit can be used to detect single digit errors (typing 124 instead of 123) and two digit errors such as transposition errors (12 instead of 21), twin errors (22 instead of 33), jump transpositions (231 instead of 123), jump twins (323 instead of 121) and more. Whether you realize it or not, you regularly encounter check digits. Check digits are included in credit card numbers, book ISBNs, vehicle VINs, US bank routing numbers and National Provider Identifiers (NPI numbers), UK NHS numbers and many other identifiers where there is an expectation that they will be manually entered at some point. Notably however, US Social Security Numbers do not include a check digit.

Before going further, I want to announce that I’ve recently released CheckDigits.Net, a .Net library of optimized implementations of 20 different check digit algorithms. Benchmarks comparing CheckDigits.Net to popular NuGet packages have shown performance increases of 3X, 10X and in some cases up to 50X, depending on the algorithm. In addition, CheckDigits.Net completely eliminates the memory allocations that are common in other packages. You can find CheckDigits.Net on NuGet or by searching for CheckDigits.Net in your IDE’s NuGet package manager.

In this article we’ll look at implementing, testing and optimizing the Luhn algorithm, developed by Hans Peter Luhn while working for IBM and patented in 1960. The Luhn algorithm can detect single digit errors, most but not all single digit transposition errors and many possible twin errors. It is very popular and is used in credit card numbers, US NPI numbers, mobile phone IMEI numbers and many more cases.

The Luhn algorithm uses a modulus 10 calculation to determine the check digit. The calculation is as follows: ignoring the position occupied by the check digit (if present), separate the value into individual digits. Start at the right most position and moving left, double every other digit and if the doubled value is ≥ 10, again separate that value into individual digits. Then sum all the individual digits and calculate the check digit as (10 — (sum mod 10)) mod 10. Here is a worked example for the value 123456789.

calculate check digit 123456789

| Digit | Double? | Value | Sum |
| ----- | ------- | -------------------------| --- |
| 9 | Y | 9 * 2 = 18 => 1 + 8 => 9 | 9 |
| 8 | N | 8 | 17 |
| 7 | Y | 7 * 2 = 14 => 1 + 4 => 5 | 22 |
| 6 | N | 6 | 28 |
| 5 | Y | 5 * 2 = 10 => 1 + 0 => 1 | 29 |
| 4 | N | 4 | 33 |
| 3 | Y | 3 * 2 = 6 | 39 |
| 2 | N | 2 | 41 |
| 1 | Y | 1 * 2 = 2 | 43 |

check digit = (10 - (43 mod 10)) mod 10 = 7

Implementing the Algorithm

With that background, let’s look at implementing the algorithm. In this case we’re going to focus on validating an existing check digit since it is far more common than calculating a check digit for a new identifier. The requirements for our method will be:

  • Input will be a string.
  • We are not assuming a specific use case (i.e. credit card numbers). We will test any string for a valid check digit.
  • We will assume that the input string includes a check digit in the last position.
  • Output will be a boolean where true = the input string contains a valid check digit and false = the input string does not contain a valid check digit.
  • The code should be resilient, meaning that invalid input should not throw an exception and instead should return false to indicate that there is not a valid check digit.

We start by defining the method signature like this:

public static class LuhnAlgorithm
{
public static Boolean ValidateCheckDigit(String str)
{
throw new NotImplementedException();
}
}

// Example usage:
var str = "1234567897";

var isValid = LuhnAlgorithm.ValidateCheckDigit(str);

And let’s follow a red/green development approach and define some unit tests. Using my preferred tools of xUnit and FluentAssertions we can quickly create the following:

public class LuhnAlgorithmTests
{
[Fact]
public void ValidateCheckDigit_ShouldReturnTrue_WhenInputContainsValidCheckDigit()
=> LuhnAlgorithm.ValidateCheckDigit("1234567897").Should().BeTrue();

[Fact]
public void ValidateCheckDigit_ShouldReturnFalse_WhenInputDoesNotContainValidCheckDigit()
=> LuhnAlgorithm.ValidateCheckDigit("1234567890").Should().BeFalse();
}

At this point we could dive into implementing ValidateCheckDigit method, but let’s be a bit more methodical about our tests. The simplest case that will return true is a two character string where the second character is the check digit. So we can revise our passing case test thusly:

   [Theory]
[InlineData("26")]
[InlineData("1234567897")]
public void ValidateCheckDigit_ShouldReturnTrue_WhenInputContainsValidCheckDigit(String str)
=> LuhnAlgorithm.ValidateCheckDigit(str).Should().BeTrue();

And even the two character string has a couple possibilities. When we double a digit we can either sum the doubled value or sum the individual digits of the doubled value when it’s ≥ 10. So we add another test case:

   [InlineData("75")]

Then we need to check cases where the individual digit is not doubled so we have another test case:

[InlineData("133")]

And let’s drop our made up test case (“1234567897”) and add a couple of real world tests in its place. Googling for “test credit card numbers” turns up a PayPal page showing some test credit card numbers for several different credit card providers. We can also find example NPI numbers and example IMEI numbers online to give us a full set of test cases for valid check digits:

   [Theory]
[InlineData("26")]
[InlineData("75")]
[InlineData("133")]
[InlineData("5555555555554444")] // MasterCard test credit card number
[InlineData("4012888888881881")] // Visa test credit card number
[InlineData("3056930009020004")] // Diners Club test credit card number
[InlineData("3566111111111113")] // JCB test credit card number
[InlineData("808401234567893")] // NPI (National Provider Identifier), including 80840 prefix
[InlineData("490154203237518")] // IMEI (International Mobile Equipment Identity)
public void ValidateCheckDigit_ShouldReturnTrue_WhenInputContainsValidCheckDigit(String str)
=> LuhnAlgorithm.ValidateCheckDigit(str).Should().BeTrue();

Also, let’s beef up our testing by testing both the known strengths and weaknesses of the algorithm. According to Wikipedia, the Luhn algorithm cannot detect transpositions of 09 to 90 and vice versa. Nor can it detect a number of twin errors (22 to 55, 33 to 66, 44 to 77 and vice versa). So we can add a new test for cases where we’ve deliberately introduced an error that the algorithm can’t detect so we expect that the result will be true.

   [Theory]
[InlineData("3056930090020004")] // Diners Club test card number with two digit transposition 09 -> 90
[InlineData("3056930000920004")] // Diners Club test card number with two digit transposition 90 -> 09
[InlineData("5555555225554444")] // MasterCard test card number with two digit twin error 55 -> 22
[InlineData("5555555225554774")] // MasterCard test card number with two digit twin error 44 -> 77
[InlineData("3533111111111113")] // JCB test card number with two digit twin error 66 -> 33
public void ValidateCheckDigit_ShouldReturnTrue_WhenInputContainsUndetectableError(String str)
=> LuhnAlgorithm.ValidateCheckDigit_Original(str).Should().BeTrue();

We can also replace our single failure test with tests that target known strengths of the algorithm.

   [Theory]
[InlineData("5558555555554444")] // MasterCard test card number with single digit transcription error 5 -> 8
[InlineData("5558555555554434")] // MasterCard test card number with single digit transcription error 4 -> 3
[InlineData("3059630009020004")] // Diners Club test card number with two digit transposition error 69 -> 96
[InlineData("3056930009002004")] // Diners Club test card number with two digit transposition error 20 -> 02
[InlineData("5559955555554444")] // MasterCard test card number with two digit twin error 55 -> 99
[InlineData("3566111144111113")] // JCB test card number with two digit twin error 11 -> 44
public void ValidateCheckDigit_ShouldReturnFalse_WhenInputContainsDetectableError(String value)
=> LuhnAlgorithm.ValidateCheckDigit_Original(value).Should().BeFalse();

At this point we can run our tests and watch them all fail. Now we’re ready to start implementing. Using the pseudocode implementation in the Wikipedia article as a guide, here is a first stab at a solution. It’s a quite literal translation of the algorithm into code and we will have the opportunity to improve on it as we proceed.

   public static Boolean ValidateCheckDigit(String str)
{
var sum = 0;
var shouldApplyDouble = true;
for(var index = str.Length - 2; index >= 0; index--)
{
var currentDigit = (Int32)Char.GetNumericValue(str, index);
if (shouldApplyDouble)
{
if (currentDigit > 4)
{
sum += currentDigit * 2 - 9;
}
else
{
sum += currentDigit * 2;
}
}
else
{
sum += currentDigit;
}
shouldApplyDouble = !shouldApplyDouble;
}
var checkDigit = 10 - (sum % 10);

return Char.GetNumericValue(str[^1]) == checkDigit;
}

In this version we skip the trailing check digit position and then process the remaining characters from right to left. There are multiple ways of keeping track of if a digit should be doubled or not, but I opted for a simple boolean value that is negated every pass through the loop. The conversion of the integer current digit is done using the Char.GetNumericValue function.

We also handle the doubling of values in two possible ways. If the doubled value would be ≥ 10 then we subtract 9 from the doubled value (the equivalent of adding the individual digits of the doubled value), otherwise we simply double the value.

Following the pseudocode implementation, we calculate the check digit using the expression 10 — (sum % 10) and then compare the calculated check digit to the integer equivalent of the check digit character in the input string. (Sharp eyed readers will have noticed an issue with the check digit calculation that we will cover later.)

Now we can run our tests again and see that they all pass. Yeah!

Improving Resiliency

So we’re done, right? All of our tests pass, and we have 100% code coverage, so what else is there to do? In fact, most of the discussions on the Luhn algorithm you find online are happy to stop at this point. But the consumers of the software we create are often more devious than we give them credit for and they delight in showing us how badly we’ve underestimated them. So, we need to address the resiliency requirement by putting some thought into how what we’ve written could go wrong.

First, what happens if we’re given a null string? You could predict that a NullReferenceException would be thrown and you’d be right. The typical .Net approach would be to check for null and throw an ArgumentNullException but trading one type of exception for another does nothing to address the resiliency requirement. We know from our requirements that an invalid string by definition can’t contain an valid check digit and should return false. So we can create the following test and when it fails, modify the ValidateCheckDigit method so that the test passes.

   [Fact]
public void ValidateCheckDigit_ShouldReturnFalse_WhenInputIsNull()
=> LuhnAlgorithm.ValidateCheckDigit(null!).Should().BeFalse();

public static Boolean ValidateCheckDigit(String str)
{
if (str is null)
{
return false;
}

var sum = 0;
.
.
.
}

You could use a similar approach to create a test for an empty string and find that an IndexOutOfRangeException is thrown. Then we again modify ValidateCheckDigit to handle this case.

   [Fact]
public void ValidateCheckDigit_ShouldReturnFalse_WhenInputIsEmpty()
=> LuhnAlgorithm.ValidateCheckDigit(String.Empty).Should().BeFalse();

public static Boolean ValidateCheckDigit(String str)
{
if (String.IsNullOrEmpty(str))
{
return false;
}

var sum = 0;
.
.
.
}

What if our input string is all zeros? It should be obvious that the check digit should be zero, but if you create a test that uses all zeros for input and that expects the result to be true then you’ll find that the test will fail! This is the issue with the calculation of the check digit that I mentioned previously. Far too many of the example implementations you find online use the expression 10 — (sum % 10), but if sum is zero the value calculated for the check digit is 10, not zero. The same issue occurs when the sum is any multiple of 10.

Here are tests for both of those cases and the update to ValidateCheckDigit so that those tests pass.

   [Fact]
public void ValidateCheckDigit_ShouldReturnTrue_WhenInputIsAllZeros()
=> LuhnAlgorithm.ValidateCheckDigit("0000000000000000").Should().BeTrue();

[Fact]
public void ValidateCheckDigit_ShouldReturnTrue_WhenCheckDigitIsCalculatesAsZero()
=> LuhnAlgorithm.ValidateCheckDigit("7624810").Should().BeTrue();

public static Boolean ValidateCheckDigit(String str)
{
.
.
.
var checkDigit = (10 - (sum % 10)) % 10;

return Char.GetNumericValue(str[^1]) == checkDigit;
}

It’s time to amp up our deviousness and ask what should happen if the input to ValidateCheckDigit is a single digit? Given that the check digit is the rightmost digit in the string then that single digit is the check digit. If we assume that a check digit without an accompanying value is meaningless then any input less than two characters in length should fail. And most single digit inputs would would indeed fail with the code as it currently is. But based on our understanding of the algorithm, we can surmise that if there are no characters to process, then the calculated check digit would be zero, which would mean that a single character “0” would return true instead of false. This is definitely an edge case, but one that we’ll guard against with the following test and modification to ValidateCheckDigit.

   [Theory]
[InlineData("0")]
[InlineData("1")]
public void ValidateCheckDigit_ShouldReturnFalse_WhenInputIsOneCharacterInLength(String value)
=> LuhnAlgorithm.ValidateCheckDigit(value).Should().BeFalse();

public static Boolean ValidateCheckDigit(String str)
{
if (String.IsNullOrEmpty(str) || str.Length < 2)
{
return false;
}

var sum = 0;
.
.
.
}

Finally, let’s consider what happens when the input string contains a non-digit character. Given that the Luhn algorithm is defined for integer digits only, the presence of a non-digit character should mean that any check digit included in the input is invalid. In our code we’re using Char.GetNumericValue to convert each character to a integer and it returns -1 for any non digit character. Depending on if the non-digit character occurred in an odd or even position, we would either add -1 or +1 to the sum used to calculate the check digit. It’s likely, but not certain, that the calculated check digit would not match the input string. Because the algorithm uses modulo 10 when calculating the check digit, there is a one in 10 chance that the calculated check digit would match the input string. So we do need to modify our code to handle that case.

To test for this situation, I used a Theory test with 10 test cases that all used the same input, including a non-digit character. Then I changed the check digit in each case so I had check digits that ranged from 0–9. When we run that test, we should seen that all but one of the test cases pass and was exactly what happened. Then I modified ValidateCheckDigit so that all the test cases passed.

   [Theory]
[InlineData("123A780")]
[InlineData("123A782")]
[InlineData("123A783")]
[InlineData("123A784")]
[InlineData("123A785")]
[InlineData("123A786")]
[InlineData("123A787")]
[InlineData("123A788")]
[InlineData("123A789")]
public void ValidateCheckDigit_ShouldReturnFalse_WhenInputContainsNonDigitCharacter(String value)
=> LuhnAlgorithm.ValidateCheckDigit(value).Should().BeFalse();

public static Boolean ValidateCheckDigit_NonDigit(String str)
{
.
.
.
var currentDigit = (Int32)Char.GetNumericValue(str, index);
if (currentDigit < 0)
{
return false;
}
.
.
.
}

Here is the full implementation that passes all the tests we’ve created.

   public static Boolean ValidateCheckDigit(String str)
{
if (String.IsNullOrEmpty(str) || str.Length < 2)
{
return false;
}

var sum = 0;
var shouldApplyDouble = true;
for (var index = str.Length - 2; index >= 0; index--)
{
var currentDigit = (Int32)Char.GetNumericValue(str, index);
if (currentDigit < 0)
{
return false;
}
if (shouldApplyDouble)
{
if (currentDigit > 4)
{
sum += currentDigit * 2 - 9;
}
else
{
sum += currentDigit * 2;
}
}
else
{
sum += currentDigit;
}
shouldApplyDouble = !shouldApplyDouble;
}
var checkDigit = (10 - (sum % 10)) % 10;

return Char.GetNumericValue(str[^1]) == checkDigit;
}

At this point we’ve developed a robust set of test cases that cover normal success and failure modes and a wide variety of edge cases that could cause unexpected errors. We’ve also developed a basic implementation of ValidateCheckDigit that handles all the test cases. Aside from some minor code style tweaks you could stop at this point and be comfortable that you had done a reasonable job of addressing the requirements.

Optimization

But what about performance? Depending on the application, that might be an issue. Credit card processors handle millions, if not tens or hundreds of millions of transactions per day. In high volume situations you’d want the most performant version you can get. After reviewing ValidateCheckDigit I can see a couple possible areas for improvement: the conversion of each character to an integer, the handling of the doubled values.

Attempting to optimize a piece of code without data is a pointless exercise. So I added a new project to the solution and added a dependency to Benchmark.Net. Then I created a benchmark to establish a baseline.

[MemoryDiagnoser]
public class LuhnAlgorithmBenchmarks
{
[Params("1234567897", "5555555555554444")]
public String Value { get; set; } = String.Empty;

[Benchmark(Baseline = true)]
public void BaseLine()
{
_ = LuhnAlgorithm.ValidateCheckDigit(Value);
}
}

To optimize the conversion of each character to an integer, I took a closer look at Char.GetNumericValue. GetNumericValue has to support a variety of Unicode characters beyond the decimal digits (0–9) that we require. It also handles UnicodeCategory.LetterNumber (Roman numerals!) and UnicodeCategory.OtherNumber (fractions, etc.) which is the reason that it returns a floating-point value instead of an integer. There should be another way to handle the conversion without the overhead that GetNumericValue brings.

Back before .Net and Unicode, C programmers used the fact that the char type was just another integer type with limited range to perform operations on ASCII characters. Uppercase to lowercase conversion could be performed by adding 32 to an uppercase character. And you could convert a decimal character to its integer equivalent by subtracting the char constant ‘0’ from the character. Even though the .Net Char type is two bytes rather than one to support Unicode, it is still an integer type and addition/subtraction operations are defined for the type. So we could create a new version of ValidateCheckDigit with the following change:

   public static Boolean ValidateCheckDigit_CharConversion(String str)
{
.
.
.
var currentDigit = str[index] - '0';
.
.
.
return str[^1] - '0' == checkDigit;
}

Then we add another benchmark for the new version. When we run the benchmarks (be sure to run in Release mode, not Debug) we get:

   [Benchmark]
public void CharConversion()
{
_ = LuhnAlgorithm.ValidateCheckDigit_CharConversion(Value);
}

| Method | Value | Mean | Error | StdDev | Ratio | Allocated | Alloc Ratio |
|--------------- |----------------- |---------:|---------:|---------:|------:|----------:|------------:|
| BaseLine | 1234567897 | 53.09 ns | 0.479 ns | 0.425 ns | 1.00 | - | NA |
| CharConversion | 1234567897 | 10.15 ns | 0.061 ns | 0.057 ns | 0.19 | - | NA |
| | | | | | | | |
| BaseLine | 5555555555554444 | 81.51 ns | 0.376 ns | 0.352 ns | 1.00 | - | NA |
| CharConversion | 5555555555554444 | 16.13 ns | 0.055 ns | 0.046 ns | 0.20 | - | NA |

Wow, that one change cut the run time by 80% for a 5X improvement. Chalk one up for the old guys!

But before we assume that everything is fine, we need to re-run all our tests to make sure that we haven’t introduced a bug. And sure enough, one of our tests fails. It’s the test for a non-digit character, though it’s a different case than the one that had failed previously. The issue is that the new conversion doesn’t return -1 if the character isn’t a digit. It could return a negative number if the character being converted precedes ‘0’ in the ASCII table or a positive number if the character follows ‘9’ in the ASCII table. The solution is to update the check done after the conversion like this:

public static Boolean ValidateCheckDigit_CharConversion(String str)
{
.
.
.
var currentDigit = str[index] - '0';
if (currentDigit < 0 || currentDigit > 9)
{
return false;
}
.
.
.
}

Once the tests pass, rerun the benchmarks to confirm that the fix hasn’t impacted the performance. The new benchmarks show a slight performance hit due to the extra check, but the improvement is still 4X or better. This new version will be our baseline going forward.

Next, we’ll look at the doubling of the odd position values. There are only ten possible values to double, so we’re performing the same calculations over and over. A standard optimization for repeated calculations is to cache the values in a lookup table. In this case we can go even further and precompute the lookup table. The changes would look something like this:

   private static readonly Int32[] _doubledValues = new Int32[] { 0, 2, 4, 6, 8, 1, 3, 5, 7, 9 };

public static Boolean ValidateCheckDigit_DoubleLookup(String str)
{
if (String.IsNullOrEmpty(str) || str.Length < 2)
{
return false;
}

var sum = 0;
var shouldApplyDouble = true;
for (var index = str.Length - 2; index >= 0; index--)
{
var currentDigit = str[index] - '0';
if (currentDigit < 0 || currentDigit > 9)
{
return false;
}
sum += shouldApplyDouble ? _doubledValues[currentDigit] : currentDigit;
shouldApplyDouble = !shouldApplyDouble;
}
var checkDigit = (10 - (sum % 10)) % 10;

return str[^1] - '0' == checkDigit;
}

This version has immediate appeal simply because it’s more concise. But what is the performance impact?

   [Benchmark]
public void DoubleLookup()
{
_ = LuhnAlgorithm.ValidateCheckDigit_DoubleLookup(Value);
}

| Method | Value | Mean | Error | StdDev | Ratio | RatioSD | Allocated | Alloc Ratio |
|--------------- |----------------- |---------:|---------:|---------:|------:|--------:|----------:|------------:|
| BaseLine | 1234567897 | 12.94 ns | 0.275 ns | 0.270 ns | 1.00 | 0.00 | - | NA |
| DoubleLookup | 1234567897 | 11.98 ns | 0.087 ns | 0.081 ns | 0.92 | 0.02 | - | NA |
| | | | | | | | | |
| BaseLine | 5555555555554444 | 18.81 ns | 0.200 ns | 0.187 ns | 1.00 | 0.00 | - | NA |
| DoubleLookup | 5555555555554444 | 18.79 ns | 0.218 ns | 0.204 ns | 1.00 | 0.01 | - | NA |

There is little or no performance improvement, but I’d still be tempted to keep the change anyway because the code is much more concise.

But what about other approaches? Because we’re working with integers, we could use a bit shift to multiply by 2. Or we could simply add the number twice. Lets look at both options and their performance.

   public static Boolean ValidateCheckDigit_BitShift(String str)
{
.
.
.
if (currentDigit > 4)
{
sum += (currentDigit << 1) - 9;
}
else
{
sum += (currentDigit << 1);
}
.
.
.
}

public static Boolean ValidateCheckDigit_Addition(String str)
{
.
.
.
sum += currentDigit;
if (shouldApplyDouble)
{
sum += (currentDigit > 4) ? currentDigit - 9 : currentDigit;
}
.
.
.
}

[Benchmark]
public void BitShift()
{
_ = LuhnAlgorithm.ValidateCheckDigit_BitShift(Value);
}

[Benchmark]
public void Addition()
{
_ = LuhnAlgorithm.ValidateCheckDigit_Addition(Value);
}

| Method | Value | Mean | Error | StdDev | Ratio | Allocated | Alloc Ratio |
|--------- |----------------- |---------:|---------:|---------:|------:|----------:|------------:|
| BaseLine | 1234567897 | 12.90 ns | 0.068 ns | 0.057 ns | 1.00 | - | NA |
| Lookup | 1234567897 | 12.01 ns | 0.122 ns | 0.114 ns | 0.93 | - | NA |
| BitShift | 1234567897 | 12.60 ns | 0.075 ns | 0.070 ns | 0.98 | - | NA |
| Addition | 1234567897 | 14.04 ns | 0.070 ns | 0.066 ns | 1.09 | - | NA |
| | | | | | | | |
| BaseLine | 5555555555554444 | 18.76 ns | 0.099 ns | 0.088 ns | 1.00 | - | NA |
| Lookup | 5555555555554444 | 18.61 ns | 0.152 ns | 0.142 ns | 0.99 | - | NA |
| BitShift | 5555555555554444 | 18.84 ns | 0.127 ns | 0.119 ns | 1.00 | - | NA |
| Addition | 5555555555554444 | 20.34 ns | 0.066 ns | 0.062 ns | 1.08 |

As you can see, neither option offers a performance advantage. In fact, it’s quite likely that the C# compiler is optimizing the 2x multiplication behind the scenes for us anyway. So we’ll stick with the lookup approach because the code is more concise. We’re almost done. However…

By introducing the precomputed lookup table, we have extracted part of the algorithm and performed it offline and injected the results into our code thus introducing the possibility that one or more of the entries in the table are incorrect (either because an entry was calculated incorrectly or it was transcribed into the code incorrectly) or that an entry was omitted entirely. And there is no guarantee that our existing suite of tests will catch that. Remember that our very first test contained two test cases that check that the doubling of odd position characters was being done correctly. We’ll expand those two test cases to cover every entry in the lookup table.

   [Theory]
[InlineData("00")] // Expanded tests to cover doubling lookup table
[InlineData("18")] // "
[InlineData("26")] // "
[InlineData("34")] // "
[InlineData("42")] // "
[InlineData("59")] // "
[InlineData("67")] // "
[InlineData("75")] // "
[InlineData("83")] // "
[InlineData("91")] // "
[InlineData("133")]
[InlineData("5555555555554444")] // MasterCard test credit card number
[InlineData("4012888888881881")] // Visa test credit card number
[InlineData("3056930009020004")] // Diners Club test credit card number
[InlineData("3566111111111113")] // JCB test credit card number
[InlineData("808401234567893")] // NPI (National Provider Identifier)
[InlineData("490154203237518")] // IMEI (International Mobile Equipment Identity)
public void ValidateCheckDigit_ShouldReturnTrue_WhenInputContainsValidCheckDigit(String value)
=> LuhnAlgorithmV8.ValidateCheckDigit(value).Should().BeTrue();

Then after running the tests once more to ensure that the change hasn’t broken anything we can say that we’re done.

Final Version

Here is our final resilient and performance optimized version.

public static class LuhnAlgorithm
{
private static readonly Int32[] _doubledValues = new Int32[] { 0, 2, 4, 6, 8, 1, 3, 5, 7, 9 };

public static Boolean ValidateCheckDigit(String str)
{
if (String.IsNullOrEmpty(str) || str.Length < 2)
{
return false;
}

var sum = 0;
var shouldApplyDouble = true;
for (var index = str.Length - 2; index >= 0; index--)
{
var currentDigit = str[index] - '0';
if (currentDigit < 0 || currentDigit > 9)
{
return false;
}
sum += shouldApplyDouble ? _doubledValues[currentDigit] : currentDigit;
shouldApplyDouble = !shouldApplyDouble;
}
var checkDigit = (10 - (sum % 10)) % 10;

return str[^1] - '0' == checkDigit;
}
}

Thanks for reading!

About Me

I’m just Some Random Programmer Guy, at least according to the nameplate that was once waiting for me on my first day at a startup. I’ve been around a while and my career has spanned FORTRAN and PL/1 on IBM mainframes to .Net Core microservices with some interesting forays with C/C++, OS/2, VB, C#, SQL, NoSQL and more along the way.

Code for this article is available my public github repository.

--

--