0.2.0 LUHN

This commit is contained in:
Rob Tillaart 2023-05-09 13:23:32 +02:00
parent 56083a3b85
commit 43f55d1eaa
12 changed files with 349 additions and 45 deletions

View File

@ -6,6 +6,19 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [0.2.0] - 2023-05-08
- add unit test
- fix stream mode - **add()** and **reset()**
- fix bugs in generation / validation.
- add **generateChecksum(const char \* buffer)**
- change **count** to be 32 bit to support really large streams.
- add **count()** function to return internal counter.
- updates readme.md
- 0.1.x versions are obsolete
----
## [0.1.1] - 2022-12-29
- add stream interface
- **char add(char c)**
@ -14,6 +27,5 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- update documentation
- fix warning
## [0.1.0] - 2022-12-24
- initial version

View File

@ -1,7 +1,7 @@
//
// FILE: LUHN.cpp
// AUTHOR: Rob Tillaart
// VERSION: 0.1.1
// VERSION: 0.2.0
// DATE: 2022-12-24
// PURPOSE: Arduino Library for calculating LUHN checksum.
// URL: https://github.com/RobTillaart/LUHN
@ -12,7 +12,8 @@
LUHN::LUHN()
{
_luhn = 0;
_luhnEven = 0;
_luhnOdd = 0;
_count = 0;
}
@ -29,10 +30,11 @@ bool LUHN::isValid(char * buffer)
uint8_t length = strlen(buffer);
if (length == 0) return false;
uint8_t parity = length & 1;
for (int i = 0; i < length-1; i++)
{
uint8_t x = buffer[i] - '0';
if (i % 2 == 0) checksum += x; // weight 1
if (i % 2 != parity) checksum += x; // weight 1
else if (x < 5) checksum += x * 2; // weight 2
else checksum += (x * 2 - 10 + 1); // weight 2 + handle overflow.
}
@ -42,14 +44,21 @@ bool LUHN::isValid(char * buffer)
}
char LUHN::generateChecksum(const char * buffer)
{
return generateChecksum((char *) buffer);
}
char LUHN::generateChecksum(char * buffer)
{
uint16_t checksum = 0;
uint8_t length = strlen(buffer);
uint8_t parity = length & 1;
for (int i = 0; i < length; i++)
{
uint8_t x = buffer[i] - '0';
if (i % 2 == 0) checksum += x; // weight 1
if (i % 2 == parity) checksum += x; // weight 1
else if (x < 5) checksum += x * 2; // weight 2
else checksum += (x * 2 - 10 + 1); // weight 2 + handle overflow.
}
@ -83,25 +92,44 @@ bool LUHN::generate(char * buffer, uint8_t length, char * prefix)
}
//////////////////////////////////////////////////////////////
//
// STREAM INTERFACE
//
char LUHN::add(char c)
{
// as we do not know the final length in advance
// both parity's must be calculated.
uint8_t x = c - '0';
if (_count % 2 == 0) _luhn += x;
else if (x < 5) _luhn += x * 2;
else _luhn += (x * 2 - 10 + 1);
// correct
if (_luhn > 9) _luhn -= 10;
// handle even lengths
if (_count % 2 == 0) _luhnEven += x;
else if (x < 5) _luhnEven += x * 2;
else _luhnEven += (x * 2 - 10 + 1);
// handle odd lengths
if (_count % 2 == 1) _luhnOdd += x;
else if (x < 5) _luhnOdd += x * 2;
else _luhnOdd += (x * 2 - 10 + 1);
_count++;
return '0' + (10 - _luhn);
if (_count & 1) return '0' + (100 - _luhnOdd) % 10;
return '0' + (100 - _luhnEven) % 10;
}
char LUHN::reset()
{
uint8_t last = _luhn;
_luhn = 0;
uint8_t last = _luhnEven;
if (_count & 1) last = _luhnOdd;
_luhnEven = 0;
_luhnOdd = 0;
_count = 0;
return '0' + (10 - last);
return '0' + (100 - last) % 10;
}
uint32_t LUHN::count()
{
return _count;
}
@ -125,5 +153,25 @@ uint8_t LUHN::Marsaglia_mod10()
}
// EXPERIMENTAL
// ~8% faster and 58 bytes bigger (UNO)
// uint8_t LUHN::Marsaglia_mod10()
// {
// static uint32_t value;
// static uint8_t digits = 0;
// if (digits == 0)
// {
// digits = 4;
// m_z = 36969L * (m_z & 65535L) + (m_z >> 16);
// m_w = 18000L * (m_w & 65535L) + (m_w >> 16);
// value = (m_z ^ m_w);
// }
// uint8_t rv = value % 10;
// value >>= 8;
// digits--;
// return rv;
// }
// -- END OF FILE --

