;;; isbn-verify --- verify ISBN-10 and ISBN-13 -*- lexical-binding: t ;; ;;; Commentary: ;; ;; This Elisp package provides functions to verify ;; ISBNs by computing their check digits. ;; ;; Author: Emanuel Berg (incal) ;; Created: 2020-05-20 ;; Keywords: bib, tex ;; License: GPL3+ ;; Package-Requires: ((cl-lib "1.0")) ;; URL: https://dataswamp.org/~incal/emacs-init/isbn-verify.el ;; Version: 1.0.0 ;; ;; Also see: ;; ;; https://dataswamp.org/~incal/emacs-init/issn-verify.el ;; ;; For more info on ISBNs and how the check digits ;; are computed, see: ;; ;; https://dataswamp.org/~incal/books/isbn.txt ;; ;; To install this package, with this file in ;; a buffer, do: ;; ;; M-x load-file RET RET ;; ;; To try it, do `isbn-verify-at-point' on the below ;; two Biblatex entries, the first one with ISBN-10, ;; the second with ISBN-13. Position point at the ;; beginning of the ISBN, then do ;; ;; M-x viap RET ;; ;; @book{russia-and-the-arms-trade, ;; author = {Ian Anthony}, ;; isbn = {0-19-829278-3}, ;; publisher = {Oxford}, ;; title = {Russia and the Arms Trade}, ;; year = 1998 ;; } ;; ;; @book{baa-lo-4, ;; author = {Yukito Kishiro}, ;; isbn = {978-1-61262-294-1}, ;; publisher = {Kodansha}, ;; title = {Battle Angel Alita: The Last Order 4}, ;; year = {2014 (2011)} ;; } ;; ;; Or with Lisp, evaluate these forms: ;; ;; (isbn-checksum-10 "0-19-829576-6") ; 6 ;; (isbn-checksum-13 "978-1-61262-294-1") ; 1 ;; ;;; Code: (require 'cl-lib) (defun isbn--digits-only-string (str) "Remove all non-digits from STR." (replace-regexp-in-string "[^0-9]" "" str) ) (defun isbn--char-to-int (c) "Convert char C into the digit it displays, e.g. ?9 into 9." (- c ?0) ) (defun isbn--string-to-integer-list (str) "Make an integer list from STR with all non-digits removed." (let*((digits-only (isbn--digits-only-string str)) (chars (string-to-list digits-only)) (ints (cl-map 'list (lambda (c) (isbn--char-to-int c)) chars) )) ints) ) (defun isbn--convert-to-digit (digit) "If DIGIT is a number between 0 and 9, return it. If it is a string containing a single digit, return it. If it is \"X\", return 10." (cond ((and (numberp digit) (<= 0 digit) (<= digit 9)) digit) ((string= digit "X") 10) ((and (string-match-p "[0-9]" digit) (= 1 (length digit)) ) (string-to-number digit)) (t (error "Incorrect digit: %s" digit)) )) ;; (isbn--convert-to-digit 0 ) ; 0 ;; (isbn--convert-to-digit 1 ) ; 1 ;; (isbn--convert-to-digit "9") ; 9 ;; (isbn--convert-to-digit "X") ; 10 ;; (isbn--convert-to-digit "100") ; Incorrect digit: 100 ;; (isbn--convert-to-digit "e") ; Incorrect digit: e (defun isbn-verify (isbn) "Verify ISBN." (let*((digits-str (isbn--digits-only-string isbn)) (num-digits (length digits-str)) (type (pcase num-digits ((or 9 10) 10) ((or 12 13) 13) ( _ (error "An ISBN has 10 or 13 digits")) )) (last-digit-str (substring isbn -1)) (last-digit-int (isbn--convert-to-digit last-digit-str) ) (check-digit (if (= type 10) (isbn-checksum-10 digits-str) (isbn-checksum-13 digits-str) )) (check-digit-int (isbn--convert-to-digit check-digit)) (pass (= last-digit-int check-digit-int)) ) (if pass (message "(ISBN-%s OK) %s" type check-digit) (error (format "Incorrect ISBN-%s. Correct check digit is %s" type check-digit) )))) (defun isbn-verify-at-point () "Compute and display the check digit for the ISBN at point. \nFor Lisp use, see `isbn-verify'." (interactive) (let((isbn-string (thing-at-point 'symbol t))) (isbn-verify isbn-string) )) (defalias 'viap #'isbn-verify-at-point) (defun isbn-checksum-13 (isbn) "Compute the checksum for ISBN which should be a ISBN-13, i.e. consist of 13 digits. Because the last digit, number 13, is the checksum, actually only 12 digits, in the form of a string, is required. \nHyphens/dashes and whitespace can be included or omitted. \nFor ISBN-10, see `isbn-checksum-10'. \nFor interactive use, do \\[isbn-verify-at-point]." (let*((isbn-ints (isbn--string-to-integer-list isbn)) (sum 0)) (cl-loop for e in isbn-ints for i from 0 to 11 do (cl-incf sum (* e (or (and (zerop (mod i 2)) 1) 3))) ) (let ((checksum (- 10 (mod sum 10)))) (if (= 10 checksum) 0 checksum) ))) (defun isbn-checksum-10 (isbn) "Compute the checksum for ISBN which should be a ISBN-10, i.e. consist of 10 digits. Because the last digit, number 10, is the checksum, actually only 9 digits, in the form of a string, is required. \nIf the check digit is computed to 10, return an X. \nFor ISBN-13, see `isbn-checksum-13'. \nFor interactive use, do \\[isbn-verify-at-point]." (let*((isbn-ints (isbn--string-to-integer-list isbn)) (sum 0) ) (cl-loop for e in isbn-ints for i downfrom 10 to 2 do (cl-incf sum (* e i)) ) (let ((checksum (mod (- 11 (mod sum 11)) 11))) (if (= 10 checksum) "X" checksum) ))) ;; check for errors: ;; ;; (isbn-verify "0-201-53992-5") ; Incorrect ISBN-10. Correct check digit is 6 ;; (isbn-verify "91-518-4657-7") ; Incorrect ISBN-10. Correct check digit is 8 ;; (isbn-verify "1-4012-0622-X") ; Incorrect ISBN-10. Correct check digit is 0 ;; (isbn-verify "91-85668-01-9") ; Incorrect ISBN-10. Correct check digit is 0 ;; (isbn-verify "978-0-470-56157-0") ; Incorrect ISBN-13. Correct check digit is 7 ;; (isbn-verify "978-91-87861-9") ; An ISBN has 10 or 13 digits ;; (isbn-verify "1-4012-062") ; An ISBN has 10 or 13 digits ;; ;; 10 ISBN-10 tests: ;; ;; (isbn-checksum-10 "0-201-53992-6") ; 6 ;; (isbn-checksum-10 "0312168144" ) ; 4 ;; (isbn-checksum-10 "1-4012-0622-0") ; 0 ;; (isbn-checksum-10 "1616558717" ) ; 7 ;; (isbn-checksum-10 "91 7054 940 0") ; 0 ;; (isbn-checksum-10 "91-510-6483-9") ; 9 ;; (isbn-checksum-10 "91-7089-710-7") ; 7 ;; (isbn-checksum-10 "91-85668-01-X") ; X ;; (isbn-checksum-10 "91-88930-23-8") ; 8 ;; (isbn-checksum-10 "9177988515" ) ; 5 ;; ;; 13 ISBN-13 tests: ;; ;; (isbn-checksum-13 "978 91 29 59023 4") ; 4 ;; (isbn-checksum-13 "978-0-470-56157-7") ; 7 ;; (isbn-checksum-13 "978-1-63236-616-0") ; 0 ;; (isbn-checksum-13 "978-91-0-012814-2") ; 2 ;; (isbn-checksum-13 "978-91-7037-681-8") ; 8 ;; (isbn-checksum-13 "978-91-7515-205-9") ; 9 ;; (isbn-checksum-13 "978-91-7515-317-9") ; 9 ;; (isbn-checksum-13 "978-91-86936-31-0") ; 0 ;; (isbn-checksum-13 "978-91-87861-54-3") ; 3 ;; (isbn-checksum-13 "978-91-87861-67-3") ; 3 ;; (isbn-checksum-13 "978-91-87861-99-4") ; 4 ;; (isbn-checksum-13 "9780062802187" ) ; 7 ;; (isbn-checksum-13 "9789188805034" ) ; 4 (provide 'isbn-verify) ;;; isbn-verify.el ends here