diff --git a/README.md b/README.md index 44ffffc..b9afa74 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ This package currently provides the following checkers: - [ip](doc/checkers/ip.md) checks if the given value is an IP address. - [ipv4](doc/checkers/ipv4.md) checks if the given value is an IPv4 address. - [ipv6](doc/checkers/ipv6.md) checks if the given value is an IPv6 address. +- [isbn](doc/checkers/isbn.md) checks if the given value is a valid ISBN number. - [luhn](doc/checkers/luhn.md) checks if the given number is valid based on the Luhn algorithm. - [mac](doc/checkers/mac.md) checks if the given value is a valid an IEEE 802 MAC-48, EUI-48, EUI-64, or a 20-octet IP over InfiniBand link-layer address. - [max](doc/checkers/max.md) checks if the given value is less than the given maximum. diff --git a/checker.go b/checker.go index 4a5cd6b..fafa56c 100644 --- a/checker.go +++ b/checker.go @@ -41,6 +41,7 @@ var makers = map[string]MakeFunc{ CheckerIP: makeIP, CheckerIPV4: makeIPV4, CheckerIPV6: makeIPV6, + CheckerISBN: makeISBN, CheckerLuhn: makeLuhn, CheckerMac: makeMac, CheckerMax: makeMax, diff --git a/doc/checkers/isbn.md b/doc/checkers/isbn.md new file mode 100644 index 0000000..40a936f --- /dev/null +++ b/doc/checkers/isbn.md @@ -0,0 +1,52 @@ +# ISBN Checker + +An [ISBN (International Standard Book Number)](https://en.wikipedia.org/wiki/International_Standard_Book_Number) is a 10 or 13 digit number that is used to identify a book. + +The `isbn` checker checks if the given value is a valid ISBN. If the given value is not a valid ISBN, the checker will return the `NOT_ISBN` result. Here is an example: + +```golang +type Book struct { + ISBN string `checkers:"isbn"` +} + +book := &Book{ + ISBN: "1430248270", +} + +_, valid := checker.Check(book) +if !valid { + // Send the mistakes back to the user +} +``` + +The `isbn` checker can also be configured to check for a 10 digit or a 13 digit number. Here is an example: + +```golang +type Book struct { + ISBN string `checkers:"isbn:13"` +} + +book := &Book{ + ISBN: "9781430248279", +} + +_, valid := checker.Check(book) +if !valid { + // Send the mistakes back to the user +} +``` + +In your custom checkers, you can call the `isbn` checker functions below to validate the user input. + +- [`IsISBN`](https://pkg.go.dev/github.com/cinar/checker#IsISBN) checks if the given value is a valid ISBN number. +- [`IsISBN10`](https://pkg.go.dev/github.com/cinar/checker#IsISBN10) checks if the given value is a valid ISBN 10 number. +- [`IsISBN13`](https://pkg.go.dev/github.com/cinar/checker#IsISBN13) checks if the given value is a valid ISBN 13 number. + +Here is an example: + +```golang +result := checker.IsISBN("1430248270") +if result != checker.ResultValid { + // Send the mistakes back to the user +} +``` diff --git a/doc/checkers/url.md b/doc/checkers/url.md index 5d7778f..c41b551 100644 --- a/doc/checkers/url.md +++ b/doc/checkers/url.md @@ -1,6 +1,6 @@ # URL Checker -The ```url``` checker checks if the given value is a valid URL. If the given value is not a valid URL, the checker will return the ```NOT_URL``` result. The checker uses [ParseRequestURI](https://pkg.go.dev/net/url#ParseRequestURI) function to parse the URL, and then checks if the schema or the host are both set. +The `url` checker checks if the given value is a valid URL. If the given value is not a valid URL, the checker will return the `NOT_URL` result. The checker uses [ParseRequestURI](https://pkg.go.dev/net/url#ParseRequestURI) function to parse the URL, and then checks if the schema or the host are both set. Here is an example: @@ -19,7 +19,7 @@ if !valid { } ``` -In your custom checkers, you can call the ```url``` checker function ```IsURL``` to validate the user input. Here is an example: +In your custom checkers, you can call the `url` checker function [`IsURL`](https://pkg.go.dev/github.com/cinar/checker#IsURL) to validate the user input. Here is an example: ```golang result := checker.IsURL("https://zdo.com") diff --git a/isbn.go b/isbn.go new file mode 100644 index 0000000..11e50db --- /dev/null +++ b/isbn.go @@ -0,0 +1,121 @@ +package checker + +import ( + "reflect" + "strings" +) + +// Program to check for ISBN +// https://www.geeksforgeeks.org/program-check-isbn/ + +// How to Verify an ISBN +// https://www.instructables.com/How-to-verify-a-ISBN/ + +// CheckerISBN is the name of the checker. +const CheckerISBN = "isbn" + +// ResultNotISBN indicates that the given value is not a valid ISBN. +const ResultNotISBN = "NOT_ISBN" + +// IsISBN10 checks if the given value is a valid ISBN-10 number. +func IsISBN10(value string) Result { + value = strings.ReplaceAll(value, "-", "") + + if len(value) != 10 { + return ResultNotISBN + } + + digits := []rune(value) + sum := 0 + + for i, e := 0, len(digits); i < e; i++ { + n := isbnDigitToInt(digits[i]) + sum += n * (e - i) + } + + if sum%11 != 0 { + return ResultNotISBN + } + + return ResultValid +} + +// IsISBN13 checks if the given value is a valid ISBN-13 number. +func IsISBN13(value string) Result { + value = strings.ReplaceAll(value, "-", "") + + if len(value) != 13 { + return ResultNotISBN + } + + digits := []rune(value) + sum := 0 + + for i, d := range digits { + n := isbnDigitToInt(d) + if i%2 != 0 { + n *= 3 + } + + sum += n + } + + if sum%10 != 0 { + return ResultNotISBN + } + + return ResultValid +} + +// IsISBN checks if the given value is a valid ISBN number. +func IsISBN(value string) Result { + value = strings.ReplaceAll(value, "-", "") + + if len(value) == 10 { + return IsISBN10(value) + } else if len(value) == 13 { + return IsISBN13(value) + } + + return ResultNotISBN +} + +// isbnDigitToInt returns the integer value of given ISBN digit. +func isbnDigitToInt(r rune) int { + if r == 'X' { + return 10 + } + + return int(r - '0') +} + +// makeISBN makes a checker function for the URL checker. +func makeISBN(config string) CheckFunc { + if config != "" && config != "10" && config != "13" { + panic("invalid format") + } + + return func(value, parent reflect.Value) Result { + return checkISBN(value, parent, config) + } +} + +// checkISBN checks if the given value is a valid ISBN number. +func checkISBN(value, _ reflect.Value, mode string) Result { + if value.Kind() != reflect.String { + panic("string expected") + } + + number := value.String() + + switch mode { + case "10": + return IsISBN10(number) + + case "13": + return IsISBN13(number) + + default: + return IsISBN(number) + } +} diff --git a/isbn_test.go b/isbn_test.go new file mode 100644 index 0000000..60becc7 --- /dev/null +++ b/isbn_test.go @@ -0,0 +1,162 @@ +package checker_test + +import ( + "testing" + + "github.com/cinar/checker" +) + +func TestIsISBN10Valid(t *testing.T) { + result := checker.IsISBN10("1430248270") + if result != checker.ResultValid { + t.Fail() + } +} + +func TestIsISBN10ValidX(t *testing.T) { + result := checker.IsISBN10("007462542X") + if result != checker.ResultValid { + t.Fail() + } +} + +func TestIsISBN10ValidWithDashes(t *testing.T) { + result := checker.IsISBN10("1-4302-4827-0") + if result != checker.ResultValid { + t.Fail() + } +} + +func TestIsISBN10InvalidLength(t *testing.T) { + result := checker.IsISBN10("143024827") + if result != checker.ResultNotISBN { + t.Fail() + } +} + +func TestIsISBN10InvalidCheck(t *testing.T) { + result := checker.IsISBN10("1430248272") + if result != checker.ResultNotISBN { + t.Fail() + } +} + +func TestIsISBN13Valid(t *testing.T) { + result := checker.IsISBN13("9781430248279") + if result != checker.ResultValid { + t.Fail() + } +} + +func TestIsISBN13ValidWithDashes(t *testing.T) { + result := checker.IsISBN13("978-1-4302-4827-9") + if result != checker.ResultValid { + t.Fail() + } +} + +func TestIsISBN13InvalidLength(t *testing.T) { + result := checker.IsISBN13("978143024827") + if result != checker.ResultNotISBN { + t.Fail() + } +} + +func TestIsISBN13InvalidCheck(t *testing.T) { + result := checker.IsISBN13("9781430248272") + if result != checker.ResultNotISBN { + t.Fail() + } +} + +func TestIsISBNValid10(t *testing.T) { + result := checker.IsISBN("1430248270") + if result != checker.ResultValid { + t.Fail() + } +} + +func TestIsISBNValid13(t *testing.T) { + result := checker.IsISBN("9781430248279") + if result != checker.ResultValid { + t.Fail() + } +} + +func TestIsISBNInvalidLenght(t *testing.T) { + result := checker.IsISBN("978143024827") + if result != checker.ResultNotISBN { + t.Fail() + } +} + +func TestCheckISBNNonString(t *testing.T) { + defer checker.FailIfNoPanic(t) + + type Book struct { + ISBN int `checkers:"isbn"` + } + + book := &Book{} + + checker.Check(book) +} + +func TestCheckISBNValid(t *testing.T) { + type Book struct { + ISBN string `checkers:"isbn"` + } + + book := &Book{ + ISBN: "1430248270", + } + + _, valid := checker.Check(book) + if !valid { + t.Fail() + } +} + +func TestCheckISBNInvalid(t *testing.T) { + defer checker.FailIfNoPanic(t) + + type Book struct { + ISBN string `checkers:"isbn:20"` + } + + book := &Book{ + ISBN: "1430248270", + } + + checker.Check(book) +} + +func TestCheckISBNValid10(t *testing.T) { + type Book struct { + ISBN string `checkers:"isbn:10"` + } + + book := &Book{ + ISBN: "1430248270", + } + + _, valid := checker.Check(book) + if !valid { + t.Fail() + } +} + +func TestCheckISBNValid13(t *testing.T) { + type Book struct { + ISBN string `checkers:"isbn:13"` + } + + book := &Book{ + ISBN: "9781430248279", + } + + _, valid := checker.Check(book) + if !valid { + t.Fail() + } +}