As I’ve been progressing through the exercise of parsing ISBN numbers, I discovered that the ISBN-10 numbers that I’ve been parsing were phased out in 2007! I guess that too many people were writing books around the world and they were running out of numbers. To deal with the problem, the ISBN people created something called a ISBN-13 code that is… (wait for it) … 13 digits long instead of the 10 digits in the previous standard.
After learning this shocking information, I decided that it wouldn’t be proper to have an ISBN validator that was almost 15 years out of date, so I added support for ISBN-13 validation to my practice example.
It turns out that ISBN-13 codes are validated differently than ISBN-10 codes. The ISBN-13 standard is outlined here. And, luckily, they’ve provided a nice worked example of how to generate check digits. Luckily, the mechanism is pretty straightforward and uses smaller numbers than the ISBN-10 mechanism. Essentially, this is how the mechanism works: each even digit (remember to start counting at zero!) is multiplied by 1 and each odd digit is multiplied by 3, and then all of the digits are summed.
- A sum of the first 12 digits is generated by multiplying all even digits by 1 and all odd digits by 3 (and then summing them)
- The 13th digit (or check digit) is generated by determining how much needs to be added to the sum generated in Step #1 to make it a multiple of 10.
- For example: If the output of Step #1 is 146, the 13th digit would be 4 because 146 + 4 = 150 (which is a multiple of 10)
- The folks at HowToGeek have written a nice explanation of what checksums are and why they are helpful in computing if you’re interested.
- Then, all of the digits are combined and THAT is the ISBN-13.
The nice thing about this approach is that to verify the ISBN, all that you need to do is generate the sum as outlined above (even digits *= 1, odd *= 3) for all 13 digits and then check to see if this is evenly divisible by 10. If it is, the ISBN is valid!
So, I extended the unit test class to include some ISBN-13 numbers and added a test method to verify my solution:
class TestISBNValidator(TestCase):
...
test_data_isbn13 = {
"123": False,
# A valid isbn10 should not work
"0136091814": False,
"978-1-86197-876-9": True,
"978-1-56619-909-4": True,
"9781566199094": True,
"9781566199092": False,
"978156619909X": False,
# Leading or trailing spaces should work
" 9781566199094": True,
" 9781566199094 ": True
}
...
def test_validate_isbn13(self):
for (code_string, valid) in TestISBNValidator.test_data_isbn13.items():
result = ISBNValidator.validate_isbn13(code_string=code_string)
self.assertEqual(result, valid)
And then I wrote the validate_isbn13() method. I also upgraded the remove_dashes method that I used to have and added the capability to not only remove dashes, but also trim spaces. I renamed the method to prepare_code_string() since it wasn’t just removing dashes anymore. Once I finished these two steps, this is what my ISBN Validator class is looking like now:
class ISBNValidator:
@staticmethod
def prepare_code_string(code_string: str) -> str:
retval = code_string.strip()
retval = retval.replace("-", "")
return retval
@staticmethod
def validate_isbn10(code_string: str) -> bool:
# Remove dashes from the string
isbn_string = ISBNValidator.prepare_code_string(code_string)
# Reject if string is wrong length, or is not numeric (including special case for digit 10)
if len(isbn_string) != 10 or \
not isbn_string[0:9].isnumeric() or \
not (isbn_string[9].isnumeric() or isbn_string[9].lower() == "x"):
return False
checksum = 0
for i in range(0, 10):
if i == 9 and isbn_string[i].lower() == "x":
digit = 10
else:
digit = int(isbn_string[i])
checksum += digit * (10 - i)
return (checksum % 11) == 0
@staticmethod
def validate_isbn13(code_string: str) -> bool:
isbn_string = ISBNValidator.prepare_code_string(code_string)
if len(isbn_string) != 13 or not isbn_string.isnumeric():
return False
checksum = 0
for (count, digit) in enumerate(isbn_string):
# Weight of 1 for even and 3 for odd digits
weight = 1 + ((count % 2) * 2)
checksum += (int(digit) * weight)
return (checksum % 10) == 0
Now, when I run my unit tests, here is what I get:
.. ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK
All seems good, and now we have support for both ISBN-10 and ISBN-13 validation!
