% \iffalse meta-comment
%
%% File: l3fp-round.dtx
%
% Copyright (C) 2011-2025 The LaTeX Project
%
% It may be distributed and/or modified under the conditions of the
% LaTeX Project Public License (LPPL), either version 1.3c of this
% license or (at your option) any later version.  The latest version
% of this license is in the file
%
%    https://www.latex-project.org/lppl.txt
%
% This file is part of the "l3kernel bundle" (The Work in LPPL)
% and all files in that bundle must be distributed together.
%
% -----------------------------------------------------------------------
%
% The development version of the bundle can be found at
%
%    https://github.com/latex3/latex3
%
% for those people who are interested.
%
%<*driver>
\documentclass[full,kernel]{l3doc}
\begin{document}
  \DocInput{\jobname.dtx}
\end{document}
%</driver>
% \fi
%
% \title{^^A
%   The \pkg{l3fp-round} module\\ Rounding floating points^^A
% }
%
% \author{^^A
%  The \LaTeX{} Project\thanks
%    {^^A
%      E-mail:
%        \href{mailto:latex-team@latex-project.org}
%          {latex-team@latex-project.org}^^A
%    }^^A
% }
%
% \date{Released 2025-12-29}
%
% \maketitle
%
% \begin{documentation}
%
% \end{documentation}
%
% \begin{implementation}
%
% \section{\pkg{l3fp-round} implementation}
%
%    \begin{macrocode}
%<*code>
%    \end{macrocode}
%
%    \begin{macrocode}
%<@@=fp>
%    \end{macrocode}
%
% ^^A todo: provide an interface for rounding modes.
% ^^A todo: provide a \l_@@_rounding_mode_int giving the current mode.
% ^^A todo: make transcendental function obey the correct rounding mode.
% ^^A todo: optimize all rounding functions for various rounding modes.
% ^^A todo: reduce the number of almost identical functions.
%
% \begin{macro}[EXP]
%   {
%     \@@_parse_word_trunc:N,
%     \@@_parse_word_floor:N,
%     \@@_parse_word_ceil:N
%   }
%    \begin{macrocode}
\cs_new:Npn \@@_parse_word_trunc:N
  { \@@_parse_function:NNN \@@_round_o:Nw \@@_round_to_zero:NNN }
\cs_new:Npn \@@_parse_word_floor:N
  { \@@_parse_function:NNN \@@_round_o:Nw \@@_round_to_ninf:NNN }
\cs_new:Npn \@@_parse_word_ceil:N
  { \@@_parse_function:NNN \@@_round_o:Nw \@@_round_to_pinf:NNN }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]
%   {
%     \@@_parse_word_round:N, \@@_parse_round:Nw,
%   }
%    \begin{macrocode}
\cs_new:Npn \@@_parse_word_round:N #1#2
  {
    \@@_parse_function:NNN
      \@@_round_o:Nw \@@_round_to_nearest:NNN #1
    #2
  }