View File

@ -2,7 +2,7 @@
//
// FILE: LUHN.h
// AUTHOR: Rob Tillaart
// VERSION: 0.1.1
// VERSION: 0.2.0
// DATE: 2022-12-24
// PURPOSE: Arduino Library for calculating LUHN checksum.
// URL: https://github.com/RobTillaart/LUHN
@ -10,7 +10,7 @@
#include "Arduino.h"
#define LUHN_LIB_VERSION (F("0.1.1"))
#define LUHN_LIB_VERSION (F("0.2.0"))
class LUHN
@ -22,6 +22,7 @@ public:
// buffer == \0 terminated
bool isValid(const char * buffer);
bool isValid(char * buffer);
char generateChecksum(const char * buffer);
char generateChecksum(char * buffer);
// GENERATE A PRODUCT ID WITH LUHN CHECKSUM
@ -31,15 +32,16 @@ public:
// STREAM INTERFACE
char add(char c);
char reset();
uint32_t count();
protected:
uint32_t m_w = 1; // random generator parameter
uint32_t m_z = 2; // random generator parameter
uint8_t Marsaglia_mod10();
uint8_t _luhn = 0;
uint16_t _count = 0;
uint16_t _luhnEven = 0;
uint16_t _luhnOdd = 0;
uint32_t _count = 0;
};

View File

@ -20,13 +20,28 @@ The LUHN check uses very few resources and is pretty fast.
Basic idea is to put all digits-1 through the formula and the output should equal the last digit.
Note: some LUHN validations uses the reversed product string.
This LUHN library also includes a "stream" based LUHN calculation, in which digits can be add
one at a time (from a stream) and it will return the LUHN checksum so far.
This is a new application as LUHN depends on the length of the input being odd or even.
To handle this two values are maintained (for odd and even lengths) and the correct one is returned.
Maintaining two checksums makes the stream **add(c)** algorithm substantial slower (~4x) than
the normally used **generateChecksum(buffer)** or the **isValid(buffer)**.
However that is a small price for the new functionality.
The amount of data that can be added in stream mode is infinite in theory.
However that is not tested for obvious reasons, internally a 32 bit counter exists.
#### Notes
- some LUHN validations uses the reversed product string.
- 0.1.x versions are obsolete due to incorrect math.
#### Links
- https://en.wikipedia.org/wiki/Luhn_algorithm
#### related
- https://github.com/RobTillaart/Adler
- https://github.com/RobTillaart/CRC
- https://github.com/RobTillaart/Fletcher
@ -46,6 +61,7 @@ The parameter buffer is a '\0' terminated char array. Length should be less than
- **char generateChecksum(char \* buffer)**
Returns the char '0'..'9' which is the checksum of the code in the parameter buffer.
The parameter buffer is a '\0' terminated char array. Length should be less than 254.
- **char generateChecksum(const char \* buffer)** idem.
- **bool generate(char \* buffer, uint8_t length, char \* prefix)**
Generates a char array including LUHN number with a defined prefix of max length.
Returns false if the prefix exceeds length -1.
@ -53,26 +69,37 @@ Returns false if the prefix exceeds length -1.
#### Stream
- **char add(char c)** add char, return LUHN so far.
- **char add(char c)** add char, returns LUHN so far.
- **char reset()** return last LUHN.
- **uint32_t count()** return internal counter.
If this value is zero, a new LUHN can be calculated, otherwise call **reset()** first.
The internal counter for the stream interface is 32 bit.
This limits the number of add() calls to about 4 billion.
For current implementation the counter is used for even/odd detection,
and even when it overflows one gets the correct **LUHN**.
The internal counter for the stream interface is 16 bit.
This limits the nr of add() calls to about 65530.
(if this is a problem, make it an uinit32_t)
## Future
#### must
#### Must
- update documentation
#### should
#### Should
- unit tests
- look for optimization
- look for optimizations
#### could
#### Could
#### Won't (unless)
- create a HEX equivalent of LUHN
- LUHN16 ?
- easy to enter HEX code with verify / line.
- mod N configurable so not only 10 but any N?
- uint64_t interface for up to 17 digits.
- expensive on small processors.
- uint32_t interface for up to 8 digit ID's (99.999.999)
- **isValid(uint32_t)**
- **generateChecksum(uint32_t)**

