% \iffalse meta-comment % %% File: l3fp-round.dtx % % Copyright (C) 2011-2024 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} % % \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 2024-03-14} % % \maketitle % % \begin{documentation} % % \end{documentation} % % \begin{implementation} % % \section{\pkg{l3fp-round} implementation} % % \begin{macrocode} %<*package> % \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 behaviour % (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}|;| can expand to |0\exp_stop_f:;| or |1\exp_stop_f:;|. % \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} |;| % \end{syntax} % Similar to \cs{@@_round:NNN}, but with an extra semicolon, this % function expands to |0\exp_stop_f:;| if rounding $\meta{final sign} % \meta{digit}.\meta{more digits}$ to an integer truncates, and to % |1\exp_stop_f:;| 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; { \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 ; } % \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} |;| % \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 semi-colon for instance. % \begin{macrocode} \cs_new:Npn \@@_round_digit:Nw #1 #2; { \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, \emph{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:nnnn { num-args } { round () } { 1 } { 3 } } { \@@_error:nffn { num-args } { \@@_round_name_from_cs:N #1 () } { 1 } { 2 } } \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 ; #3 ; \s_@@ \@@_chk:w #4#5#6 ; #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 ; #3 ; } { \@@_error:nnnn { num-args } { round () } { 1 } { 3 } \exp_after:wN \c_nan_fp } } { \@@_error:nffn { num-args } { \@@_round_name_from_cs:N #1 () } { 1 } { 2 } \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 ; #3 ; { \@@_small_int:wTF #3; { \@@_round:Nwn #1#2; } { \if:w 3 \@@_kind:w #3 ; \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; #3; } } } } } \cs_new:Npn \@@_round:Nwn #1 \s_@@ \@@_chk:w #2#3#4; #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; } \cs_new:Npn \@@_round_normal:NwNNnw #1#2 \s_@@ \@@_chk:w 1#3#4#5; { \@@_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; #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 ; #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 ; #2 } \cs_new:Npn \@@_round_normal_end:wwNnn #1;#2;#3#4#5 { \exp_after:wN \@@_exp_after_o:w \exp:w \exp_end_continue_f:w \@@_sanitize:Nw #3 #4 ; #1 ; } \cs_new:Npn \@@_round_special:NwwNnn #1#2;#3;#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: ; } \cs_new:Npn \@@_round_special_aux:Nw #1#2; { \exp_after:wN \@@_exp_after_o:w \exp:w \exp_end_continue_f:w \@@_sanitize:Nw #1#2; {1000}{0000}{0000}{0000}; } % \end{macrocode} % \end{macro} % \end{macro} % % \begin{macrocode} % % \end{macrocode} % % \end{implementation} % % \PrintChanges % % \PrintIndex