diff --git a/v2/luhn.go b/v2/luhn.go new file mode 100644 index 0000000..9b3f5e4 --- /dev/null +++ b/v2/luhn.go @@ -0,0 +1,61 @@ +// Copyright (c) 2023-2024 Onur Cinar. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +// https://github.com/cinar/checker + +package v2 + +import ( + "reflect" + "unicode" +) + +const ( + // nameLUHN is the name of the LUHN check. + nameLUHN = "luhn" +) + +var ( + // ErrNotLUHN indicates that the given value is not a valid LUHN number. + ErrNotLUHN = NewCheckError("LUHN") +) + +// IsLUHN checks if the value is a valid LUHN number. +func IsLUHN(value string) (string, error) { + var sum int + var alt bool + + for i := len(value) - 1; i >= 0; i-- { + r := rune(value[i]) + if !unicode.IsDigit(r) { + return value, ErrNotLUHN + } + + n := int(r - '0') + if alt { + n *= 2 + if n > 9 { + n -= 9 + } + } + sum += n + alt = !alt + } + + if sum%10 != 0 { + return value, ErrNotLUHN + } + + return value, nil +} + +// checkLUHN checks if the value is a valid LUHN number. +func checkLUHN(value reflect.Value) (reflect.Value, error) { + _, err := IsLUHN(value.Interface().(string)) + return value, err +} + +// makeLUHN makes a checker function for the LUHN checker. +func makeLUHN(_ string) CheckFunc[reflect.Value] { + return checkLUHN +} diff --git a/v2/luhn_test.go b/v2/luhn_test.go new file mode 100644 index 0000000..2dae79d --- /dev/null +++ b/v2/luhn_test.go @@ -0,0 +1,76 @@ +// Copyright (c) 2023-2024 Onur Cinar. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +// https://github.com/cinar/checker + +package v2_test + +import ( + "fmt" + "testing" + + v2 "github.com/cinar/checker/v2" +) + +func ExampleIsLUHN() { + _, err := v2.IsLUHN("79927398713") + if err != nil { + fmt.Println(err) + } +} + +func TestIsLUHNInvalid(t *testing.T) { + _, err := v2.IsLUHN("123456789") + if err == nil { + t.Fatal("expected error") + } +} + +func TestIsLUHNValid(t *testing.T) { + _, err := v2.IsLUHN("79927398713") + if err != nil { + t.Fatal(err) + } +} + +func TestCheckLUHNNonString(t *testing.T) { + defer FailIfNoPanic(t, "expected panic") + + type Card struct { + Number int `checkers:"luhn"` + } + + card := &Card{} + + v2.CheckStruct(card) +} + +func TestCheckLUHNInvalid(t *testing.T) { + type Card struct { + Number string `checkers:"luhn"` + } + + card := &Card{ + Number: "123456789", + } + + _, ok := v2.CheckStruct(card) + if ok { + t.Fatal("expected error") + } +} + +func TestCheckLUHNValid(t *testing.T) { + type Card struct { + Number string `checkers:"luhn"` + } + + card := &Card{ + Number: "79927398713", + } + + _, ok := v2.CheckStruct(card) + if !ok { + t.Fatal("expected valid") + } +} diff --git a/v2/maker.go b/v2/maker.go index 5943eef..1a49c6e 100644 --- a/v2/maker.go +++ b/v2/maker.go @@ -26,6 +26,7 @@ var makers = map[string]MakeCheckFunc{ nameIPv4: makeIPv4, nameIPv6: makeIPv6, nameISBN: makeISBN, + nameLUHN: makeLUHN, nameMaxLen: makeMaxLen, nameMinLen: makeMinLen, nameRequired: makeRequired,