12

I would like to calculate a checksum in LaTeX. My problem is that I don't know how to get the numeric value of a char.

Here is the pseudo code of the algorithm:

var input = "123456789";
car output = "";
var checksum = 0;
var weight = 10;

foreach(Char c in input) {
    checksum += Char.GetNumericValue(c) * weight;
    weight--;
}

checksum = 11-(checksum mod 11);
if(checksum == 10)
    output += "X";
else if (checksum == 11)
    output += "0";
else
    output += checksum;

print output;

Actually I tried this using the packages forloop and xstring:

\newcommand{\inputstr}{123456789}
\newcounter{i}
\newcounter{c}
\forloop{i}{1}{\value{i} < 10}{%
    %\StrChar{\inputstr}{\value{i}} % returns one char
    \setcounter{c}{\value{\StrChar{\inputstr}{\value{i}}}} % here is no convertion :(
}

Multiply and addition should be also no problem with \multiply and \addcounter but how to do the modulo operation?

  • 2
    \number`\A will give you the character number. – Yiannis Lazarides Dec 31 '11 at 13:45
  • @YiannisLazarides: Why not useing `<char> directly? It already gives the ASCII number. – Martin Scharrer Dec 31 '11 at 13:52
  • 1
    The numeric value of 1 is 1, of course. – egreg Dec 31 '11 at 14:01
  • 2
    Is this the ISBN 10 check sum calculation? If so, I suggest to mention it. The Wikipedia article also mentions a simpler algorithm: "While this may seem more complicated than the first scheme, it can be validated simply by adding all the products together then dividing by 11. The sum can be computed without any multiplications by initializing two variables, t and sum, to 0 and repeatedly performing t = t + digit; sum = sum + t; (which can be expressed in C as sum += t += digit;). If the final sum is a multiple of 11, the ISBN is valid." – Christian Lindig Dec 31 '11 at 14:04
  • 2
    @rekire Now that we know better what you need try the ean13isbn package \usepackage[ISBN=978-80-85955-35-4,SC0]{ean13isbn} and the isbn is EAN13 compatible as well. – Yiannis Lazarides Dec 31 '11 at 15:19
8

If you have an argument as an character, i.e. #1=A, then `#1 will give you the ASCII number of this character. If you want that the character '0' gives you a numeric value of 0, and so on, you simply have to subtract the value `0 from each value. Luckily then characters for the digits are coded in numeric order in ASCII, i.e. `0-`1 = 1 etc.

I would loop over the input text yourself by putting it in front of an end-marker and reading one character a time in a recursive fashion.

\documentclass{article}
\usepackage{calc}

\newcounter{checksum}
\newcounter{weight}
\makeatletter
\newcommand\checksum[1]{%
    \setcounter{checksum}{0}%
    \setcounter{weight}{10}%
    \expandafter\@checksum#1\@nnil
    \loop\ifnum\value{checksum}>10
        \addtocounter{checksum}{-11}%
    \repeat
    \setcounter{checksum}{11-\value{checksum}}%
    \ifnum\value{checksum}=10
        \def\checksumdigit{X}%
    \else
    \ifnum\value{checksum}=11
        \def\checksumdigit{0}%
    \else
        \edef\checksumdigit{\arabic{checksum}}%
    \fi\fi
    \checksumdigit
}
% Reads the input one token a time, should only contains normal characters!
\def\@checksum#1{%
    \ifx\@nnil#1\relax\else % stop looping when endmarker is read
        \addtocounter{checksum}{\value{weight}*(`#1-`0)}%
        \addtocounter{weight}{-1}%
        \expandafter\@checksum % Recursive call => loop
    \fi
}
\makeatother




\begin{document}

\checksum{383480757}%5

\checksum{055215295}%1

\checksum{020113448}%9

\end{document}

This stores the checksum digit into \checksumdigit and prints it in the text. I tested it successfully on the three books above.

  • Of course this doesn't work with ISBN-13, where the weights are alternatively 1 and 3. – egreg Dec 31 '11 at 15:40
  • @egreg: Is ISBN-13 than always equal to the EAN? You could change the \addtocounter{weight}{-1} to \ifnum\value{weight}=1 \setcounter{weight}{3}\else\setcounter{weight}{1}\fi easily, and change the init value of course. Ok, you don't have an automatic detection, but this was not asked for in the question. I simply gave him the code he asked for. – Martin Scharrer Dec 31 '11 at 15:45
  • An ISBN-10 can be converted to ISBN-13 by adding 978 in front and recomputing the checksum. – egreg Dec 31 '11 at 15:47
  • @egreg: I thought so, that's actually a general EAN code with the normal ISBN code (i.e. the 10digit one) integrated. Funny that they now gave it an own name, it has been around like this for longer. – Martin Scharrer Dec 31 '11 at 15:49
