在我维护的本地课程文件中,我们有一个可以自动将数字(在我们的例子中是作业的分数)相加然后输出总和的系统。
我想发展它以增加有限的非整数输入(并妥善处理非数字输入,即仅输出给定的参数,或以其他方式捕获这种情况)。具体来说,我希望它能够处理半分。
我预期为用户提供的界面是一个命令,比如说\half
,它可以用来将半个标记“添加”到累计总数中,并将半个标记呈现为分数。
以下是基于egreg 对类似问题的回答。
\documentclass{article}
\newcounter{sum}
\makeatletter
\def\mycommand#1{%
\afterassignment\get@args\count@=0#1\hfuzz#1\hfuzz}
\def\get@args#1\hfuzz#2\hfuzz{%
\if\relax\detokenize{#1}\relax
#2 is a number%
\addtocounter{sum}{#2}
\else
#2 is not a number%
\fi
}
\def\@dhalf{\(\frac12\)}
\gdef\@half{\@dhalf\gdef\half{\@@half}}
\gdef\@@half{\@dhalf\addtocounter{sum}{1}\gdef\half{\@half}}
\let\half\@half
\makeatother
\begin{document}
\mycommand{\half}\par
\mycommand{1}\par
\mycommand{1\half}\par
\mycommand{2}\par
\mycommand{text}\par
Total is: \thesum
\end{document}
如您所见,一次将两个半分加到计数器中sum
没有问题。(如果存在剩余的半分,我也可以渲染它,尽管我没有在 MWE 中这样做过。)但是,当您有类似 这样的数字时1\half
,那么这“不是一个数字”,因此 1 不会添加到总数中。
我坦率地承认,我的麻烦部分在于我对数字的定义\mycommand
和\get@args
捕获方法只有一个肤浅的理解,但无论如何,也许有人可以建议一种方法来让它按照预期的方式运行?
答案1
在此实现中,只允许一个尾随\half
,无论前面是否有整数。整数和之间\half
允许有空格。
分数存储在本地,因此如果您有多个部分需要评估不同的分数,则可以使用环境。
我使用是\Marks
因为\marks
已经被占用(一个 e-TeX 原语)。
其思路是先检查输入是否合法,然后判断是否\half
存在。在这种情况下,“半”计数器会递增。打印时,其值除以 2,如果“半分”的数量为奇数,则附加分数。
控制序列\half
实际上是一个虚拟的,因此为了打印我们必须将其设置为\frac{1}{2}
。
\documentclass{article}
\ExplSyntaxOn
\int_new:N \l_rbrignall_mark_integer_int
\int_new:N \l_rbrignall_mark_half_int
\NewDocumentCommand{\Marks}{m}
{% print and store
\regex_match:nnTF { \A [[:digit:]]* \s* \c{half}? \Z } { #1 }
{% good input
$\cs_set:Npn \half {\frac{1}{2}} #1$ % print
\rbrignall_store:n { #1 } % store
}
{ \mbox{FIX~MARKS!!!} }
}
\NewExpandableDocumentCommand{\half}{}{}% dummy
\NewDocumentCommand{\TotalMarks}{}
{
$
\int_eval:n
{
\l_rbrignall_mark_integer_int + \int_div_truncate:nn {\l_rbrignall_mark_half_int}{2}
}
\int_if_odd:nT { \l_rbrignall_mark_half_int } { \frac{1}{2} }
$
}
\cs_new_protected:Nn \rbrignall_store:n
{
\tl_if_in:nnT { #1 } { \half } { \int_incr:N \l_rbrignall_mark_half_int }
\int_add:Nn \l_rbrignall_mark_integer_int { 0#1 }
}
\ExplSyntaxOff
\begin{document}
\textbf{Test 1}
\begingroup % for testing more examples
\Marks{\half}\par
\Marks{1}\par
\Marks{1 \half}\par
\Marks{2}\par
\Marks{text}\par
Total is: \TotalMarks\par
\endgroup
\bigskip
\textbf{Test 2}
\begingroup % for testing more examples
\Marks{\half}\par
\Marks{1\half}\par
\Marks{1 \half}\par
\Marks{2}\par
Total is: \TotalMarks\par
\endgroup
\end{document}
答案2
下面的代码定义了一个命令\addmark
和一个命令\totalmarks
。其中\addmark
,\half
可用于添加0.5分。此命令可以多次使用。例如,\half\half
添加1和2\half 3
添加5.5。
\documentclass[border=6pt,varwidth]{standalone}
\ExplSyntaxOn
\fp_new:N \l__rbrignall_total_fp
\tl_new:N \l__rbrignall_mark_tl
\NewDocumentCommand \addmark { m }
{
\tl_set:Nn \l__rbrignall_mark_tl {#1}
\tl_replace_all:Nnn \l__rbrignall_mark_tl { \half } { + 0.5 + }
\fp_add:Nn \l__rbrignall_total_fp { \l__rbrignall_mark_tl + 0 }%note the + 0 for the case that #1 ends with \half
}
\NewDocumentCommand \totalmarks { }
{
\fp_compare:nNnTF { \l__rbrignall_total_fp } = { 0.5 }
{ $ \frac { 1 } { 2 } $ }
{
\fp_compare:nNnTF { \l__rbrignall_total_fp - trunc ( \l__rbrignall_total_fp , 0 ) } = { 0 }
{ \fp_use:N \l__rbrignall_total_fp }
{ \fp_eval:n { \l__rbrignall_total_fp - 0.5 } $ \frac { 1 } { 2 } $ }
}
}
\ExplSyntaxOff
\begin{document}
\addmark{\half}\totalmarks
\addmark{\half\half}\totalmarks
\addmark{1\half}\totalmarks
\addmark{2\half 3}\totalmarks
\addmark{4\half 5\half}\totalmarks
\end{document}
答案3
还有一些其他使用 Latex3 的优秀答案,但当我离线时,我设法结合了部分答案(现已删除)中给出的方法,该方法使用长度而不是计数器(感谢海报提出的想法!),这个答案中关于检查浮点数的方法。
为了完整起见,我将在这里分享结果。请注意,该\half
命令实际上现在有点失效了(只需输入浮点小数),因此可以说这回答了一个与我提出的问题略有不同的问题,但无论如何,这里是它:
\documentclass{article}
\makeatletter
\begingroup
\catcode`P=12
\catcode`T=12
\lowercase{%
\def\x{%
\def\my@rem@pt##1.##2PT{%
\ifnum##2=5
\ifnum##1=0\else##1\fi% integer part
\(\frac12\)% if 0.5, print 1/2
\else % Could capture more fractions here
##1%
\ifnum##2=0% if no decimal part do nothing more
\else.##2% otherwise print the decimal
\fi
\fi
}%
}%
}\expandafter\endgroup\x
\newlength{\mysum}
\newlength{\mkval}
\newcommand{\thesum}{\expandafter\my@rem@pt\the\mysum}
\newcommand*{\mycommand}[1]{%
\begingroup
\def\half{.5}%
\newif\if@nan\@nanfalse%
\newif\if@fndpt\@fndptfalse%
\edef\tmp{\testleadneg#1\relax}%
\expandafter\testreal\tmp\relax%
\if@nan%
\def\half{\(\frac12\)}%
#1%
\else\global\advance\mysum#1\p@%
\setlength{\mkval}{#1pt}%
\expandafter\my@rem@pt\the\mkval
\fi
\endgroup%
}
\def\testreal#1#2\relax{%
\if.#1\if@fndpt\@nantrue\else\@fndpttrue\fi\else
\if1#1\else\if2#1\else\if3#1\else\if4#1\else
\if5#1\else\if6#1\else\if7#1\else\if8#1\else
\if9#1\else\if0#1\else\@nantrue%
\fi\fi\fi\fi\fi\fi\fi\fi\fi\fi\fi
\if@nan\else\if\relax#2\else\testreal#2\relax\fi\fi}
%
\def\testleadneg#1#2\relax{\if-#1#2\else#1#2\fi}
%
\makeatother
\begin{document}
\begin{tabular}{c|c}
Mark&Total\\\hline
\mycommand{0}&\thesum\\
\mycommand{\half}&\thesum\\
\mycommand{1}&\thesum\\
\mycommand{1.5}&\thesum\\
\mycommand{2\half}&\thesum\\
\mycommand{.2}&\thesum\\
\mycommand{text 2}&\thesum\\
\end{tabular}
\end{document}
答案4
据该答案初始版本的作者所知,TeX 通过寄存器和原语可以在 -2 31 +1 到 +2 31 -1 范围内进行算术运算。
如果
- 只有整数和分数 ½ 和
- 你不介意将范围缩小到 -2 30 +1 至 +2 30 -1,
然后你就可以让 TeX 维护
- 一个宏
\doublesum
,它不会浪费一个\count
寄存器,而是保存要计算的总和的两倍, - 一个宏
\thesum
,它将减半的结果\doublesum
作为数字序列提供,并且可能提供一个实例\frac{1}{2}
, - 一个宏
\addtosum
,用于将其参数的两倍添加\doublesum
到 - 一个宏
\clearsum
,定义为\doublesum
空。
(顺便说一下,\sum
已经在 TeX/LaTeX 中定义,用于在数学排版中获取大写 sigma,其下标/上标限制通常用于索引元素的总和。因此,\sum
在实现自己的宏“基础设施”的过程中,不要试图意外覆盖名称中包含短语“sum”的宏。)
这个答案的初始版本的作者没有费心检查参数是否\addtosum
具有正确的形式。
\half
一方面,其他答案中已经显示了实施某种检查,将输入限制为特定类别的字符标记(符号、数字、空格)中的特定星座的明确字符标记,后面最多跟一个控制字标记。
另一方面,问题并没有明确说明什么才是“正确形式”。
例如,如果任何 TeX ⟨number⟩ 数量——⟨number⟩ 在 TeXbook 的“第 24 章:垂直模式总结”中以 Backus-Naur 符号进行解释;ε-TeX 和 pdfTeX 和 LuaTeX 和 XeTeX 以及其他 TeX 引擎扩展了 TeX 的 ⟨number⟩ 的概念——再加上可能要\half
接受一个由可选空格包围的控制字标记实例,那么就会出现如何解释诸如\numexpr\mycounter\relax\half
当 的当前值为\mycounter
0 时之类的问题。那是 0½ 吗?如果是这样:因为 0=-0,所以 0½=-0½ 吗?那是 ½ 还是 -½?
除此之外,当 TeX 收集 ⟨number⟩ 的标记时,扩展通常不会被抑制。因此,正确的检查可能需要在 TeX 中实现一个算法,用于检查构成参数的标记是否扩展为一组标记,这些标记形成有效的⟨可选符号⟩或完整的有效⟨数字⟩,可能后面跟着一个可选控制字标记\half
,该控制字标记可能被⟨可选空格⟩/⟨一个可选空格⟩包围,以防您设法将其放在控制字标记后面。因此,人们将面临在 TeX 中实现一个算法来检查扩展结果的任务。当涉及宏参数标记的扩展时,这些标记本身可以被视为扩展驱动算法实现的各个方面。因此,在其他事物之下实现一个用于检查扩展结果的算法的任务将需要检查由构成参数的标记组成的算法是否
- 无错误终止。
- 终止时不会产生不必要的副作用,也不会产生错误消息(可能会发生一些边缘情况,例如,在排列宏参数的标记时,扩展它们会导致吞噬/重新排列不属于该参数的后续标记,但是这些标记是某些宏的⟨定义⟩的⟨替换文本⟩的组成部分——该宏反过来可能属于形成检查例程的宏机制……——因此,宏机制的实现者旨在将其作为保持扩展链正常运转的手段)。
- 完全终止。
这在 TeX 中并不简单,这个答案的初始版本的作者想起了停机问题。
\makeatletter
%-----------------------------------------------------------------------
\@ifdefinable\stopromannumeral{\chardef\stopromannumeral=`\^^00 }%
\newcommand\@twooftwo[2]{#1#2}%
%-----------------------------------------------------------------------
\newcommand\doublesum{}%
%-----------------------------------------------------------------------
\newcommand*\addtosum[1]{%
\edef\doublesum{%
\the\numexpr(0\doublesum)+(\addtosumSplitAtHalf#1\half Z\relax)%
\relax
}%
}%
\@ifdefinable\addtosumSplitAtHalf{%
\def\addtosumSplitAtHalf#1\half#2\relax{%
(#1+0)*2
\ifx#2Z\else
% The token \half is there, thus
% - in case the number is negative or 0 is prefixed by a
% minus sign subtract 1,
% - in case the number is positive or 0 is not prefixed by a
% minus sign add 1.
\ifnum\numexpr(#1+0)\relax=0 %
\ifnum\numexpr(#11)\relax<0 -\else+\fi
\else
\ifnum\numexpr(#1+0)\relax<0 -\else+\fi
\fi
1\fi
}%
}%
%-----------------------------------------------------------------------
\newcommand\clearsum{\def\doublesum{}}%
%-----------------------------------------------------------------------
\newcommand\thesum{%
\romannumeral
% Check if the integer-part of the sum is 0.
\ifnum
\numexpr
(0\doublesum)/2%
\ifodd\numexpr(0\doublesum)\relax
\ifnum\numexpr(0\doublesum)\relax>0 -\else+\fi1%
\fi
\relax=0 %
\expandafter\@firstoftwo\else\expandafter\@secondoftwo\fi
{%
% In case the integer-part of the sum is 0 and the doubled sum is
% even, no rounding occured, thus just print 0. In case of the
% doubled sum being odd, rounding occurred, so 1/2 or -1/2 needs
% to be delivered depending on whether the doubled sum is positive
% or negative.
\ifodd\numexpr(0\doublesum)\relax
\expandafter\@firstoftwo\else\expandafter\@secondoftwo\fi
{%
\ifnum\numexpr(0\doublesum)\relax<0 %
\expandafter\stopromannumeral\expandafter-%
\else
\expandafter\stopromannumeral
\fi
\frac{1}{2}%
}%
{\stopromannumeral0}%
}{%
% In case the integer-part of the sum is not 0, calculate it
% by halving the doubled via \numexpr's division where rounding
% occurs in case the doubled sum is odd. In case of roundig
% and the number being positive subtract 1 otherwise add 1.
% In case of rounding also deliver 1/2.
\expandafter\stopromannumeral
\the\numexpr
(0\doublesum)/2%
\ifodd\numexpr(0\doublesum)\relax\ifnum\numexpr(0\doublesum)\relax
>0 -\else+\fi1\expandafter\@twooftwo\else\expandafter\@firstoftwo\fi
\relax{\frac{1}{2}}%
}%
}%
%-----------------------------------------------------------------------
% \addtosum and \clearsum can be prefixed by \global.
% Due to \romannumeral-expansion the result of \thesum is generated
% within two expansion steps.
%-----------------------------------------------------------------------
\makeatother
\documentclass{article}
\begin{document}
\global\clearsum \(\thesum\)
\par\(\thesum - 7 = \global\addtosum{-7} \thesum\)
\par\(\thesum + 3 = \global\addtosum{3} \thesum\)
\par\(\thesum + 1\frac{1}{2} = \global\addtosum{1\half} \thesum\)
\par\(\thesum - 2\frac{1}{2} = \global\addtosum{-2\half} \thesum\)
\par\(\thesum + 5 = \global\addtosum{5} \thesum\)
\par\(\thesum + 7 = \global\addtosum{7} \thesum\)
\par\(\thesum - 3 = \global\addtosum{-3} \thesum\)
\par\(\thesum - 1\frac{1}{2} = \global\addtosum{-1\half} \thesum\)
\par\(\thesum + 2\frac{1}{2} = \global\addtosum{+2\half} \thesum\)
\par\(\thesum - 5 = \global\addtosum{-5} \thesum\)
\par\(\thesum - 1 = \global\addtosum{-1} \thesum\)
\par\(\thesum + \frac{1}{2} = \global\addtosum{0\half} \thesum\)
\par\(\thesum + \frac{1}{2} = \global\addtosum{\half} \thesum\)
\par\(\thesum + \frac{1}{2} = \global\addtosum{+\half} \thesum\)
\par\(\thesum + \frac{1}{2} = \global\addtosum{+0\half} \thesum\)
\par\(\thesum - \frac{1}{2} = \global\addtosum{-0\half} \thesum\)
\par\(\thesum - \frac{1}{2} = \global\addtosum{-\half} \thesum\)
\par\(\thesum - 0 = \global\addtosum{-0} \thesum\)
\par\(\thesum + 0 = \global\addtosum{+0} \thesum\)
\par\(\thesum + 0 = \global\addtosum{0} \thesum\)
\par\(\thesum + 0 = \global\addtosum{} \thesum\)
\par\(\thesum + 0 = \global\addtosum{+} \thesum\)
\par\(\thesum + 0 = \global\addtosum{-} \thesum\)
\par\(\thesum + 15\frac{1}{2} = \global\addtosum{15\half} \thesum\)
\edef\SomeSumMacro{%
\unexpanded\expandafter\expandafter\expandafter{\thesum}%
}
\par The sum calculated so far is stored in the macro
\verb|\SomeSumMacro| whose meaning is:\\
\texttt{\meaning\SomeSumMacro}
\end{document}
\documentclass{article} \newcounter{sum} \makeatletter \def\mycommand#1{% \afterassignment\get@args\count@=0#1\hfuzz#1\hfuzz} \def\get@args#1\hfuzz#2\hfuzz{% \if\relax\detokenize{#1}\relax #2 is a number% \addtocounter{sum}{#2} \else #2 is not a number% \fi }
[...]
\makeatother
[...]
我坦然承认,我遇到的麻烦部分在于,我对数字的定义\mycommand
和\get@args
捕捉方法只有肤浅的理解,
[...]
\mycommand
是一个宏。在扩展它时,TeX 会抓取一个未分隔的参数,并传递 的\mycommand
⟨replacement text⟩,其中参数#1
被构成参数的标记替换。扩展\mycommand
会产生以下内容:
\afterassignment\get@args
\count@=0⟨\mycommand's argument⟩\hfuzz⟨\mycommand's argument⟩\hfuzz
-directive\afterassignment
使 TeX在从标记流中收集并处理了\get@args
对 -register 进行赋值所需的所有标记\count
(用 ⟨countdef token⟩ 表示)后立即插入标记: 因为有一个前导,所以收集属于赋值的 ⟨number⟩ 的标记并据此扩展事物的过程会在遇到非数字时停止。(或者,一旦很明显 ⟨\mycommand's arcgis 参数⟩ 表示的数字太大。在这种情况下,你会通过低级 TeX 错误消息收到通知。)执行赋值之后,TeX 位于赋值之后,因此标记会插入在第一个标记之前,在收集赋值的 ⟨number⟩ 的过程中,该标记被认为不是该 ⟨number⟩ 的组成部分。\count@
0
\get@args
如果 ⟨\mycommand 的参数⟩(扩展后)为空或者仅由不会形成太大数字的数字组成,则这是\hfuzz
来自 ⟨replacement text⟩ 的第一个标记\mycommand
。
然后\get@args
抓取直到第一个标记的所有内容\hfuzz
作为其第一个\hfuzz
-分隔参数,并将 ⟨\mycommand 的参数⟩ 作为其由第二个标记 分隔的第二个参数\hfuzz
。因此,如果在为 -assignment 收集 ⟨number⟩ 的标记过程中 ⟨\mycommand 的参数⟩\count@
没有产生空值或只有数字,\get@args
则 的第一个参数不为空。(通过 测试空值。) 在这种情况下, 的第二个参数(包含 ⟨\mycommand 的参数⟩)用于传递。如果的第一个参数为空,收集分配的 ⟨number⟩ 的标记不会导致留下不是 ⟨number⟩ 组成部分的东西,你会得到。\if\relax\detokenize{\get@args's 1st argument⟩}\relax⟨empty⟩else⟨not empty⟩\fi
\get@args
⟨\mycommand's argument⟩ is not a number
\get@args
⟨\mycommand's argument⟩ is a number\addtocounter{sum}{⟨\mycommand's argument⟩}
测试并非完全安全。
例如,如果 ⟨\mycommand's arcgis⟩ 为空,则测试假设为 0,但这不一定被视为不良/错误行为。
例如,如果 ⟨\mycommand's arcgis⟩ 扩展为数字,后面跟着标记\hfuzz
,
则测试失败。例如,如果在收集属于 ⟨number⟩ 的标记的过程中扩展 ⟨\mycommand's arcgis⟩ 导致尝试扩展某些内容,而尝试扩展会产生错误消息(例如未定义的标记、不平衡的\if..
/ \else
/ \fi
、不平衡的\csname
/ \endcsname
,\the
后面跟着不是 ⟨internal quantum⟩ 的东西,…),则测试失败。
例如,在收集属于 ⟨number⟩ 的标记的过程中,扩展 ⟨\mycommand's arcgis 参数⟩ 会导致扩展级联(例如\expanded
/ \romannumeral
/ \the
/ \if
/\number
等),从而删除或重新排列一些后续标记\hfuzz⟨\mycommand's argument⟩\hfuzz
,因此 的使用\get@args
与其定义不匹配……)
时,测试失败。例如,在扩展 ⟨\mycommand's arcgis 参数⟩ 的过程中,TeX 陷入某种尾部递归循环,该循环要么永不结束,要么在某个阶段以低级错误消息结束,提示 TeX 的内存容量已超出或数字太大或类似情况,例如\mycommand{3\NastyMacro}
当\NastyMacro
定义为\def\NastyMacro{0\NastyMacro}
或\def\NastyMacro{\NastyMacro0}
或\def\NastyMacro{\NastyMacro 1}
时\def\NastyMacro{1\NastyMacro}
。