diff options
Diffstat (limited to 'macros/latex/required/l3kernel/l3fp-round.dtx')
-rw-r--r-- | macros/latex/required/l3kernel/l3fp-round.dtx | 592 |
1 files changed, 592 insertions, 0 deletions
diff --git a/macros/latex/required/l3kernel/l3fp-round.dtx b/macros/latex/required/l3kernel/l3fp-round.dtx new file mode 100644 index 0000000000..48ae82730c --- /dev/null +++ b/macros/latex/required/l3kernel/l3fp-round.dtx @@ -0,0 +1,592 @@ +% \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} +%</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 2024-04-11} +% +% \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} +%</package> +% \end{macrocode} +% +% \end{implementation} +% +% \PrintChanges +% +% \PrintIndex |