ISBN13 check digit: Difference between revisions
Langurmonkey (talk | contribs) |
Langurmonkey (talk | contribs) |
||
Line 537: | Line 537: | ||
<lang langur>val .isbn13checkdigit = f(var .s) { |
<lang langur>val .isbn13checkdigit = f(var .s) { |
||
.s = replace(.s, RE/[\- ]/) |
.s = replace(.s, RE/[\- ]/) |
||
var .alt = |
var .alt = false |
||
matching(re/^[0-9]{13}$/, .s) and |
matching(re/^[0-9]{13}$/, .s) and |
||
for[=0] .d in s2n(.s) { |
for[=0] .d in s2n(.s) { _for += if(.alt: .d x 3; .d); not= .alt } div 10 |
||
} |
} |
||
Revision as of 18:03, 12 April 2020
- Task
Validate the check digit of an ISBN-13 code. Multiply every other digit by 3. Add the digits together. Take the remainder of division by 10. If it is 0, the ISBN-13 check digit is correct.
Use the following codes for testing:
978-1734314502: good 978-1734314509: bad 978-1788399081: good 978-1788399083: bad
Show output here, on this page
- See also
See https://isbn-information.com/the-13-digit-isbn.html for details on the method of validation.
AppleScript
Composition of pure functions
<lang applescript>-- isISBN13 :: String -> Bool on isISBN13(s)
script digitValue on |λ|(c) if isDigit(c) then {c as integer} else {} end if end |λ| end script set digits to concatMap(digitValue, characters of s) 13 = length of digits ¬ and 0 = sum(zipWith(my mul, digits, cycle({1, 3}))) mod 10
end isISBN13
TEST----------------------------
on run
script test on |λ|(s) {s, isISBN13(s)} end |λ| end script map(test, {"978-1734314502", "978-1734314509", ¬ "978-1788399081", "978-1788399083"})
end run
GENERIC FUNCTIONS---------------------
-- concatMap :: (a -> [b]) -> [a] -> [b] on concatMap(f, xs)
set lng to length of xs set acc to {} tell mReturn(f) repeat with i from 1 to lng set acc to acc & (|λ|(item i of xs, i, xs)) end repeat end tell return acc
end concatMap
-- cycle :: [a] -> Generator [a] on cycle(xs)
script property lng : 1 + (length of xs) property i : missing value on |λ|() if missing value is i then set i to 1 else set nxt to (1 + i) mod lng if 0 = ((1 + i) mod lng) then set i to 1 else set i to nxt end if end if return item i of xs end |λ| end script
end cycle
-- foldl :: (a -> b -> a) -> a -> [b] -> a on foldl(f, startValue, xs)
tell mReturn(f) set v to startValue set lng to length of xs repeat with i from 1 to lng set v to |λ|(v, item i of xs, i, xs) end repeat return v end tell
end foldl
-- isDigit :: Char -> Bool on isDigit(c)
set n to (id of c) 48 ≤ n and 57 ≥ n
end isDigit
-- length :: [a] -> Int on |length|(xs)
set c to class of xs if list is c or string is c then length of xs else (2 ^ 29 - 1) -- (maxInt - simple proxy for non-finite) end if
end |length|
-- map :: (a -> b) -> [a] -> [b] on map(f, xs)
-- The list obtained by applying f -- to each element of xs. tell mReturn(f) set lng to length of xs set lst to {} repeat with i from 1 to lng set end of lst to |λ|(item i of xs, i, xs) end repeat return lst end tell
end map
-- min :: Ord a => a -> a -> a on min(x, y)
if y < x then y else x end if
end min
-- mReturn :: First-class m => (a -> b) -> m (a -> b) on mReturn(f)
-- 2nd class handler function lifted into 1st class script wrapper. if script is class of f then f else script property |λ| : f end script end if
end mReturn
-- mul (*) :: Num a => a -> a -> a on mul(a, b)
a * b
end mul
-- sum :: [Num] -> Num on sum(xs)
script add on |λ|(a, b) a + b end |λ| end script foldl(add, 0, xs)
end sum
-- take :: Int -> [a] -> [a] -- take :: Int -> String -> String on take(n, xs)
set c to class of xs if list is c then if 0 < n then items 1 thru min(n, length of xs) of xs else {} end if else if string is c then if 0 < n then text 1 thru min(n, length of xs) of xs else "" end if else if script is c then set ys to {} repeat with i from 1 to n set v to |λ|() of xs if missing value is v then return ys else set end of ys to v end if end repeat return ys else missing value end if
end take
-- zipWith :: (a -> b -> c) -> [a] -> [b] -> [c] on zipWith(f, xs, ys)
set lng to min(|length|(xs), |length|(ys)) if 1 > lng then return {} set xs_ to take(lng, xs) -- Allow for non-finite set ys_ to take(lng, ys) -- generators like cycle etc set lst to {} tell mReturn(f) repeat with i from 1 to lng set end of lst to |λ|(item i of xs_, item i of ys_) end repeat return lst end tell
end zipWith</lang>
- Output:
{{"978-1734314502", true}, {"978-1734314509", false}, {"978-1788399081", true}, {"978-1788399083", false}}
Straightforward
This task can be tackled very simply by working through the numeric text two characters at a time:
<lang applescript>on validateISBN13(ISBN13)
if (ISBN13's class is not text) then return false set astid to AppleScript's text item delimiters set AppleScript's text item delimiters to {"-", space} set ISBN13 to ISBN13's text items set AppleScript's text item delimiters to "" set ISBN13 to ISBN13 as text set AppleScript's text item delimiters to astid if (((count ISBN13) is not 13) or (ISBN13 contains ".") or (ISBN13 contains ",")) then return false try ISBN13 as number on error return false end try set sum to 0 repeat with i from 1 to 12 by 2 set sum to sum + (character i of ISBN13) + (character (i + 1) of ISBN13) * 3 -- Automatic text-to-number coercions. end repeat return ((sum + (character 13 of ISBN13)) mod 10 = 0)
end validateISBN13
-- Test: set output to {} set verdicts to {"bad", "good"} repeat with thisISBN13 in {"978-1734314502", "978-1734314509", "978-1788399081", "978-1788399083"}
set isValid to validateISBN13(thisISBN13) set end of output to thisISBN13 & ": " & item ((isValid as integer) + 1) of verdicts
end repeat
set astid to AppleScript's text item delimiters set AppleScript's text item delimiters to linefeed set output to output as text set AppleScript's text item delimiters to astid return output</lang>
- Output:
"978-1734314502: good 978-1734314509: bad 978-1788399081: good 978-1788399083: bad"
Or it can be handled purely numerically. Since the "weights" alternate and are palindromic, it makes no difference whether the last digit or the first is treated as the check digit. In fact, if preferred, the repeat below can go round 7 times with the return line as simply: return (sum mod 10 = 0).
<lang applescript>on validateISBN13(ISBN13)
if (ISBN13's class is not text) then return false set astid to AppleScript's text item delimiters set AppleScript's text item delimiters to {"-", space} set ISBN13 to ISBN13's text items set AppleScript's text item delimiters to "" set ISBN13 to ISBN13 as text set AppleScript's text item delimiters to astid if (((count ISBN13) is not 13) or (ISBN13 contains ".") or (ISBN13 contains ",")) then return false try set ISBN13 to ISBN13 as number on error return false end try set sum to 0 repeat 6 times set sum to sum + ISBN13 mod 10 + ISBN13 mod 100 div 10 * 3 set ISBN13 to ISBN13 div 100 end repeat return ((sum + ISBN13) mod 10 = 0)
end validateISBN13</lang>
AWK
<lang AWK>
- syntax: GAWK -f ISBN13_CHECK_DIGIT.AWK
BEGIN {
arr[++n] = "978-1734314502" arr[++n] = "978-1734314509" arr[++n] = "978-1788399081" arr[++n] = "978-1788399083" arr[++n] = "9780820424521" arr[++n] = "0820424528" for (i=1; i<=n; i++) { printf("%s %s\n",arr[i],isbn13(arr[i])) } exit(0)
} function isbn13(isbn, check_digit,i,sum) {
gsub(/[ -]/,"",isbn) if (length(isbn) != 13) { return("NG length") } for (i=1; i<=12; i++) { sum += substr(isbn,i,1) * (i % 2 == 1 ? 1 : 3) } check_digit = 10 - (sum % 10) return(substr(isbn,13,1) == check_digit ? "OK" : sprintf("NG check digit S/B %d",check_digit))
} </lang>
- Output:
978-1734314502 OK 978-1734314509 NG check digit S/B 2 978-1788399081 OK 978-1788399083 NG check digit S/B 1 9780820424521 OK 0820424528 NG length
C
<lang c>#include <stdio.h>
int check_isbn13(const char *isbn) {
int ch = *isbn, count = 0, sum = 0; /* check isbn contains 13 digits and calculate weighted sum */ for ( ; ch != 0; ch = *++isbn, ++count) { /* skip hyphens or spaces */ if (ch == ' ' || ch == '-') { --count; continue; } if (ch < '0' || ch > '9') { return 0; } if (count & 1) { sum += 3 * (ch - '0'); } else { sum += ch - '0'; } } if (count != 13) return 0; return !(sum%10);
}
int main() {
int i; const char* isbns[] = {"978-1734314502", "978-1734314509", "978-1788399081", "978-1788399083"}; for (i = 0; i < 4; ++i) { printf("%s: %s\n", isbns[i], check_isbn13(isbns[i]) ? "good" : "bad"); } return 0;
}</lang>
- Output:
978-1734314502: good 978-1734314509: bad 978-1788399081: good 978-1788399083: bad
Factor
<lang factor>USING: combinators.short-circuit formatting kernel math math.functions math.parser math.vectors qw sequences sequences.extras sets unicode ;
- (isbn13?) ( str -- ? )
string>digits [ <evens> sum ] [ <odds> 3 v*n sum + ] bi 10 divisor? ;
- isbn13? ( str -- ? )
"- " without { [ length 13 = ] [ [ digit? ] all? ] [ (isbn13?) ] } 1&& ;
qw{ 978-1734314502 978-1734314509 978-1788399081 978-1788399083 } [ dup isbn13? "good" "bad" ? "%s: %s\n" printf ] each</lang>
- Output:
978-1734314502: good 978-1734314509: bad 978-1788399081: good 978-1788399083: bad
Go
<lang go>package main
import (
"fmt" "strings" "unicode/utf8"
)
func checkIsbn13(isbn string) bool {
// remove any hyphens or spaces isbn = strings.ReplaceAll(strings.ReplaceAll(isbn, "-", ""), " ", "") // check length == 13 le := utf8.RuneCountInString(isbn) if le != 13 { return false } // check only contains digits and calculate weighted sum sum := int32(0) for i, c := range isbn { if c < '0' || c > '9' { return false } if i%2 == 0 { sum += c - '0' } else { sum += 3 * (c - '0') } } return sum%10 == 0
}
func main() {
isbns := []string{"978-1734314502", "978-1734314509", "978-1788399081", "978-1788399083"} for _, isbn := range isbns { res := "bad" if checkIsbn13(isbn) { res = "good" } fmt.Printf("%s: %s\n", isbn, res) }
}</lang>
- Output:
978-1734314502: good 978-1734314509: bad 978-1788399081: good 978-1788399083: bad
Haskell
<lang haskell>import Data.Char (isDigit, digitToInt) import Control.Monad (forM_) import Text.Printf (printf)
testISBNs :: [String] testISBNs = ["978-1734314502", "978-1734314509", "978-1788399081", "978-1788399083"]
pair :: Num a => [a] -> [(a, a)] pair [] = [] pair xs = p (take 2 xs) : pair (drop 2 xs)
where p ps = case ps of (x:y:zs) -> (x,y) (x:zs) -> (x,0)
validIsbn13 :: String -> Bool validIsbn13 isbn
| length (digits isbn) /= 13 = False | otherwise = calc isbn `rem` 10 == 0 where digits = map digitToInt . filter isDigit calc = foldl (\a (x, y) -> x + y * 3 + a) 0 . pair . digits
main :: IO () main = forM_ testISBNs (\isbn -> printf "%s: Valid: %s\n" isbn (show $ validIsbn13 isbn))</lang>
- Output:
978-1734314502: Valid: True 978-1734314509: Valid: False 978-1788399081: Valid: True 978-1788399083: Valid: False
Or, expressed in terms of cycle:
<lang haskell>import Data.Char (digitToInt, isDigit)
isISBN13 :: String -> Bool isISBN13 =
(0 ==) . flip rem 10 . sum . flip (zipWith ((*) . digitToInt) . filter isDigit) (cycle [1, 3])
main :: IO () main =
mapM_ print $ ((,) <*> isISBN13) <$> ["978-1734314502", "978-1734314509", "978-1788399081", "978-1788399083"]</lang>
- Output:
("978-1734314502",True) ("978-1734314509",False) ("978-1788399081",True) ("978-1788399083",False)
Julia
<lang julia>function isbncheck(str)
return sum(iseven(i) ? 3 * parse(Int, ch) : parse(Int, ch) for (i, ch) in enumerate(replace(str, r"\D" => ""))) % 10 == 0
end
const testingcodes = ["978-1734314502", "978-1734314509",
"978-1788399081", "978-1788399083"]
for code in testingcodes
println(code, ": ", isbncheck(code) ? "good" : "bad")
end
</lang>
- Output:
978-1734314502: good 978-1734314509: bad 978-1788399081: good 978-1788399083: bad
langur
In this example, we map to multiple functions (actually 1 no-op). <lang langur>val .isbn13checkdigit = f(var .s) {
.s = replace(.s, RE/[\- ]/) matching(re/^[0-9]{13}$/, .s) and fold(f{+}, map [_, f{x 3}], s2n .s) div 10
}
val .tests = h{
"978-1734314502": true, "978-1734314509": false, "978-1788399081": true, "978-1788399083": false,
}
for .key of .tests {
val .pass = .isbn13checkdigit(.key) write .key, ": ", if(.pass: "good"; "bad") writeln if(.pass == .tests[.key]: ""; " (ISBN-13 CHECK DIGIT TEST FAILED)")
}</lang>
In this example, we set a for loop value as it progresses. <lang langur>val .isbn13checkdigit = f(var .s) {
.s = replace(.s, RE/[\- ]/) var .alt = false matching(re/^[0-9]{13}$/, .s) and for[=0] .d in s2n(.s) { _for += if(.alt: .d x 3; .d); not= .alt } div 10
}
val .tests = h{
"978-1734314502": true, "978-1734314509": false, "978-1788399081": true, "978-1788399083": false,
}
for .key of .tests {
val .pass = .isbn13checkdigit(.key) write .key, ": ", if(.pass: "good"; "bad") writeln if(.pass == .tests[.key]: ""; " (ISBN-13 CHECK DIGIT TEST FAILED)")
}</lang>
- Output:
978-1734314502: good 978-1734314509: bad 978-1788399081: good 978-1788399083: bad
Nanoquery
<lang nanoquery>def checkIsbn13(isbn)
// remove any hyphens or spaces isbn = str(isbn).replace("-","").replace(" ","")
// check length = 13 if len(isbn) != 13 return false end
// check only contains digits and calculate weighted sum sum = 0 for i in range(0, len(isbn) - 1) c = isbn[i] if (ord(c) < ord("0")) or (ord(c) > ord("9")) return false end
if (i % 2) = 0 sum += ord(c) - ord("0") else sum += 3 * (ord(c) - ord("0")) end end
return (sum % 10) = 0
end
isbns = {"978-1734314502", "978-1734314509", "978-1788399081", "978-1788399083"} for isbn in isbns
res = "bad" if checkIsbn13(isbn) res = "good" end
print format("%s: %s\n", isbn, res)
end</lang>
- Output:
978-1734314502: good 978-1734314509: bad 978-1788399081: good 978-1788399083: bad
Perl
<lang perl>use strict; use warnings; use feature 'say';
sub check_digit {
my($isbn) = @_; my($sum); $sum += (1,3)[$_%2] * (split , join , split /\D/, $isbn)[$_] for 0..11; (10 - $sum % 10) % 10;
}
for (<978-1734314502 978-1734314509 978-1788399081 978-1788399083 978-2-74839-908-0 978-2-74839-908-5>) {
my($isbn,$check) = /(.*)(.)/; my $check_d = check_digit($isbn); say "$_ : " . ($check == $check_d ? 'Good' : "Bad check-digit $check; should be $check_d")
}</lang>
- Output:
978-1734314502 : Good 978-1734314509 : Bad check-digit 9; should be 2 978-1788399081 : Good 978-1788399083 : Bad check-digit 3; should be 1 978-2-74839-908-0 : Good 978-2-74839-908-5 : Bad check-digit 5; should be 0
Phix
<lang Phix>procedure check_isbn13(string isbn)
integer digits = 0, checksum = 0, w = 1 for i=1 to length(isbn) do integer ch = isbn[i] if ch!=' ' and ch!='-' then ch -= '0' if ch<0 or ch>9 then checksum = 9 exit end if checksum += ch*w digits += 1 w = 4-w end if end for checksum = remainder(checksum,10) string gb = iff(digits=13 and checksum=0 ? "good" : "bad") printf(1,"%s: %s\n",{isbn,gb})
end procedure
constant isbns = {"978-1734314502", "978-1734314509", "978-1788399081", "978-1788399083",
"978-2-74839-908-0","978-2-74839-908-5","978 1 86197 876 9"}
for i=1 to length(isbns) do check_isbn13(isbns[i]) end for</lang>
- Output:
978-1734314502: good 978-1734314509: bad 978-1788399081: good 978-1788399083: bad 978-2-74839-908-0: good 978-2-74839-908-5: bad 978 1 86197 876 9: good
PicoLisp
<lang PicoLisp>(de isbn13? (S)
(let L (make (for N (chop S) (and (format N) (link @)) ) ) (and (= 13 (length L)) (=0 (% (sum * L (circ 1 3)) 10)) ) ) )
(mapc
'((A) (tab (-19 1) A (if (isbn13? A) 'ok 'fail) ) ) (quote "978-1734314502" "978-1734314509" "978-1-86197-876-9" "978-2-74839-908-5" "978 1 86197 876 9" ) )</lang>
- Output:
978-1734314502 ok 978-1734314509 fail 978-1-86197-876-9 ok 978-2-74839-908-5 fail 978 1 86197 876 9 ok
Python
<lang python>def is_isbn13(n):
n = n.replace('-',).replace(' ', ) if len(n) != 13: return False product = (sum(int(ch) for ch in n[::2]) + sum(int(ch) * 3 for ch in n[1::2])) return product % 10 == 0
if __name__ == '__main__':
tests =
978-1734314502 978-1734314509 978-1788399081 978-1788399083.strip().split()
for t in tests: print(f"ISBN13 {t} validates {is_isbn13(t)}")</lang>
- Output:
ISBN13 978-1734314502 validates True ISBN13 978-1734314509 validates False ISBN13 978-1788399081 validates True ISBN13 978-1788399083 validates False
Or, expressed in terms of itertools.cycle
<lang python>ISBN13 check digit
from itertools import cycle from operator import mul
- isISBN13 :: String -> Bool
def isISBN13(s):
True if the digits of s form a valid ISBN-13 code. digits = [int(c) for c in s if c.isdigit()] return 13 == len(digits) and 0 == sum( map( mul, digits, cycle([1, 3]) ) ) % 10
- ---------------------------TEST---------------------------
- main :: IO ()
def main():
Validation of four strings:
for s in ['978-1734314502', '978-1734314509', '978-1788399081', '978-1788399083']: print((s, isISBN13(s)))
- MAIN ---
if __name__ == '__main__':
main()</lang>
- Output:
('978-1734314502', True) ('978-1734314509', False) ('978-1788399081', True) ('978-1788399083', False)
Raku
(formerly Perl 6)
Also test a value that has a zero check digit.
<lang perl6>sub check-digit ($isbn) {
(10 - (sum (|$isbn.comb(/<[0..9]>/)) »*» (1,3)) % 10).substr: *-1
}
{
my $check = .substr(*-1); my $check-digit = check-digit .chop; say "$_ : ", $check == $check-digit ?? 'Good' !! "Bad check-digit $check; should be $check-digit"
} for words <
978-1734314502 978-1734314509 978-1788399081 978-1788399083 978-2-74839-908-0 978-2-74839-908-5
>;</lang>
- Output:
978-1734314502 : Good 978-1734314509 : Bad check-digit 9; should be 2 978-1788399081 : Good 978-1788399083 : Bad check-digit 3; should be 1 978-2-74839-908-0 : Good 978-2-74839-908-5 : Bad check-digit 5; should be 0
REXX
A couple of additional checks were made to verify a correct length, and also that the ISBN-13 code is all numerics (with optional minus signs). <lang rexx>/*REXX pgm validates the check digit of an ISBN─13 code (it may have embedded minuses).*/ parse arg $ /*obtain optional arguments from the CL*/ if $= | if $="," then $= '978-1734314502 978-1734314509 978-1788399081 978-1788399083' @ISBN= "ISBN─13 code isn't" /*a literal used when displaying msgs. */
/* [↓] remove all minuses from X code.*/ do j=1 for words($); y= word($,j) /*obtain an ISBN─13 code from $ list.*/ x= space( translate(y, , '-'), 0) /*remove all minus signs from the code.*/ L= length(x) /*obtain the length of the ISBN-13 code*/ if L \== 13 then do; say @ISBN '13 characters: ' x; exit 13; end if verify(x, 9876543210)\==0 then do; say @ISBN 'numeric: ' x; exit 10; end sum= 0 do k=1 for L; #= substr(x, k, 1) /*get a decimal digit from the X code. */ if \(k//2) then #= # * 3 /*multiply every other digit by three. */ sum= sum + # /*add the digit (or product) to the SUM*/ end /*k*/
if right(sum, 1)==0 then say ' ISBN-13 code ' x " is valid." else say ' ISBN-13 code ' x " isn't valid." end /*j*/ /*stick a fork in it, we're all done. */</lang>
- output when using the four default inputs:
ISBN-13 code 9781734314502 is valid. ISBN-13 code 9781734314509 isn't valid. ISBN-13 code 9781788399081 is valid. ISBN-13 code 9781788399083 isn't valid.
Swift
<lang swift>func checkISBN(isbn: String) -> Bool {
guard !isbn.isEmpty else { return false }
let sum = isbn .compactMap({ $0.wholeNumberValue }) .enumerated() .map({ $0.offset & 1 == 1 ? 3 * $0.element : $0.element }) .reduce(0, +)
return sum % 10 == 0
}
let cases = [
"978-1734314502", "978-1734314509", "978-1788399081", "978-1788399083"
]
for isbn in cases {
print("\(isbn) => \(checkISBN(isbn: isbn) ? "good" : "bad")")
}</lang>
- Output:
978-1734314502 => good 978-1734314509 => bad 978-1788399081 => good 978-1788399083 => bad
XPL0
<lang XPL0>include xpllib; \contains StrLen function
proc ISBN13(Str); \Show if International Standard Book Number is good char Str; int Sum, Cnt, Dig, I; [Sum:= 0; Cnt:= 0; for I:= 0 to StrLen(Str)-1 do
[Dig:= Str(I) - ^0; if Dig>=0 & Dig<=9 then [Sum:= Sum + Dig; Cnt:= Cnt + 1; if (Cnt&1) = 0 then Sum:= Sum + Dig + Dig; ]; ];
Text(0, Str); Text(0, if rem(Sum/10)=0 & Cnt=13 then ": good" else ": bad"); CrLf(0); ];
[ISBN13("978-1734314502");
ISBN13("978-1734314509"); ISBN13("978-1788399081"); ISBN13("978-1788399083"); ISBN13("978-1-59327-220-3"); ISBN13("978-178839918");
]</lang>
- Output:
978-1734314502: good 978-1734314509: bad 978-1788399081: good 978-1788399083: bad 978-1-59327-220-3: good 978-178839918: bad
zkl
<lang zkl>fcn ISBN13_check(isbn){ // "978-1734314502", throws on invalid digits
var [const] one3=("13"*6 + 1).split("").apply("toInt"); // examine 13 digits // one3=("13"*6) if you want to calculate what the check digit should be one3.zipWith('*,isbn - " -").sum(0) % 10 == 0
}</lang> <lang zkl>isbns:=
- <<<"
978-1734314502 978-1734314509 978-1788399081 978-1788399083 978-2-74839-908-0 978-2-74839-908-5".split("\n");
- <<<
foreach isbn in (isbns)
{ println(isbn.strip()," ",ISBN13_check(isbn) and " Good" or " Bad") }</lang>
- Output:
978-1734314502 Good 978-1734314509 Bad 978-1788399081 Good 978-1788399083 Bad 978-2-74839-908-0 Good 978-2-74839-908-5 Bad