aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--skate/isbn/isbn.go155
-rw-r--r--skate/isbn/isbn_test.go90
2 files changed, 245 insertions, 0 deletions
diff --git a/skate/isbn/isbn.go b/skate/isbn/isbn.go
new file mode 100644
index 0000000..c50eaaa
--- /dev/null
+++ b/skate/isbn/isbn.go
@@ -0,0 +1,155 @@
+// Copyright 2015 Rodrigo Moraes. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+/*
+Package isbn provides functions to validate ISBN strings, calculate ISBN
+check digits and convert ISBN-10 to ISBN-13.
+*/
+package isbn
+
+import (
+ "fmt"
+ "strconv"
+)
+
+// sum10 returns the weighted sum of the provided ISBN-10 string. It is used
+// to calculate the ISBN-10 check digit or to validate an ISBN-10.
+//
+// The provided string must have a length of 9 or 10 and no formatting
+// characters (spaces or hyphens).
+func sum10(isbn string) (int, error) {
+ s := 0
+ w := 10
+ for k, v := range isbn {
+ if k == 9 && v == 88 {
+ // Handle "X" as the digit.
+ s += 10
+ } else {
+ n, err := strconv.Atoi(string(v))
+ if err != nil {
+ return -1, fmt.Errorf("Failed to convert ISBN-10 character to int: %s", string(v))
+ }
+ s += n * w
+ }
+ w--
+ }
+ return s, nil
+}
+
+// sum13 returns the weighted sum of the provided ISBN-13 string. It is used
+// to calculate the ISBN-13 check digit or to validate an ISBN-13.
+//
+// The provided string must have a length of 12 or 13 and no formatting
+// characters (spaces or hyphens).
+func sum13(isbn string) (int, error) {
+ s := 0
+ w := 1
+ for _, v := range isbn {
+ n, err := strconv.Atoi(string(v))
+ if err != nil {
+ return -1, fmt.Errorf("Failed to convert ISBN-13 character to int: %s", string(v))
+ }
+ s += n * w
+ if w == 1 {
+ w = 3
+ } else {
+ w = 1
+ }
+ }
+ return s, nil
+}
+
+// CheckDigit10 returns the check digit for an ISBN-10.
+//
+// The provided string must have a length of 9 or 10 and no formatting
+// characters (spaces or hyphens). For a 10-length string, the last character
+// (the digit) is ignored since that is what is being (re)calculated.
+func CheckDigit10(isbn10 string) (string, error) {
+ if len(isbn10) != 9 && len(isbn10) != 10 {
+ return "", fmt.Errorf("A string of length 9 or 10 is required to calculate the ISBN-10 check digit. Provided was: %s", isbn10)
+ }
+ s, err := sum10(isbn10[:9])
+ if err != nil {
+ return "", err
+ }
+ d := (11 - (s % 11)) % 11
+ if d == 10 {
+ return "X", nil
+ }
+ return strconv.Itoa(d), nil
+}
+
+// CheckDigit13 returns the check digit for an ISBN-13.
+//
+// The provided string must have a length of 12 or 13 and no formatting
+// characters (spaces or hyphens). For a 13-length string, the last character
+// (the digit) is ignored since that is what is being (re)calculated.
+func CheckDigit13(isbn13 string) (string, error) {
+ if len(isbn13) != 12 && len(isbn13) != 13 {
+ return "", fmt.Errorf("A string of length 12 or 13 is required to calculate the ISBN-13 check digit. Provided was: %s", isbn13)
+ }
+ s, err := sum13(isbn13[:12])
+ if err != nil {
+ return "", err
+ }
+ d := 10 - (s % 10)
+ if d == 10 {
+ return "0", nil
+ }
+ return strconv.Itoa(d), nil
+}
+
+// Validate returns true if the provided string is a valid ISBN-10 or ISBN-13.
+//
+// The provided string must have a length of 10 or 13 and no formatting
+// characters (spaces or hyphens).
+func Validate(isbn string) bool {
+ switch len(isbn) {
+ case 10:
+ return Validate10(isbn)
+ case 13:
+ return Validate13(isbn)
+ }
+ return false
+}
+
+// Validate10 returns true if the provided string is a valid ISBN-10.
+//
+// The provided string must have a length of 10 and no formatting
+// characters (spaces or hyphens).
+func Validate10(isbn10 string) bool {
+ if len(isbn10) == 10 {
+ s, _ := sum10(isbn10)
+ return s%11 == 0
+ }
+ return false
+}
+
+// Validate13 returns true if the provided string is a valid ISBN-13.
+//
+// The provided string must have a length of 13 and no formatting
+// characters (spaces or hyphens).
+func Validate13(isbn13 string) bool {
+ if len(isbn13) == 13 {
+ s, _ := sum13(isbn13)
+ return s%10 == 0
+ }
+ return false
+}
+
+// To13 converts an ISBN-10 to an ISBN-13.
+//
+// The provided string must have a length of 9 or 10 and no formatting
+// characters (spaces or hyphens).
+func To13(isbn10 string) (string, error) {
+ if len(isbn10) != 9 && len(isbn10) != 10 {
+ return "", fmt.Errorf("A string of length 9 or 10 is required to convert an ISBN-10 to an ISBN-13. Provided was: %s", isbn10)
+ }
+ isbn13 := "978" + isbn10[:9]
+ d, err := CheckDigit13(isbn13)
+ if err != nil {
+ return "", err
+ }
+ return isbn13 + d, nil
+}
diff --git a/skate/isbn/isbn_test.go b/skate/isbn/isbn_test.go
new file mode 100644
index 0000000..cfd905c
--- /dev/null
+++ b/skate/isbn/isbn_test.go
@@ -0,0 +1,90 @@
+// Copyright 2015 Rodrigo Moraes. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package isbn
+
+import (
+ "testing"
+)
+
+type book struct {
+ isbn10 string
+ isbn13 string
+ valid bool
+}
+
+var books = []book{
+ // Calvin and Hobbes, 1987
+ {"0836220889", "9780836220889", true},
+ // Something Under the Bed Is Drooling, 1988
+ {"0836218256", "9780836218251", true},
+ // Yukon Ho!, 1989
+ {"0836218353", "9780836218350", true},
+ // Weirdos from Another Planet!, 1990
+ {"1449407102", "9781449407100", true},
+ // Scientific Progress Goes 'Boink', 1991
+ {"0836218787", "9780836218787", true},
+ // Attack of the Deranged Mutant Killer Monster Snow Goons, 1992
+ {"0836218833", "9780836218831", true},
+ // The Days are Just Packed, 1993
+ {"0836217357", "9780836217353", true},
+ // Invalid: too many characters
+ {"08362208891", "97808362208891", false},
+ {"08362182562", "97808362182512", false},
+ {"08362183533", "97808362183503", false},
+ {"08362186204", "97804391374924", false},
+ {"08362187875", "97808362187875", false},
+ {"08362188336", "97808362188316", false},
+ {"08362173577", "97808362173537", false},
+ // Invalid: too few characters
+ {"083622088", "978083622088", false},
+ {"083621825", "978083621825", false},
+ {"083621835", "978083621835", false},
+ {"083621862", "978043913749", false},
+ {"083621878", "978083621878", false},
+ {"083621883", "978083621883", false},
+ {"083621735", "978083621735", false},
+ // Invalid: bad check digit
+ {"0836220888", "9780836220880", false},
+ {"0836218255", "9780836218252", false},
+ {"0836218352", "9780836218351", false},
+ {"0836218629", "9780439137493", false},
+ {"0836218786", "9780836218788", false},
+ {"0836218832", "9780836218832", false},
+ {"0836217356", "9780836217354", false},
+}
+
+func TestISBN(t *testing.T) {
+ for _, v := range books {
+ shouldbe, shouldnotbe := "valid", "invalid"
+ if v.valid {
+ d10, err := CheckDigit10(v.isbn10)
+ if err != nil || d10 != v.isbn10[len(v.isbn10)-1:] {
+ t.Errorf("CheckDigit10: failed to calculate check digit for %s: got %s, expected %s (error: %v)", v.isbn10, d10, v.isbn10[len(v.isbn10)-1:], err)
+ }
+ d13, err := CheckDigit13(v.isbn13)
+ if err != nil || d13 != v.isbn13[len(v.isbn13)-1:] {
+ t.Errorf("CheckDigit13: failed to calculate check digit for %s: got %s, expected %s (error: %v)", v.isbn13, d13, v.isbn13[len(v.isbn13)-1:], err)
+ }
+ to13, err := To13(v.isbn10)
+ if err != nil || to13 != v.isbn13 {
+ t.Errorf("To13: failed to convert %s from ISBN-10 to ISBN-13: got %s, expected %s (error: %v)", v.isbn10, to13, v.isbn13, err)
+ }
+ } else {
+ shouldbe, shouldnotbe = "invalid", "valid"
+ }
+ if Validate(v.isbn10) != v.valid {
+ t.Errorf("Validate: %s should be %s, got %s", v.isbn10, shouldbe, shouldnotbe)
+ }
+ if Validate(v.isbn13) != v.valid {
+ t.Errorf("Validate: %s should be %s, got %s", v.isbn13, shouldbe, shouldnotbe)
+ }
+ if Validate10(v.isbn10) != v.valid {
+ t.Errorf("Validate10: %s should be %s, got %s", v.isbn10, shouldbe, shouldnotbe)
+ }
+ if Validate13(v.isbn13) != v.valid {
+ t.Errorf("Validate13: %s should be %s, got %s", v.isbn13, shouldbe, shouldnotbe)
+ }
+ }
+}