\cs_new:Npn \@@_parse_round:Nw #1 #2 \@@_round_to_nearest:NNN #3#4
  { #2 #1 #3 }
%    \end{macrocode}
% \end{macro}
%
% \subsection{Rounding tools}
%
% \begin{variable}{\c_@@_five_int}
%   This is used as the half-point for which numbers are rounded
%   up/down.
%    \begin{macrocode}
\int_const:Nn \c_@@_five_int { 5 }
%    \end{macrocode}
% \end{variable}
%
% Floating point operations often yield a result that cannot be exactly
% represented in a significand with $16$ digits.  In that case, we need to
% round the exact result to a representable number.  The \textsc{ieee}
% standard defines four rounding modes:
% \begin{itemize}
%   \item Round to nearest: round to the representable floating point
%     number whose absolute difference with the exact result is the
%     smallest.  If the exact result lies exactly at the mid-point
%     between two consecutive representable floating point numbers,
%     round to the floating point number whose last digit is even.
%   \item Round towards negative infinity: round to the greatest
%     floating point number not larger than the exact result.
%   \item Round towards zero: round to a floating point number with the
%     same sign as the exact result, with the largest absolute value not
%     larger than the absolute value of the exact result.
%   \item Round towards positive infinity: round to the least floating
%     point number not smaller than the exact result.
% \end{itemize}
% This is not fully implemented in \pkg{l3fp} yet, and transcendental
% functions fall back on the \enquote{round to nearest} mode.  All
% rounding for basic algebra is done through the functions defined in
% this module, which can be redefined to change their rounding behavior
% (but there is not interface for that yet).
%
% The rounding tools available in this module are many variations on a
% base function \cs{@@_round:NNN}, which expands to |0\exp_stop_f:| or
% |1\exp_stop_f:| depending on whether the final result should be rounded up
% or down.
% \begin{itemize}
%   \item \cs{@@_round:NNN} \meta{sign} \meta{digit_1} \meta{digit_2}
%     can expand to |0\exp_stop_f:| or |1\exp_stop_f:|.
%   \item \cs{@@_round_s:NNNw} \meta{sign} \meta{digit_1} \meta{digit_2}
%     \meta{more digits}\cs{@@_sep:} can expand to
%     |0\exp_stop_f:|\cs{@@_sep:} or |1\exp_stop_f:|\cs{@@_sep:}.
%   \item \cs{@@_round_neg:NNN} \meta{sign} \meta{digit_1} \meta{digit_2}
%     can expand to |0\exp_stop_f:| or |1\exp_stop_f:|.
% \end{itemize}
% See implementation comments for details on the syntax.
%
% \begin{macro}[rEXP]{\@@_round:NNN}
% \begin{macro}[rEXP]
%   {
%     \@@_round_to_nearest:NNN,
%     \@@_round_to_nearest_ninf:NNN,
%     \@@_round_to_nearest_zero:NNN,
%     \@@_round_to_nearest_pinf:NNN,
%     \@@_round_to_ninf:NNN,
%     \@@_round_to_zero:NNN,
%     \@@_round_to_pinf:NNN
%   }
%   \begin{syntax}
%     \cs{@@_round:NNN} \meta{final sign} \meta{digit_1} \meta{digit_2}
%   \end{syntax}
%   If rounding the number $\meta{final sign}
%   \meta{digit_1}.\meta{digit_2}$ to an integer rounds it towards zero
%   (truncates it), this function expands to |0\exp_stop_f:|, and otherwise
%   to |1\exp_stop_f:|.  Typically used within the scope of an
%   \cs{@@_int_eval:w}, to add~$1$ if needed, and thereby round
%   correctly.  The result depends on the rounding mode.
%
%   It is very important that \meta{final sign} be the final sign of the
%   result. Otherwise, the result would be incorrect in the case of
%   rounding towards~$-\infty$ or towards~$+\infty$. Also recall that
%   \meta{final sign} is~$0$ for positive, and~$2$ for negative.
%
%   By default, the functions below return |0\exp_stop_f:|, but this is
%   superseded by \cs{@@_round_return_one:}, which instead returns
%   |1\exp_stop_f:|, expanding everything and removing |0\exp_stop_f:| in the
%   process.  In the case of rounding towards~$\pm\infty$ or
%   towards~$0$, this is not really useful, but it prepares us for the
%   \enquote{round to nearest, ties to even} mode.
%
%   The \enquote{round to nearest} mode is the default.  If the
%   \meta{digit_2} is larger than~$5$, then round up.  If it is less
%   than~$5$, round down.  If it is exactly $5$, then round such that
%   \meta{digit_1} plus the result is even.  In other words, round up if
%   \meta{digit_1} is odd.
%
%   The \enquote{round to nearest} mode has three variants, which differ
%   in how ties are rounded: down towards $-\infty$, truncated towards $0$,
%   or up towards $+\infty$.
%    \begin{macrocode}
\cs_new:Npn \@@_round_return_one:
  { \exp_after:wN 1 \exp_after:wN \exp_stop_f: \exp:w }
\cs_new:Npn \@@_round_to_ninf:NNN #1 #2 #3
  {
    \if_meaning:w 2 #1
      \if_int_compare:w #3 > \c_zero_int
        \@@_round_return_one:
      \fi:
    \fi:
    \c_zero_int
  }
\cs_new:Npn \@@_round_to_zero:NNN #1 #2 #3 { \c_zero_int }
\cs_new:Npn \@@_round_to_pinf:NNN #1 #2 #3
  {
    \if_meaning:w 0 #1
      \if_int_compare:w #3 > \c_zero_int
        \@@_round_return_one:
      \fi:
    \fi:
    \c_zero_int
  }
\cs_new:Npn \@@_round_to_nearest:NNN #1 #2 #3
  {
    \if_int_compare:w #3 > \c_@@_five_int
      \@@_round_return_one:
    \else:
      \if_meaning:w 5 #3
        \if_int_odd:w #2 \exp_stop_f:
          \@@_round_return_one:
        \fi:
      \fi:
    \fi:
    \c_zero_int
  }
\cs_new:Npn \@@_round_to_nearest_ninf:NNN #1 #2 #3
  {
    \if_int_compare:w #3 > \c_@@_five_int
      \@@_round_return_one:
    \else:
      \if_meaning:w 5 #3
        \if_meaning:w 2 #1
            \@@_round_return_one:
        \fi:
      \fi:
    \fi:
    \c_zero_int
  }
\cs_new:Npn \@@_round_to_nearest_zero:NNN #1 #2 #3
  {
    \if_int_compare:w #3 > \c_@@_five_int
      \@@_round_return_one:
    \fi:
    \c_zero_int
  }
\cs_new:Npn \@@_round_to_nearest_pinf:NNN #1 #2 #3
  {
    \if_int_compare:w #3 > \c_@@_five_int
      \@@_round_return_one:
    \else:
      \if_meaning:w 5 #3
        \if_meaning:w 0 #1
            \@@_round_return_one:
        \fi:
      \fi:
    \fi:
    \c_zero_int
  }
\cs_new_eq:NN \@@_round:NNN \@@_round_to_nearest:NNN
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \begin{macro}[EXP]{\@@_round_s:NNNw}
%   \begin{syntax}
%     \cs{@@_round_s:NNNw} \meta{final sign} \meta{digit} \meta{more digits} \cs{@@_sep:}
%   \end{syntax}
%   Similar to \cs{@@_round:NNN}, but with an extra \cs{@@_sep:}, this
%   function expands to |0\exp_stop_f:|\cs{@@_sep:} if rounding
%   $\meta{final sign}\meta{digit}.\meta{more digits}$ to an integer
%   truncates, and to |1\exp_stop_f:|\cs{@@_sep:} otherwise.
%   The \meta{more digits} part must be a digit,
%   followed by something that does not overflow a \cs{int_use:N}
%   \cs{@@_int_eval:w} construction.  The only relevant information about
%   this piece is whether it is zero or not.
%    \begin{macrocode}
\cs_new:Npn \@@_round_s:NNNw #1 #2 #3 #4\@@_sep:
  {
    \exp_after:wN \@@_round:NNN
    \exp_after:wN #1
    \exp_after:wN #2
    \int_value:w \@@_int_eval:w
      \if_int_odd:w 0 \if_meaning:w 0 #3 1 \fi:
                      \if_meaning:w 5 #3 1 \fi:
                \exp_stop_f:
        \if_int_compare:w \@@_int_eval:w #4 > \c_zero_int
          1 +
        \fi:
      \fi:
      #3
    \@@_sep:
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]{\@@_round_digit:Nw}
%   \begin{syntax}
%     \cs{int_value:w} \cs{@@_round_digit:Nw} \meta{digit} \meta{int expr} \cs{@@_sep:}
%   \end{syntax}
%   This function should always be called within an \cs{int_value:w}
%   or \cs{@@_int_eval:w} expansion; it may add an extra
%   \cs{@@_int_eval:w}, which means that the integer or integer
%   expression should not be ended with a synonym of \tn{relax}, but
%   with a \cs{@@_sep:} for instance.
%    \begin{macrocode}
\cs_new:Npn \@@_round_digit:Nw #1 #2\@@_sep:
  {
    \if_int_odd:w \if_meaning:w 0 #1 1 \else:
                  \if_meaning:w 5 #1 1 \else:
                  0 \fi: \fi: \exp_stop_f:
      \if_int_compare:w \@@_int_eval:w #2 > \c_zero_int
        \@@_int_eval:w 1 +
      \fi:
    \fi:
    #1
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]{\@@_round_neg:NNN}
% \begin{macro}[EXP]
%   {
%     \@@_round_to_nearest_neg:NNN,
%     \@@_round_to_nearest_ninf_neg:NNN,
%     \@@_round_to_nearest_zero_neg:NNN,
%     \@@_round_to_nearest_pinf_neg:NNN,
%     \@@_round_to_ninf_neg:NNN,
%     \@@_round_to_zero_neg:NNN,
%     \@@_round_to_pinf_neg:NNN
%   }
%   \begin{syntax}
%     \cs{@@_round_neg:NNN} \meta{final sign} \meta{digit_1} \meta{digit_2}
%   \end{syntax}
%   This expands to |0\exp_stop_f:| or |1\exp_stop_f:| after doing the following
%   test. Starting from a number of
%   the form $ \meta{final sign}0.\meta{15 digits}\meta{digit_1} $ with exactly
%   $15$ (non-all-zero) digits before \meta{digit_1}, subtract from it
%   $\meta{final sign}0.0\ldots{}0\meta{digit_2}$, where there are $16$~zeros.
%   If in the current rounding mode the result should be rounded down,
%   then this function returns |1\exp_stop_f:|. Otherwise, i.e.,
%   if the result is rounded back to the first operand, then this function
%   returns |0\exp_stop_f:|.
%
%   It turns out that this negative \enquote{round to nearest}
%   is identical to the positive one. And this is the default mode.
%    \begin{macrocode}
\cs_new_eq:NN \@@_round_to_ninf_neg:NNN \@@_round_to_pinf:NNN
\cs_new:Npn \@@_round_to_zero_neg:NNN #1 #2 #3
  {
    \if_int_compare:w #3 > \c_zero_int
      \@@_round_return_one:
    \fi:
    \c_zero_int
  }
\cs_new_eq:NN \@@_round_to_pinf_neg:NNN \@@_round_to_ninf:NNN
\cs_new_eq:NN \@@_round_to_nearest_neg:NNN \@@_round_to_nearest:NNN
\cs_new_eq:NN \@@_round_to_nearest_ninf_neg:NNN
  \@@_round_to_nearest_pinf:NNN
\cs_new:Npn \@@_round_to_nearest_zero_neg:NNN #1 #2 #3
  {
    \if_int_compare:w #3 < \c_@@_five_int \else:
      \@@_round_return_one:
    \fi:
    \c_zero_int
  }
\cs_new_eq:NN \@@_round_to_nearest_pinf_neg:NNN
  \@@_round_to_nearest_ninf:NNN
\cs_new_eq:NN \@@_round_neg:NNN \@@_round_to_nearest_neg:NNN
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \subsection{The \texttt{round} function}
%
% ^^A todo: This macro is intermingled with l3fp-parse.
% ^^A todo: Add explanations.
% \begin{macro}[EXP]{\@@_round_o:Nw, \@@_round_aux_o:Nw}
%   First check that all arguments are floating point numbers.
%   The |trunc|, |ceil| and |floor| functions expect one or two
%   arguments (the second is $0$ by default), and the |round| function
%   also accepts a third argument (\texttt{nan} by default), which
%   changes |#1| from \cs{@@_round_to_nearest:NNN} to one of its
%   analogues.
%    \begin{macrocode}
\cs_new:Npn \@@_round_o:Nw #1
  {
    \@@_parse_function_all_fp_o:fnw
      { \@@_round_name_from_cs:N #1 }
      { \@@_round_aux_o:Nw #1 }
  }
\cs_new:Npn \@@_round_aux_o:Nw #1#2 @
  {
    \if_case:w
      \@@_int_eval:w \@@_array_count:n {#2} \@@_int_eval_end:
         \@@_round_no_arg_o:Nw #1 \exp:w
    \or: \@@_round:Nwn #1 #2 {0} \exp:w
    \or: \@@_round:Nww #1 #2 \exp:w
    \else: \@@_round:Nwww #1 #2 @ \exp:w
    \fi:
    \exp_after:wN \exp_end:
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]{\@@_round_no_arg_o:Nw}
%    \begin{macrocode}
\cs_new:Npn \@@_round_no_arg_o:Nw #1
  {
    \cs_if_eq:NNTF #1 \@@_round_to_nearest:NNN
      { \@@_error_num_args:nnnn { round } { 1 } { 3 } { 0 } }
      {
        \@@_error_num_args:ffff
          { \@@_round_name_from_cs:N #1 } { 1 } { 2 } { 0 }
      }
    \exp_after:wN \c_nan_fp
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]{\@@_round:Nwww}
%   Having three arguments is only allowed for |round|, not |trunc|,
%   |ceil|, |floor|, so check for that case.  If all is well, construct
%   one of \cs{@@_round_to_nearest:NNN}, \cs{@@_round_to_nearest_zero:NNN},
%   \cs{@@_round_to_nearest_ninf:NNN}, \cs{@@_round_to_nearest_pinf:NNN}
%   and act accordingly.
%    \begin{macrocode}
\cs_new:Npn \@@_round:Nwww
    #1#2 \@@_sep: #3 \@@_sep: \s_@@ \@@_chk:w #4#5#6 \@@_sep: #7 @
  {
    \cs_if_eq:NNTF #1 \@@_round_to_nearest:NNN
      {
        \tl_if_empty:nTF {#7}
          {
            \exp_args:Nc \@@_round:Nww
              {
                @@_round_to_nearest
                \if_meaning:w 0 #4 _zero \else:
                \if_case:w #5 \exp_stop_f: _pinf \or: \else: _ninf \fi: \fi:
                :NNN
              }
            #2 \@@_sep: #3 \@@_sep:
          }
          {
            \@@_error_num_args:ffff { round } { 1 } { 3 }
              { \int_eval:n { 3 + \@@_array_count:n {#7} } }
            \exp_after:wN \c_nan_fp
          }
      }
      {
        \@@_error_num_args:ffff
          { \@@_round_name_from_cs:N #1 } { 1 } { 2 }
          { \int_eval:n { 3 + \@@_array_count:n {#7} } }
        \exp_after:wN \c_nan_fp
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]{\@@_round_name_from_cs:N}
%    \begin{macrocode}
\cs_new:Npn \@@_round_name_from_cs:N #1
  {
    \cs_if_eq:NNTF #1 \@@_round_to_zero:NNN { trunc }
      {
        \cs_if_eq:NNTF #1 \@@_round_to_ninf:NNN { floor }
          {
            \cs_if_eq:NNTF #1 \@@_round_to_pinf:NNN { ceil }
              { round }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]{\@@_round:Nww, \@@_round:Nwn}
% \begin{macro}[EXP]
%   {
%     \@@_round_normal:NwNNnw ,
%     \@@_round_normal:NnnwNNnn ,
%     \@@_round_pack:Nw ,
%     \@@_round_normal:NNwNnn ,
%     \@@_round_normal_end:wwNnn ,
%     \@@_round_special:NwwNnn ,
%     \@@_round_special_aux:Nw
%   }
%   If the number of digits to round to is an integer or infinity all is
%   good; if it is \texttt{nan} then just produce a \texttt{nan};
%   otherwise invalid as we have something like |round(1,3.14)| where
%   the number of digits is not an integer.
%    \begin{macrocode}
\cs_new:Npn \@@_round:Nww #1#2 \@@_sep: #3 \@@_sep:
  {
    \@@_small_int:wTF #3\@@_sep: { \@@_round:Nwn #1#2\@@_sep: }
      {
        \if:w 3 \@@_kind:w #3 \@@_sep:
          \exp_after:wN \use_i:nn
        \else:
          \exp_after:wN \use_ii:nn
        \fi:
        { \exp_after:wN \c_nan_fp }
        {
          \@@_invalid_operation_tl_o:ff
            { \@@_round_name_from_cs:N #1 }
            { \@@_array_to_clist:n { #2\@@_sep: #3\@@_sep: } }
        }
      }
  }
\cs_new:Npn \@@_round:Nwn #1 \s_@@ \@@_chk:w #2#3#4\@@_sep: #5
  {
    \if_meaning:w 1 #2
      \exp_after:wN \@@_round_normal:NwNNnw
      \exp_after:wN #1
      \int_value:w #5
    \else:
      \exp_after:wN \@@_exp_after_o:w
    \fi:
    \s_@@ \@@_chk:w #2#3#4\@@_sep:
  }
\cs_new:Npn \@@_round_normal:NwNNnw #1#2 \s_@@ \@@_chk:w 1#3#4#5\@@_sep:
  {
    \@@_decimate:nNnnnn { \c_@@_prec_int - #4 - #2 }
      \@@_round_normal:NnnwNNnn #5 #1 #3 {#4} {#2}
  }
\cs_new:Npn \@@_round_normal:NnnwNNnn #1#2#3#4\@@_sep: #5#6
  {
    \exp_after:wN \@@_round_normal:NNwNnn
    \int_value:w \@@_int_eval:w
      \if_int_compare:w #2 > \c_zero_int
        1 \int_value:w #2
        \exp_after:wN \@@_round_pack:Nw
        \int_value:w \@@_int_eval:w 1#3 +
      \else:
        \if_int_compare:w #3 > \c_zero_int
          1 \int_value:w #3 +
        \fi:
      \fi:
      \exp_after:wN #5
      \exp_after:wN #6
      \use_none:nnnnnnn #3
      #1
      \@@_int_eval_end:
      0000 0000 0000 0000 \@@_sep: #6
  }
\cs_new:Npn \@@_round_pack:Nw #1
  { \if_meaning:w 2 #1 + 1 \fi: \@@_int_eval_end: }
\cs_new:Npn \@@_round_normal:NNwNnn #1 #2
  {
    \if_meaning:w 0 #2
      \exp_after:wN \@@_round_special:NwwNnn
      \exp_after:wN #1
    \fi:
    \@@_pack_twice_four:wNNNNNNNN
    \@@_pack_twice_four:wNNNNNNNN
    \@@_round_normal_end:wwNnn
    \@@_sep: #2
  }
\cs_new:Npn \@@_round_normal_end:wwNnn #1\@@_sep:#2\@@_sep:#3#4#5
  {
    \exp_after:wN \@@_exp_after_o:w \exp:w \exp_end_continue_f:w
    \@@_sanitize:Nw #3 #4 \@@_sep: #1 \@@_sep:
  }
\cs_new:Npn \@@_round_special:NwwNnn #1#2\@@_sep:#3\@@_sep:#4#5#6
  {
    \if_meaning:w 0 #1
      \@@_case_return:nw
        { \exp_after:wN \@@_zero_fp:N \exp_after:wN #4 }
    \else:
      \exp_after:wN \@@_round_special_aux:Nw
      \exp_after:wN #4
      \int_value:w \@@_int_eval:w 1
        \if_meaning:w 1 #1 -#6 \else: +#5 \fi:
    \fi:
    \@@_sep:
  }
\cs_new:Npn \@@_round_special_aux:Nw #1#2\@@_sep:
  {
    \exp_after:wN \@@_exp_after_o:w \exp:w \exp_end_continue_f:w
    \@@_sanitize:Nw #1#2\@@_sep: {1000}{0000}{0000}{0000}\@@_sep:
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
%    \begin{macrocode}
%</code>
%    \end{macrocode}
%
% \end{implementation}
%
% \PrintChanges
%
% \PrintIndex
