In my previous post Practicing Python, I started work on this exercise at Codementor. I got a decent solution working, but it still had a few deficiencies. Namely:
- It did not account for dashes in the ISBN
- Some of the error handling was not complete (namely making sure that the last digit was either numeric or an “x” character)
- The test code was also incomplete (did not test the last digit as “x” or a ISBN with dashes)
Because of this, I decided to spend a little bit of my practice time enhancing the code. I also combined all of my methods/functions into a class because in the future, I’d like to maybe build an application that can display and run my practice exercises. I considered splitting things into modules, but I preferred the cleaner file structure of a class as well as the opportunity to use some polymorphism later on to “process” these exercises.
I like to do test driven development, so the first thing I did was to compose and add some tests that would exercise these scenarios:
isbn = "155404295X"
assert ISBNExercise.validate_isbn10(isbn) is True
isbn = "1-55404-295-X"
assert ISBNExercise.validate_isbn10(isbn) is True
isbn = "1-55404-294-X"
assert ISBNExercise.validate_isbn10(isbn) is False
The next thing I did was create a method to remove the dashes from the ISBN string. This is a nice “one liner” in Python using the str.replace method:
def remove_dashes(code_string: str) -> str:
return code_string.replace("-", "")
Then, I updated my main method to use this new remove_dashes helper method as well as adding in the additional error handling:
def validate_isbn10(code_string: str) -> bool:
# Remove dashes from the string
isbn_string = ISBNExercise.remove_dashes(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
Now that I finished this, all of my tests are passing and I feel like my ISBN processor is much more robust. I think that tomorrow I will convert the test function into something that uses the Python unit test infrastructure instead.
If you’re interested, here is my complete class as it exists now:
class ISBNExercise:
@staticmethod
def remove_dashes(code_string: str) -> str:
return code_string.replace("-", "")
@staticmethod
def validate_isbn10(code_string: str) -> bool:
# Remove dashes from the string
isbn_string = ISBNExercise.remove_dashes(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 test_isbns():
isbn = "123"
assert ISBNExercise.validate_isbn10(isbn) is False
isbn = "0136091814"
assert ISBNExercise.validate_isbn10(isbn) is True
isbn = "1616550416"
assert ISBNExercise.validate_isbn10(isbn) is False
isbn = "0553418025"
assert ISBNExercise.validate_isbn10(isbn) is True
isbn = "3859574859"
assert ISBNExercise.validate_isbn10(isbn) is False
isbn = "155404295X"
assert ISBNExercise.validate_isbn10(isbn) is True
isbn = "1-55404-295-X"
assert ISBNExercise.validate_isbn10(isbn) is True
isbn = "1-55404-294-X"
assert ISBNExercise.validate_isbn10(isbn) is False
def run_exercise(self):
self.test_isbns()
print("ISBN Exercise - Complete!")
if __name__ == '__main__':
ex = ISBNExercise()
ex.run_exercise()