View File

@ -23,6 +23,8 @@ void setup()
while (!Serial);
Serial.println();
Serial.println(__FILE__);
Serial.print("LUHN_LIB_VERSION: ");
Serial.println(LUHN_LIB_VERSION);
// SHOULD PRINT 3
Serial.println(checker.generateChecksum((char *)"7992739871"));

View File

@ -24,8 +24,10 @@ void setup()
while (!Serial);
Serial.println();
Serial.println(__FILE__);
Serial.print("LUHN_LIB_VERSION: ");
Serial.println(LUHN_LIB_VERSION);
Serial.println("run I: debug");
Serial.println("run I: add() per character");
for (uint8_t i = 0; i < strlen(ID) - 1; i++)
{
c = checker.add(ID[i]);
@ -46,12 +48,45 @@ void setup()
}
c = checker.reset();
stop = micros();
Serial.println();
Serial.print("run II:\t");
Serial.println(c); // should also print 3 .
Serial.print(" time:\t");
Serial.print("LUHN:\t");
Serial.println(c); // should print 3.
Serial.print("time:\t");
Serial.print(stop - start);
Serial.println();
Serial.println();
delay(10);
Serial.println("run II: should have same 2nd column as run 1.");
for (uint8_t i = 0; i < strlen(ID); i++)
{
char tmp[32];
strcpy(tmp, ID);
tmp[i] = 0;
c = checker.generateChecksum(tmp);
Serial.print(tmp);
Serial.print("\t");
Serial.println(c);
}
Serial.println();
delay(10);
char tmp[32];
strcpy(tmp, ID);
tmp[strlen(ID)-1] = 0;
start = micros();
c = checker.generateChecksum(tmp);
stop = micros();
Serial.println();
Serial.println("run III: generateChecksum()");
Serial.print("LUHN:\t");
Serial.println(c); // should print 3.
Serial.print("time:\t");
Serial.print(stop - start);
Serial.println();
Serial.println();
delay(10);
Serial.println("\ndone...");
}

View File

@ -0,0 +1,40 @@
Arduino UNO
IDE 1.8.19
luhn_check_stream.ino
LUHN_LIB_VERSION: 0.2.0
run I: add() per character
7 5
9 4
9 7
2 1
7 0
3 8
9 8
8 2
7 5
1 3
LUHN: 3
time: 256
run II: should have same 2nd column as run 1.
0
7 5
79 4
799 7
7992 1
79927 0
799273 8
7992739 8
79927398 2
799273987 5
7992739871 3
run III: generateChecksum()
LUHN: 3
time: 64
done...

View File

@ -22,6 +22,10 @@ void setup()
while (!Serial);
Serial.println();
Serial.println(__FILE__);
Serial.print("LUHN_LIB_VERSION: ");
Serial.println(LUHN_LIB_VERSION);
Serial.println();
delay(10);
}
@ -35,7 +39,8 @@ void loop()
Serial.print(checker.isValid(number));
Serial.print("\t");
Serial.print(stop - start);
// Serial.print((1.0*(stop - start))/ strlen(number)); per digit
Serial.print("\t");
Serial.print((1.0 * (stop - start)) / strlen(number)); // per digit
Serial.print("\t");
Serial.println(number);
delay(100);

View File

@ -11,6 +11,8 @@ generateChecksum KEYWORD2
randomize KEYWORD2
generate KEYWORD2
add KEYWORD2
reset KEYWORD2
# Constants (LITERAL1)
LUHN_LIB_VERSION LITERAL1

