Skip to content

Commit ae3e728

Browse files
authored
Introduce Luhn Checksum Validation (#1009)
1 parent 0665b95 commit ae3e728

File tree

4 files changed

+100
-17
lines changed

4 files changed

+100
-17
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ Baked-in Validations
180180
| jwt | JSON Web Token (JWT) |
181181
| latitude | Latitude |
182182
| longitude | Longitude |
183+
| luhn_checksum | Luhn Algorithm Checksum (for strings and (u)int) |
183184
| postcode_iso3166_alpha2 | Postcode |
184185
| postcode_iso3166_alpha2_field | Postcode |
185186
| rgb | RGB String |

baked_in.go

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ var (
223223
"semver": isSemverFormat,
224224
"dns_rfc1035_label": isDnsRFC1035LabelFormat,
225225
"credit_card": isCreditCard,
226+
"luhn_checksum": hasLuhnChecksum,
226227
"mongodb": isMongoDB,
227228
"cron": isCron,
228229
}
@@ -2681,6 +2682,29 @@ func isDnsRFC1035LabelFormat(fl FieldLevel) bool {
26812682
return dnsRegexRFC1035Label.MatchString(val)
26822683
}
26832684

2685+
// digitsHaveLuhnChecksum returns true if and only if the last element of the given digits slice is the Luhn checksum of the previous elements
2686+
func digitsHaveLuhnChecksum(digits []string) bool {
2687+
size := len(digits)
2688+
sum := 0
2689+
for i, digit := range digits {
2690+
value, err := strconv.Atoi(digit)
2691+
if err != nil {
2692+
return false
2693+
}
2694+
if size%2 == 0 && i%2 == 0 || size%2 == 1 && i%2 == 1 {
2695+
v := value * 2
2696+
if v >= 10 {
2697+
sum += 1 + (v % 10)
2698+
} else {
2699+
sum += v
2700+
}
2701+
} else {
2702+
sum += value
2703+
}
2704+
}
2705+
return (sum % 10) == 0
2706+
}
2707+
26842708
// isMongoDB is the validation function for validating if the current field's value is valid mongoDB objectID
26852709
func isMongoDB(fl FieldLevel) bool {
26862710
val := fl.Field().String()
@@ -2705,24 +2729,29 @@ func isCreditCard(fl FieldLevel) bool {
27052729
return false
27062730
}
27072731

2708-
sum := 0
2709-
for i, digit := range ccDigits {
2710-
value, err := strconv.Atoi(digit)
2711-
if err != nil {
2712-
return false
2713-
}
2714-
if size%2 == 0 && i%2 == 0 || size%2 == 1 && i%2 == 1 {
2715-
v := value * 2
2716-
if v >= 10 {
2717-
sum += 1 + (v % 10)
2718-
} else {
2719-
sum += v
2720-
}
2721-
} else {
2722-
sum += value
2723-
}
2732+
return digitsHaveLuhnChecksum(ccDigits)
2733+
}
2734+
2735+
// hasLuhnChecksum is the validation for validating if the current field's value has a valid Luhn checksum
2736+
func hasLuhnChecksum(fl FieldLevel) bool {
2737+
field := fl.Field()
2738+
var str string // convert to a string which will then be split into single digits; easier and more readable than shifting/extracting single digits from a number
2739+
switch field.Kind() {
2740+
case reflect.String:
2741+
str = field.String()
2742+
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
2743+
str = strconv.FormatInt(field.Int(), 10)
2744+
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
2745+
str = strconv.FormatUint(field.Uint(), 10)
2746+
default:
2747+
panic(fmt.Sprintf("Bad field type %T", field.Interface()))
27242748
}
2725-
return (sum % 10) == 0
2749+
size := len(str)
2750+
if size < 2 { // there has to be at least one digit that carries a meaning + the checksum
2751+
return false
2752+
}
2753+
digits := strings.Split(str, "")
2754+
return digitsHaveLuhnChecksum(digits)
27262755
}
27272756

27282757
// isCron is the validation function for validating if the current field's value is a valid cron expression

doc.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1357,6 +1357,13 @@ This validates that a string value contains a valid credit card number using Luh
13571357
Usage: credit_card
13581358
13591359
1360+
# Luhn Checksum
1361+
1362+
Usage: luhn_checksum
1363+
1364+
This validates that a string or (u)int value contains a valid checksum using the Luhn algorithm.
1365+
1366+
13601367
#MongoDb ObjectID
13611368
13621369
This validates that a string is a valid 24 character hexadecimal string.
@@ -1372,6 +1379,7 @@ This validates that a string value contains a valid cron expression.
13721379
13731380
Alias Validators and Tags
13741381
1382+
Alias Validators and Tags
13751383
NOTE: When returning an error, the tag returned in "FieldError" will be
13761384
the alias tag unless the dive tag is part of the alias. Everything after the
13771385
dive tag is not reported as the alias tag. Also, the "ActualTag" in the before

validator_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12692,6 +12692,51 @@ func TestCreditCardFormatValidation(t *testing.T) {
1269212692
}
1269312693
}
1269412694

12695+
func TestLuhnChecksumValidation(t *testing.T) {
12696+
testsUint := []struct {
12697+
value interface{} `validate:"luhn_checksum"` // the type is interface{} because the luhn_checksum works on both strings and numbers
12698+
tag string
12699+
expected bool
12700+
}{
12701+
{uint64(586824160825533338), "luhn_checksum", true}, // credit card numbers are just special cases of numbers with luhn checksum
12702+
{586824160825533338, "luhn_checksum", true},
12703+
{"586824160825533338", "luhn_checksum", true},
12704+
{uint64(586824160825533328), "luhn_checksum", false},
12705+
{586824160825533328, "luhn_checksum", false},
12706+
{"586824160825533328", "luhn_checksum", false},
12707+
{10000000116, "luhn_checksum", true}, // but there may be shorter numbers (11 digits)
12708+
{"10000000116", "luhn_checksum", true},
12709+
{10000000117, "luhn_checksum", false},
12710+
{"10000000117", "luhn_checksum", false},
12711+
{uint64(12345678123456789011), "luhn_checksum", true}, // or longer numbers (19 digits)
12712+
{"12345678123456789011", "luhn_checksum", true},
12713+
{1, "luhn_checksum", false}, // single digits (checksum only) are not allowed
12714+
{"1", "luhn_checksum", false},
12715+
{-10, "luhn_checksum", false}, // negative ints are not allowed
12716+
{"abcdefghijklmnop", "luhn_checksum", false},
12717+
}
12718+
12719+
validate := New()
12720+
12721+
for i, test := range testsUint {
12722+
errs := validate.Var(test.value, test.tag)
12723+
if test.expected {
12724+
if !IsEqual(errs, nil) {
12725+
t.Fatalf("Index: %d luhn_checksum failed Error: %s", i, errs)
12726+
}
12727+
} else {
12728+
if IsEqual(errs, nil) {
12729+
t.Fatalf("Index: %d luhn_checksum failed Error: %s", i, errs)
12730+
} else {
12731+
val := getError(errs, "", "")
12732+
if val.Tag() != "luhn_checksum" {
12733+
t.Fatalf("Index: %d luhn_checksum failed Error: %s", i, errs)
12734+
}
12735+
}
12736+
}
12737+
}
12738+
}
12739+
1269512740
func TestMultiOrOperatorGroup(t *testing.T) {
1269612741
tests := []struct {
1269712742
Value int `validate:"eq=1|gte=5,eq=1|lt=7"`

0 commit comments

Comments
 (0)