5
\documentclass{article}
\usepackage{xstring}
\def\GOODISBN#1{ISBN #1 is valid}
\def\BADISBN#1{ISBN #1 is invalid}

\makeatletter
\def\checkISBN#1{%
  \def\ISBN@arg{#1}%
  \StrDel{#1}{-}[\ISBN@temp]%
  \expandafter\StrLen\expandafter{\ISBN@temp}[\ISBN@length]%
  \ifnum\ISBN@length=10 
    \expandafter\checkISBNold\expandafter{\ISBN@temp}%
  \else
    \ifnum\ISBN@length=13
      \expandafter\checkISBNnew\expandafter{\ISBN@temp}%
    \else
      \BADISBN{\ISBN@arg}
    \fi
  \fi}

\def\checkISBNold#1{%
  \StrGobbleRight{#1}{1}[\ISBN@temp]%
  \StrRight{#1}{1}[\ISBN@check]%
  \@tempcnta=11 \@tempcntb=\z@
  \expandafter\@tfor\expandafter\next 
  \expandafter:\expandafter=\ISBN@temp\do
    {\advance\@tempcnta\m@ne
     \@tempcntb=\numexpr\@tempcntb+\next*\@tempcnta\relax
    }
  \@tempcnta=\@tempcntb
  \divide\@tempcnta by 11
  \multiply\@tempcnta by 11
  \advance\@tempcntb-\@tempcnta
  \@tempcntb=\numexpr11-\@tempcntb\relax
  \ifnum\@tempcntb=11
    \def\ISBN@final{0}%
  \else
    \ifnum\@tempcntb=10
      \def\ISBN@final{X}%
    \else
      \edef\ISBN@final{\number\@tempcntb}%
    \fi
  \fi
  \ifx\ISBN@final\ISBN@check
    \GOODISBN{\ISBN@arg}
  \else
    \BADISBN{\ISBN@arg}
  \fi  
}
\def\checkISBNnew#1{%
  \StrGobbleRight{#1}{1}[\ISBN@temp]%
  \StrRight{#1}{1}[\ISBN@check]%
  \@tempcnta=\z@ \@tempcntb=\z@
  \expandafter\@tfor\expandafter\next
  \expandafter:\expandafter=\ISBN@temp\do
    {\advance\@tempcnta\@ne
     \@tempcntb=\numexpr\@tempcntb+\next*\ifodd\@tempcnta 1\else 3\fi\relax
    }
  \@tempcnta=\@tempcntb
  \divide\@tempcnta by 10
  \multiply\@tempcnta by 10
  \advance\@tempcntb-\@tempcnta
  \@tempcntb=\numexpr10-\@tempcntb\relax
    \ifnum\@tempcntb=10
      \def\ISBN@final{0}%
    \else
      \edef\ISBN@final{\number\@tempcntb}%
   \fi
  \ifx\ISBN@final\ISBN@check
    \GOODISBN{\ISBN@arg}
  \else
    \BADISBN{\ISBN@arg}
  \fi
}

\begin{document}
\checkISBN{1000000011}

\checkISBN{1-00-000001-X}

\checkISBN{0-306-40615-2}

\checkISBN{978-0-306-40615-7}
\end{document}

The result is

ISBN 1000000011 is invalid
ISBN 1-00-000001-X is valid
ISBN 0-306-40615-2 is valid
ISBN 978-0-306-40615-7 is valid

The computation for old ISBN numbers can be streamlined:

\def\checkISBNold#1{%
  \@tempcnta=11 \@tempcntb=\z@
  \@tfor\next:=#1\do   
    {\advance\@tempcnta\m@ne
     \@tempcntb=\numexpr\@tempcntb+\if\next X10\else\next\fi*\@tempcnta\relax
    }
  \@tempcnta=\@tempcntb
  \divide\@tempcnta by 11
  \multiply\@tempcnta by 11 
  \advance\@tempcntb-\@tempcnta
  \ifnum\@tempcntb=\z@
    \GOODISBN{\ISBN@arg}
  \else
    \BADISBN{\ISBN@arg}
  \fi
}
1

An other solution with luatex, based on what @egreg provided.

\begin{filecontents*}{isbn.lua}
function checksum(str)
   local temp = 0
   local weight = 10
   for i = 1, string.len(str) do
      local c = str:sub(i,i)
      temp = temp + tonumber(c) * weight
      weight = weight - 1
   end
   temp = 11 - (temp % 11)
   if temp == 10 then
      return "X"
   else 
      if temp == 11 then
         return "0"
      else
         return tostring(temp)
      end
   end
end

function checkISBN(str)
   local ISBN
   local ISBN_test_str = "ISBN " .. str .. " is "
   ISBN = str:gsub("-","")
   if string.len(ISBN) == 10 then
      return tex.sprint(ISBN_test_str .. checkISBNold(ISBN))
   else
      if string.len(ISBN) == 13 then
         return tex.sprint(ISBN_test_str .. checkISBNnew(ISBN))
      else
         return tex.sprint(ISBN_test_str .. "invalid")
      end
   end
end

function checkISBNold(str)
   local check = str:sub(-1)
   local computedcheck = tostring(checksum(str:sub(1,string.len(str)-1)))
   if check == computedcheck then
      return "valid"
   else
      return "invalid"
   end
end

function checkISBNnew(str)
   local check = str:sub(-1)
   local ISBN = str:sub(1,string.len(str)-1)
   local temp = 0
   local weight,computedcheck
   for i = 1, string.len(ISBN) do
      local c = str:sub(i,i)
      if i%2 == 1 then
         weight = 1
      else
         weight = 3
      end
      temp = temp + tonumber(c) * weight
   end
   temp = temp % 10
   if temp == 0 then
      computedcheck = "0"
   else 
      computedcheck = tostring(10 - temp)
   end
   if check == computedcheck then
      return "valid"
   else
      return "invalid"
   end
end
\end{filecontents*}

\documentclass{minimal}
\directlua{dofile("isbn.lua")}
\def\checksum#1{%
  \directlua{tex.sprint(checksum("#1"))}}
\def\checkISBN#1{%
  \directlua{checkISBN("#1")}}