View File

@ -15,7 +15,7 @@
"type": "git",
"url": "https://github.com/RobTillaart/LUHN.git"
},
"version": "0.1.1",
"version": "0.2.0",
"license": "MIT",
"frameworks": "arduino",
"platforms": "*",

View File

@ -1,5 +1,5 @@
name=LUHN
version=0.1.1
version=0.2.0
author=Rob Tillaart <rob.tillaart@gmail.com>
maintainer=Rob Tillaart <rob.tillaart@gmail.com>
sentence=Arduino Library for calculating LUHN checksum.

View File

@ -0,0 +1,131 @@
//
// FILE: unit_test_001.cpp
// AUTHOR: Rob Tillaart
// DATE: 2023-05-02
// PURPOSE: unit tests for the LUHN checksum
// https://github.com/RobTillaart/LUHN
// https://github.com/Arduino-CI/arduino_ci/blob/master/REFERENCE.md
//
// supported assertions
// ----------------------------
// assertEqual(expected, actual)
// assertNotEqual(expected, actual)
// assertLess(expected, actual)
// assertMore(expected, actual)
// assertLessOrEqual(expected, actual)
// assertMoreOrEqual(expected, actual)
// assertTrue(actual)
// assertFalse(actual)
// assertNull(actual)
#include <ArduinoUnitTests.h>
#include "Arduino.h"
#include "LUHN.h"
unittest_setup()
{
fprintf(stderr, "LUHN_LIB_VERSION: %s\n", (char *) LUHN_LIB_VERSION);
}
unittest_teardown()
{
}
unittest(test_constructor)
{
LUHN luhn;
assertEqual('0', luhn.reset());
assertEqual('0', luhn.reset());
}
unittest(test_isValid)
{
LUHN luhn;
assertTrue(luhn.isValid("0"));
assertTrue(luhn.isValid("79927398713"));
assertTrue(luhn.isValid("1111111"));
assertTrue(luhn.isValid("11111111111111111111"));
// generated by https://www.dcode.fr/luhn-algorithm
assertTrue(luhn.isValid("08192186963920766401"));
// https://www.mobilefish.com/services/credit_card_number_generator/credit_card_number_generator.php
fprintf(stderr, "Example American Express credit card number\n");
// An American Express credit card number starts with number 34 or 37 and
// the credit card number has total 15 digits:
assertTrue(luhn.isValid("377261620324999"));
assertTrue(luhn.isValid("349854194206314"));
fprintf(stderr, "Example Mastercard credit card number\n");
// A Mastercard credit card number starts with number 51, 52, 53, 54 or 55 and
// the credit card number has total 16 digits:
assertTrue(luhn.isValid("5181975718047403"));
assertTrue(luhn.isValid("5204571199083364"));
assertTrue(luhn.isValid("5322683667269933"));
assertTrue(luhn.isValid("5477754834149242"));
assertTrue(luhn.isValid("5539624233693270"));
fprintf(stderr, "Example Laser credit card number\n");
// A Laser credit card number starts with number 6304, 6706, 6771 or 6709 and
// the credit card number has total 16, 17, 18 or 19 digits:
assertTrue(luhn.isValid("670676038979126821"));
assertTrue(luhn.isValid("6771363087405086"));
assertTrue(luhn.isValid("6304096514549839"));
assertTrue(luhn.isValid("6304219447607087665"));
}
unittest(test_generateChecksum)
{
LUHN luhn;
assertEqual('3', luhn.generateChecksum("7992739871"));
}
unittest(test_generate)
{
LUHN luhn;
for (int i = 0; i < 10; i++)
{
char buffer[24];
char prefix[10] = "007";
assertTrue(luhn.generate(buffer, 20, prefix));
assertTrue(luhn.isValid(buffer));
fprintf(stderr, "%d %s\n", i, buffer);
}
}
unittest(test_stream)
{
LUHN luhn;
luhn.reset();
char buffer[24] = "7992739871";
for (int i = 0; i < strlen(buffer); i++)
{
char c = luhn.add(buffer[i]);
fprintf(stderr, "%d %c\n", i, c);
}
assertEqual('3', luhn.reset());
}
unittest_main()
// -- END OF FILE --