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!