\begin{document}
\checksum{123456789}

\checkISBN{1000000011}

\checkISBN{1-00-000001-X}

\checkISBN{0-306-40615-2}

\checkISBN{978-0-306-40615-7}

\checkISBN{978-0-306-40415-7}
\end{document}
1

A bit for fun, here is an expandable macro \checksum answering the initial query. Its definition uses no auxiliary macros. I provide two versions, the second one is perhaps more efficient. Anyhow, this is not very computationally intensive task.

\documentclass{article}

\usepackage{xintexpr}

% w index like in Python language
% [L] like a list in Python
% This syntax computes L only once, but each [L][w] to fetch item with
% index w "sees" the whole L
%
% there is annoying problem when the modulo 11 gives 0, then we
% we want final result to be 0 not 11 ...
% I solve it via 10 - (10+x) mod 11 trick to avoid an iterated mod 11 !
\newcommand\checksum[1]{%
\xinttheiiexpr 
  10 - (10 + subs(add([L][w]*(10-w), w = 0..8), L=\xintListWithSep{,}{#1})) 'mod' 11
\relax
}

% more efficient (probably), as here "i" is an individual digit of the input
% and @ at each step is <partial sum>, <decreasing weight>
\renewcommand\checksum[1]{%
\xinttheiiexpr 
   10 - 
% @ is previous value. I.e. a two element "list".
  (10 + [iter(0,10; ([@][0]+[@][1]*i,[@][1]-1), i=\xintListWithSep{,}{#1})][0]) 'mod' 11 
\relax
}% we used 10 - (10 + x) mod 11 trick.

\begin{document}

% expandable macro!

\checksum{123456789}%10

\checksum{383480757}%5

\checksum{055215295}%1

\checksum{020113448}%9

\ifnum\checksum{123456789}=10 OK\else\ERROR\fi

\end{document}

enter image description here

And a no-package approach (as it has not been given in other answers)

\documentclass{article}

\makeatletter
\newcommand\checksum[1]{\check@sum #1}
\def\check@sum #1#2#3#4#5#6#7#8#9{%
    \expandafter\check@@sum\the\numexpr 10*#1 + 9*#2 + 8*#3
    + 7*#4 + 6*#5 + 5*#6 + 4*#7 + 3*#8 + 2*#9.%
}
\def\check@@sum #1.{\the\numexpr 10 - 
                    % this is 10 + #1 mod 11
                    (10 + #1 - 11*((#1+16)/11-1))}
\makeatother

\begin{document}
% expandable macro!

\checksum{123456789}%10

\checksum{383480757}%5

\checksum{055215295}%1

\checksum{020113448}%9

\ifnum\checksum{123456789}=10 OK\else\ERROR\fi

\end{document}

This is naturally much more efficient!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.