diff options
-rw-r--r-- | skate/isbn/isbn.go | 155 | ||||
-rw-r--r-- | skate/isbn/isbn_test.go | 90 |
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) + } + } +} |