Thursday, January 24, 2008

ISBN check API

A smart young programmer from a book-related company and I were talking. It turns out that, to validate ISBNs and get back both 10- and 13-digit versions he was submitting ISBNs to Amazon Web Services. That's like calling NORAD to find out if it's raining.* Nor did he seem likely to hunt around for an ISBN library for Ruby. After all, what he was doing worked.

So I made a quick, very stupid API, ie. http://www.librarything.com/isbncheck.php?isbn=0765344629
  • Give it any old ISBN and it does the math to return the ISBN10 and ISBN13 forms, if both exist.
  • It removes dashes and other junk.
  • It transparently fixes missing initial zeroes. This is a common problem with data from Excel files, which turn 0765344629 into 765344629.
  • If the ISBN isn't valid and can't be easily fixed, it returns an error.
Don't hit it more than 10 times/second. Otherwise, there are no usage restrictions.

*Amazon take note—I got your back, buddy!

Labels: ,

8 Comments:

Blogger GreyHead said...

Very nice. Have you thought about the recursive application to LibraryThing - some of us currently use a FireFox extension LibraryThing Plus to check the ISBNs in our LibraryThing libraries?

1/24/2008 3:47 AM  
OpenID six-crazy-guys said...

Great. Now the only thing we need is a version that adds the dashes.

*likes his ISBN dashed*

1/24/2008 2:06 PM  
Blogger Tim said...

Ha. I think that requires a list of publishers, which is fiddly.

1/24/2008 2:11 PM  
Blogger Keith Fahlgren said...

Via O'Reilly's code (why doesn't blogger allow <pre> ?!):

module Isbn
def self.dash_isbn(isbn)
raise ArgumentError.new("ISBN argument must be string") unless isbn.is_a?(String)
if isbn.length == 10
return isbn[/^./] + "-" + isbn[1..3] + "-" + isbn[4..8] + "-" + isbn[/.$/]
elsif isbn.length == 13
return isbn[0..2] + "-" + isbn[3].chr + "-" + isbn[4..6] + "-" + isbn[7..11] + "-" + isbn[/.$/]
else
raise ArgumentError.new("ISBN must be 10 or 13 characters")
end
end

def self.isbn10toisbn13(isbn)
raise ArgumentError.new("ISBN argument must be string") unless isbn.is_a?(String)
raise ArgumentError.new("ISBN must be of length 10") unless isbn.length == 10
prefix = "978"
isbn12 = prefix + isbn[0...-1]
return isbn12 + check_digit_13(isbn12).to_s
end


def self.check_digit_13(isbn_12)
# http://www.barcodeisland.com/ean13.phtml
# need to subtract remainder from 10
# and do exemption for zero LMS 08.29.2006
raise ArgumentError.new("ISBN must be of length 12") unless isbn_12.length == 12
sum = 0
odds = 0
evens = 0
isbn_12.scan(/\d/).each_with_index {|d, i|
if (i % 2) == 0
evens = evens + (d.to_i * 1)
else
odds = odds + (d.to_i * 3)
end
}
sum = evens + odds
digit = sum % 10
if digit.zero?
return 0
else
return 10 - digit
end
end
end # of module Isbns

Following calls our database, which knows what's up. Replace PDB call with something you trust.

#!/usr/bin/env ruby

require 'test/unit'
require 'pdb'
require 'isbn'

class IsbnTest < Test::Unit::TestCase
def setup
@isbn10 = "059610123X"
@isbn13 = "978059610123X"
end
def test_dash_isbn
# must be a String
assert_raise ArgumentError do Isbn.dash_isbn(123) end
# must be 10 or 13 characters
assert_raise ArgumentError do Isbn.dash_isbn("123") end
assert_equal("0-596-10123-X", Isbn.dash_isbn(@isbn10))
assert_equal("978-0-596-10123-X", Isbn.dash_isbn(@isbn13))
end
def test_isbn10toisbn13
# must be a String
assert_raise ArgumentError do Isbn.isbn10toisbn13(123) end
# must be 10 characters
assert_raise ArgumentError do Isbn.isbn10toisbn13(@isbn13) end
assert_equal("9780596101237", Isbn.isbn10toisbn13(@isbn10))
end
def test_check_digit_13
# must be 12 characters
assert_raise ArgumentError do Isbn.check_digit_13(@isbn13) end
assert_equal(7, Isbn.check_digit_13(@isbn13[0..-2]))
assert_equal(0, Isbn.check_digit_13("123456789018"))
end
def test_isbn13
["0596008627", "1565929470", "0596002734", "0596004001", "0596527357",
"0596101635", "0596005059", "0596527063", "1565926374", "0596526946",
"1565926420", "0596101805", "059652742X", "156592455X", "0596006446",
"0596008473", "0596009607", "0596100582", "0596100493", "0596004427",
"1565925890", "1565927141", "059651610X"].each {|isbn|
puts "Testing #{isbn}"
assert_equal(PDB::ProdDB.new(isbn).isbn13, Isbn.isbn10toisbn13(isbn), "Bad checksum!")
}
end
end

1/24/2008 5:31 PM  
Blogger Keith Fahlgren said...

Again, more legibly

1/24/2008 5:42 PM  
Blogger Tim said...

Very nice. Very nice.

1/24/2008 6:07 PM  
OpenID six-crazy-guys said...

Actually, the list of ranges is the only thing you need (though I admit it is likely mildly fiddly).

1/24/2008 10:38 PM  
Anonymous Anonymous said...

http://pcn.loc.gov/isbncnvt.html

It even hyphenates them if you check the little box.

1/28/2008 1:54 PM  

Post a Comment

<< Home