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?

rekire
  • 1,454
  • 2
    \number\\A` will give you the character number. – yannisl Dec 31 '11 at 13:45
  • @YiannisLazarides: Why not useing \` directly? It already gives the ASCII number. – Martin Scharrer Dec 31 '11 at 13:52
  • You need to put it where you need the number to be printed or used. If you parsing for character \@tfor is a better solution (from the LaTeX core). Will post a solution if nobody provides one in the meantime to-morrow. – yannisl Dec 31 '11 at 13:57
  • 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
  • Well yes it is the ISBN 10 checksum calculation. But how can I use that \,\numberor@tfor`? I find no examples with google. I could write the convertion code in differnt programming languages but I'm lost in LaTeX. – rekire Dec 31 '11 at 14:07
  • @rekire: They are all either lower-level TeX primitives or an internal LaTeX macro. You won't find them in common LaTeX guides. TeX by Topic should mention the first two. – Martin Scharrer Dec 31 '11 at 14:16
  • @ChristianLindig: The simpler algorithm is for checking an existing ISBN for validity, but as I understand the question the OP wants to calculate the still-missing checksum digit. – Martin Scharrer Dec 31 '11 at 14:23
  • @MartinScharrer That's true but would be surprised if the idea behind the algorithm could not be used for a simpler calculation of the last digit. – Christian Lindig Dec 31 '11 at 14:30
  • 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. – yannisl Dec 31 '11 at 15:19

4 Answers4

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.

Martin Scharrer
  • 262,582
  • 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
}
egreg
  • 1,121,712
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}
cjorssen
  • 10,032
  • 4
  • 36
  • 126
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!