# Calculating with \directlua in Plain LuaTeX

I have never used Lua before, so I have some questions about the calculations:

1. How can avoid appending .0 if the result is an integer?
2. How can I achieve that the right number of zeros is appended after rounding?
3. How can I change the decimal marker . into a comma ,?

What I have tried (MWE):

% Plain LuaTeX
\catcode\@=11

% From latex.ltx
\long\def\@ifnextchar#1#2#3{%
\let\reserved@d=#1%
\def\reserved@a{#2}%
\def\reserved@b{#3}%
\futurelet\@let@token\@ifnch}
\def\@ifnch{%
\ifx\@let@token\@sptoken
\let\reserved@c\@xifnch
\else
\ifx\@let@token\reserved@d
\let\reserved@c\reserved@a
\else
\let\reserved@c\reserved@b
\fi
\fi
\reserved@c}
\def\:{\let\@sptoken= } \:  % this makes \@sptoken a space token
\def\:{\@xifnch} \expandafter\def\: {\futurelet\@let@token\@ifnch}

% seen somewhere on tex.stackexchange.com
\directlua{
function math.round_int ( x )
return x>=0 and math.floor(x+0.5) or math.ceil(x-0.5)
end
function math.round ( x , n )
return ( math.round_int ( x*10^n ) / 10^n )
end
}

% My macros
\def\c@lc[#1]#2{\directlua{tex.print(math.round(#2,#1))}}
\def\calc{\@ifnextchar[\c@lc{\c@lc[2]}} % Default is rounding to 10^-2

Let's try the macros:

$\sqrt2 \approx \calc[4]{2^(1/2)}$ is OK.

$\root5\of{32} = \calc{32^(1/5)}$ should be $2$ as it is an integer.

$1.699999 \approx \calc[3]{1.699999}$ should be $1.700$ because it is rounded to $10^{-3}$.

$1.9 \approx \calc[0]{1.9}$ should be $2$ as it is rounded to whole numbers.

$8 \approx \calc[-1]{8}$ should be $10$ as it is rounded to $10^1$.

\bye


• LuaTeX always adds a digit after the decimal point (even if it's 0) when a calculation involves operations on floating point numbers, which 32^(1/5) is, for instance. – egreg Oct 26 '19 at 15:39
• This is due to the way Lua 5.3 is handling its numbers. To avoid the decimal point, you should use one of the proper string format: '%d' should make it as far as I understand. Hans Hagen gave an example in the last ConTeXt group proceedings: local b=2*(1/2) print(string.format("%d", b), math.type(x)) gives as output b 2*(1/2) d 1 float – sztruks Oct 26 '19 at 15:45
• @sztruks - Your brief comments were actually helpful to me as I began thinking about a solution. :-) For the issue at hand, though, it's better to apply coercion-to-int if the value of the rounding operation is known to be integer-valued (which will be the case if n is 0 or negative). If n is positive, then a string.format ( "%."..n.."f" , <rounded number here>) operation is probably most appropriate. – Mico Oct 26 '19 at 21:36

How can I avoid appending .0 if the result [of the rounding operation] is an integer?

In the versions of Lua prior to 5.3, there was only a single numeric data type ("float"); however, Lua was programmed to display numbers without decimal parts without the trailing .0, i.e., as though they were whole numbers (which, of course, they are). Version 5.3 of Lua introduced a new numeric data type called "integer"; Lua version 5.3 was incorporated into LuaTeX earlier this year (2019). A side effect of this change is that whole numbers are now shown without a trailing .0 only if their type is "integer". (Of course, Lua 5.3 provides methods for converting a number from type float to type integer. One of these methods is employed in the code shown below.)

The issue you're experiencing, then, is caused by the fact that the Lua function math.round, which is called by \c@lc, always returns a number of type "float"; hence the trailing .0. To display whole numbers without a trailing .0, I suggest you make two changes. First, add the following code chunk

function math.myround ( x , n )
if n==0 then
return ( math.round_int ( x ) )
elseif n<0 then
return ( math.floor ( math.round ( x , n ) ) )
else
return ( math.round ( x , n ) )
end
end


to the argument of the first \directlua call. Second change

\def\c@lc[#1]#2{\directlua{tex.print(math.round(#2,#1))}}


to

\def\c@lc[#1]#2{\directlua{tex.print(math.myround(#2,#1))}}


How does math.myround differ from math.round? The function math.myround calls math.round_int directly if the first argument of \c@lc is 0, i.e., if rounding to the nearest integer is to be performed. Moreover, it applies coercion-from-float-to-integer (after calling math.round) if the first argument of \c@lc is negative. That way, the rounded number returned by math.myround is of type integer if n is 0 or negative, i.e., if the rounded number is, in fact, an whole number.

How can I achieve that the right number of zeros is appended after rounding?

If x is equal to, say, 1.7 after rounding to n digits after the decimal marker (where n is an integer, aka a positive whole number), then running

string.format ( "%."..n.."f" , x )


creates a string with n explicit digits after the decimal marker -- some of which may be zero.

Note: Since there's a % symbol present in the first argument of string.format, you can't load the code via \directlua as TeX will choke fatally on the % symbol. You'll have to place the Lua code into an external file called, say, myround.lua, and load that file via the instruction \directlua{dofile("myround.lua")}.

The third question you asked was,

How can I change the decimal marker . into a comma ,?

Lua provides a very powerful function called string.gsub. (The g in gsub stands for "global".) You could change

    return ( math.round ( x , n ) )


in the function math.myround to

    return ( ( string.gsub ( math.round ( x , n ) , "%." , "{,}" ) )


Observe that . is changed not to , but to {,}; this is done to inform TeX that it shouldn't treat these instances of , as characters of type math-punct.

Same note as above: Since there's a % symbol present in the argument of string.gsub, you mustn't load the code via \directlua (since TeX will again choke painfully and agonizingly on the % symbol). I suggest you store the Lua code in an external file called, say, myround.lua, and load that file via the instruction \directlua{dofile("myround.lua")}.

The following solution addresses all three questions you asked. It employs LaTeX rather than Plain-TeX, mostly because I don't know how to employ the filecontents environment -- which is very useful for creating external files -- in a Plain-TeX setting. See further below, though, for instructions that lead to a Plain-TeX solution.

% !TEX TS-program = lualatex
\documentclass{article} % or some other suitable document class

% The next instruction requires LaTeX format 2019-10-01 (or more recent)
\begin{filecontents*}[overwrite]{myround.lua}

-- First two functions seen somewhere on tex.stackexchange.com.
-- (Maybe https://tex.stackexchange.com/a/468118/5001 ??)

function math.round_int ( x )
return x>=0 and math.floor(x+0.5) or math.ceil(x-0.5)
end

function math.round ( x , n )
return math.round_int ( x*10^n ) / 10^n
end

-- New:
function math.myround ( x , n )
if n==0 then
return math.round_int ( x )
elseif n<0 then
return math.floor ( math.round ( x , n ) )
else   -- n>0
x = math.round ( x , n ) -- replace 'x' with rounded version
x = string.format ( "%."..n.."f" , x )
return ( x:gsub ( "%." , "{,}" , 1 ) )
end
end

\end{filecontents*}

\directlua{dofile("myround.lua")} % Load the externally-stored code

\newcommand\calc[2][0]{% default value of opt. param.: 0
\directlua{tex.sprint(math.myround(#2,#1))}}

\begin{document}
\obeylines
Let's try the macros:
$\sqrt{2} \approx \calc[4]{2^(1/2)}$. OK.
$\sqrt[5]{32} = \calc{32^(1/5)}$. OK.
$1.699999 \approx \calc[3]{1.699999}$. OK.
$1.699999 \approx \calc[1]{1.699999}$. Also OK.
$1.9 \approx \calc{1.9}$. OK.
$8 \approx \calc[-1]{8}$. OK.
\end{document}


Addendum: For the sake of completeness, here's how a Plain-TeX solution would look like:

• First, create an external file called myround.lua and place in it the contents of the filecontents environment (3 Lua functions) shown above.

• Second, delete the contents of the first \directlua instruction in your example code, since that code is now contained in the external file.

• Third, replace

% My macros
\def\c@lc[#1]#2{\directlua{tex.print(math.round(#2,#1))}}
\def\calc{\@ifnextchar[\c@lc{\c@lc[2]}} % Default is rounding to 10^-2


with

\directlua{dofile("myround.lua")} % load the externally-stored code

% My macros
\def\c@lc[#1]#2{\directlua{tex.print(math.myround(#2,#1))}}
\def\calc{\@ifnextchar[\c@lc{\c@lc[0]}} % Default rounding: to nearest integer
`
• Thank you. Really impressive. :) – Weißer Kater Oct 26 '19 at 21:18
• @user125730 - You're most welcome! – Mico Oct 26 '19 at 21:21