Re: Factor

Factor: the language, the theory, and the practice.

ISBN

Saturday, September 19, 2015

#math #text

Update: Thanks to Javi Mari Lobato for pointing out that ISBN-10 check digits can be X, representing 10. The code below now handles this case, which you can try using Javi’s barcode validation tool.

Most books are issued a unique International Standard Book Number (ISBN) number. Often different formats of the same book will have different ISBN numbers. On a print book, you can usually find the ISBN on a barcode on the back cover.

Most countries seem to have a national ISBN registration agency. In some countries this is a free service provided by a government agency. In other countries, this is operated by a commercial entity. In the United States, one company (R.R. Bowker LLC) has an apparent monopoly on issuing ISBN numbers which can cost $125 for one (less if you buy in bulk).

The ISBN is 13 digits long if assigned starting in 2007, and 10 digits long if assigned before 2007. Each ISBN contains a check digit which is used for basic error detection. We are going to build a few words in Factor to calculate the check digits and validate ISBNs.

We need to turn an ISBN (which might include spaces or dashes) into numeric digits. An ISBN-10 can end in an X check character, which represents 10, so we preserve it and map it to that value:

: digits ( str -- digits )
    [ dup digit? swap CHAR: X = or ] filter
    [ dup CHAR: X = [ drop 10 ] [ digit> ] if ] map ;

For ISBN-10, we sum each of the 10 values multiplied by a weight (descending from 10 to 1), then take the result modulo 11. A valid ISBN-10 has a remainder of zero.

: isbn-10-check ( digits -- n )
    0 [ 10 swap - * + ] reduce-index 11 mod ;

For ISBN-13, we sum each of the 13 digits multiplied by a weight (alternating between 1 and 3), then take the result modulo 10. Here too, a valid ISBN has a remainder of zero.

: isbn-13-check ( digits -- n )
    0 [ even? 1 3 ? * + ] reduce-index 10 mod ;

We can validate an ISBN by grabbing the digits and running either the ISBN-10 or ISBN-13 check and verifying that the result is zero. We also check that X appears only in the final position of an ISBN-10, and return false for unsupported lengths.

: valid-isbn? ( str -- ? )
    digits dup length {
        { 10 [
            dup 9 head [ 10 < ] all?
            [ isbn-10-check 0 = ] [ drop f ] if
        ] }
        { 13 [
            dup [ 10 < ] all?
            [ isbn-13-check 0 = ] [ drop f ] if
        ] }
        [ 2drop f ]
    } case ;

The code (and some tests) for this is on my GitHub.