尝试在 TeX 语言中实现一个简单的算法,我尝试实现一个字符串长度函数(可能是一个标记计数器函数)。我知道 TeX 引擎中的间距与我们习惯的间距有不同的含义,但是……即使将空格字符的 catcode 更改为另一个,我的算法也无法检测到它们。我做错了什么?我忘记了一本不太技术性且有点冗长的约 500 页的书的一段中描述了什么特殊的细节?
\def\step#1{\advance#1by1\relax}
\def\strlen{\begingroup\catcode`\ =11\strlenmain}
%\catcode` =11 do not work too
\def\strlenmain#1{%
\countdef\len=0
\len=0
\edef\arg{#1}
\def\measurelen##1{%
\ifx\endstr##1
\else
\step\len
\expandafter\measurelen
\fi
}
\expandafter\measurelen\arg\endstr
\the\len
\endgroup
}
O tamanho de "..." aberta é :
\strlen{Uma noite em Peron} %%%% outputs 15 %%%%
\def\frase{Uma noite em Peron}
O tamanho de "\frase" é :
\strlen\frase %%%% outputs 15 %%%%
\par
\def\frase{Uma\ noite\ em\ Peron}
O tamanho de "\frase" é :
\strlen\frase %%%% outputs 18 %%%%
\par
\end
答案1
如果你
\def\measurelen#1{...}
那么 #1 就是忽略空格后的第一个标记,或者是括在 中的文本{...}
。我建议创建可扩展的宏\prepspaces
来更改文本
word word word
通过文本
word{ }word{ }word
然后就可以读取文本,\measureelement
而且它也可以读取其#1
空格。
\def\step#1{\advance#1by1\relax}
\def\strlen{\begingroup\strlenmain}
\def\prepspaces#1 #2{#1\ifx\end#2\else{ }#2\expandafter\prepspaces\fi}
\def\strlenmain#1{%
\countdef\len=0
\len=0
\edef\arg{\expandafter\prepspaces#1 \end}
\def\measurelen##1{%
\ifx\endstr##1
\else
\step\len
\expandafter\measurelen
\fi
}
\expandafter\measurelen\arg\endstr
\the\len
\endgroup
}
O tamanho de "..." aberta é :
\strlen{Uma noite em Peron} %%%% outputs 18 %%%%
\def\frase{Uma noite em Peron}
O tamanho de "\frase" é :
\strlen\frase %%%% outputs 18 %%%%
\def\frase{Uma\ noite\ em\ Peron}
O tamanho de "\frase" é :
\strlen\frase %%%% outputs 18 %%%%
\end
答案2
我相信有了 会更容易expl3
。
\input expl3-generic
\ExplSyntaxOn
\cs_new_protected:Npn \textlen #1
{
\bandeira_textlen:n { #1 }
\int_use:N \l__bandeira_textlen_int
}
\cs_new_protected:Npn \textlensave #1 #2
{
\bandeira_textlen:n { #2 }
\tl_set:Nx #1 { \int_use:N \l__bandeira_textlen_int }
}
\int_new:N \l__bandeira_textlen_int
\cs_generate_variant:Nn \text_map_inline:nn { e }
\cs_new_protected:Nn \bandeira_textlen:n
{
\bool_lazy_and:nnTF { \tl_if_single_p:n { #1 } } { \token_if_cs_p:N #1 }
{
\__bandeira_textlen:o { #1 }
}
{
\__bandeira_textlen:n { #1 }
}
}
\cs_new_protected:Nn \__bandeira_textlen:n
{
\int_zero:N \l__bandeira_textlen_int
\text_map_inline:en { \text_purify:n { #1 } } { \int_incr:N \l__bandeira_textlen_int }
}
\cs_generate_variant:Nn \__bandeira_textlen:n { o }
% add other things to change here
\text_declare_purify_equivalent:Nn \' {} % ignore \' for counting
\ExplSyntaxOff
\textlen{Uma noite em Peron}
\def\frase{Uma noite em Peron}
O tamanho de ``\frase'' é:
\textlen\frase
\def\frase{Uma\ noite\ em\ Peron}
O tamanho de ``\frase'' é:
\textlen\frase
\def\frase{Uma {\it noite} em Per\'on}
O tamanho de ``\frase'' é:
\textlen\frase
\textlensave\foo{ÁÉónh EEE}
\foo
\bye
添加对 UTF-8 的支持并避免重音已经在这里处理过了;有普通的 TeX 宏包可以做到这一点,例如 OpTeX。
答案3
啊,注释错了。虽然\strlen\frase
这显然行不通(空格已经被标记化),但另一部分的问题是您需要修改代码以...
\def\strlen{\begingroup\catcode`\ =11 \strlenmain}
回想起来这一切都很明显(如果省略空格,TeX 会提前查看数字是否已经终止,这涉及扩展以下宏并获取参数),但如果没有帮助,调试起来会有点困难。
(帮助例如打印出来#1
并看到它包含空格 catcode 的空间而不是字母 catcode 的预期空间)
由于您不需要可扩展性,因此使用显式空间标记(如\frase
)实现它的一种方法是使用\let\macro=⟨explicit space⟩
来获取和删除一个标记。
答案4
尝试用 TeX 语言实现一个简单的算法,我试图实现一个字符串长度函数(可能是令牌计数器函数)。
TeX 中的“字符串”概念很模糊。
在读取和标记 .tex 输入文件之后(参考 Donald E. Knuth 将 TeX 比作有眼睛和消化道的有机体),这是通过 TeX 的眼睛和嘴巴完成的——TeX 中的一切都与标记有关(显式字符标记、控制字标记、控制符号标记)。
随着宏的扩展(这是在 TeX 的喉咙中完成的),一切都与宏参数有关。宏参数可以为空,也可以由单个标记组成,也可以由多个标记组成。
您实现的机制基本上是递归调用宏\measurelen
来删除未定界的宏参数,并且 — 如果参数的第一个标记的含义不等于标记的含义\endstr
— 则增加计数器并再次调用自身。
您实现的机制实际上不计算标记,但计算未定界的宏参数。(但是,单个宏参数(算作一个项目)可以由多个字符标记组成……)
关于你的日常工作应该如何处理事情,出现了一些问题:
- 假设宏参数/组成“字符串”的标记集还包含匹配的花括号,即类别 1(开始组)和类别 2(结束组)的一些显式字符标记 — / 。这些标记通常可能不会产生可见的输出。它们可能会影响 TeX 运行时的分组/作用域。无论如何,这些标记是否应由 -routine 计数? 您希望使用 获得什么结果?应该是 20(括号计数)吗?应该是 14(括号不计数)吗?在执行某些操作时, further 可能有意义。例如,其中表示一组不属于类别 6(参数)的不可扩展显式字符标记,并表示未在 中出现的动词分隔符。
{1
}2
\strlen
\strlen{A {BCD{EFG}H } {}I J}
\strlen{#2}...\scantokens\expandafter{\string\verb*#1#2#1}
#2
#1
#2
- 假设宏参数/组成“字符串”的标记集还包含类别 6(参数)/哈希的显式字符标记。每个哈希字符标记是否应单独计算,还是应取两个连续的哈希字符标记并将其计为一个?后者在直接从参数定义临时宏时可能有意义,以便在扩展临时宏时将两个连续的哈希折叠为一个。
#6
\def
- 如何计算那些不产生单个字符但产生结果的标记,例如包含图形、绘制规则、分配给宏/寄存器、吞噬/删除后续标记?
- 如何计算那些不表示字形/字素实例(如数字或字母)绘制但表示(例如)应(不)发生换行符(例如像 ASCII 的 CR 和 LF;例如像 utf 8 的“单词连接符”)或应插入一些水平空格(space、nobreak-space、enspace、emspace)的字符?
- 如何计算产生单个重音字母的标记序列?
- 该机制是否适用于 8 位引擎和 XeTeX/LuaTeX 等 utf-8 引擎?如果可以,那么如何在 8 位引擎中处理多字节 utf-8 字符?在带有 LaTeX 的 8 位引擎中,utf-8 编码文件使用带有 utf-8 选项的 inputenc 包进行处理,因此在带有 LaTeX 的 8 位引擎中,多字节 utf-8 字符由多个标记表示,每个标记都来自对 .tex 输入文件进行 8 位字符标记,而不是多字节字符标记。
基本评论:
在 TeX 中,标记可以通过两种方式产生:
- 通过让 TeX 读取并标记 .tex 输入文件中的内容。
- 在扩展过程中,TeX 用形成替换文本的标记替换标记(例如,宏标记及其参数,例如,扩展 -expression 的结果
\csname..\endcsname
,例如,执行 -directive 的结果\the
)。
让我们看看你的代码:
\def\strlen{\begingroup\catcode`\ =11\strlenmain}
- 不要将空格切换到类别 11(字母)!如果这样做之后,构成 参数的标记
\strlenmain
将通过让 TeX 从 .tex-input-file 读取并标记化而产生,则 .tex-input-file 中的空格可以用作控制序列标记名称的组成部分。以同样的方式,\makeatletter
用于“告诉”TeX,从此以后@
可以成为通过让 TeX 从 .tex-input-file 读取并标记化而产生的控制序列标记名称的组成部分。例如,\strlen\frase %%%
的参数\strlenmain
将不是标记\frase
,而是标记。\frase⟨space⟩
- 切换空格字符的类别代码对那些不是通过 TeX 从 .tex-input-file 读取和标记而产生的材料没有帮助,而是由于扩展而产生的或由
\strlen
另一个宏传递给的材料。
例如,\strlen\frase
在扩展时,从 .tex-input-file 读取并标记\frase
替换文本的标记\frase
是在 定义时读取的,即在 的类别代码的临时更改尚未生效\frase
时,因此不会影响事物的标记方式。 例如,如果您使用, 的参数将是空格像往常一样被标记/作为类别 10(空格)和字符代码 32 的显式空格标记,因为 的参数(传递给)是在 的类别代码的临时更改尚未生效时被标记的,因此不会影响事物的标记方式。 因此,您仍然需要考虑存在类别 10(空格)和字符代码 32 的显式空格标记的情况。\strlen
\def\PassToSrlen#1{\strlen{#1}}
\PassToSrlen{A B C}
\strlen
A B C
\PassToSrlen
\strlen
\strlen
11
由于和之间没有任何内容\strlenmain
可能表明构成数字的数字序列已完成,因此 TeX 将扩展\strlenmain
以查明是否还有更多数字(如果有,则会引发有关没有有效类别代码的错误)。因此\strlenmain
将在评估如何完成类别代码分配的过程中进行顶层扩展,而不是在类别代码分配已经完成时进行顶层扩展。在这种情况下,这并不重要,因为扩展后的第一个标记\strlenmain
是\countdef
这显然不是数字。但在某些情况下,在形成数字的数字标记序列的同时进行扩展⟨数字⟩-quantity 没有终止可能会咬你,所以如果 TeX-⟨数字⟩-quantity 将由一系列明确的数字字符标记组成,最好用明确的空格标记终止该数字字符标记序列。TeX 的例程将删除空格标记,以收集明确的数字字符标记序列,如 TeX-⟨数字⟩-数量。
\def\strlenmain#1{%
\countdef\len=0
\len=0
\edef\arg{#1}
\def\measurelen##1 [... etc etc]
}
- 可能您不需要在每次执行时定义和。我
\len
建议 在组外/外部定义这些内容。\measurelen
\strlenmain
\strlen
\strlenmain
- 您使用
\edef
定义一个临时宏,该宏将提供参数的扩展。如果参数包含类别 6(参数)的单个井号标记,则这些标记将被视为表示宏的参数的东西,而定义\arg
宏时没有带参数文本的-assignment。因此,在这种情况下您将收到错误。如果参数包含多个这样的井号标记的序列,则在扩展( )时,每个第一个和每个第二个这样的井号标记都会折叠成一个井号标记。这种井号标记的折叠会影响标记的计数。使用最新的 TeX 引擎,您可以使用而不是定义临时宏。这样,井号就不会进入宏定义的替换文本,因此井号进入宏定义的替换文本所带来的问题就不复存在了。\arg
\edef
\arg
\expandafter\measurelen\arg\endstr
\expanded{...}
实际上,您的机制计算的是未限定的参数,而不是标记。
您希望获得什么结果\strlen{A {BCD{EFG}H } {}I J}
?
TeX 在扫描属于未定界参数的第一个标记时会丢弃显式空格标记(字符代码 32,类别 10)。例如,在\def\processtwo{(#1)/(#2)}
\processtwo{A} {B}
和\processtwo{A}{B}
和\processtwo A {B}
和\processtwo A{B}
和\processtwo{A} B
和\processtwo{A}B
和\processtwo A B
之后\processtwo AB
都产生相同的结果,即(A)/(B)
。( 的第一个和第二个未定界参数之间是否有显式空格标记\processtwo
并不重要。)
如果您的“字符串”包含不平衡的\else
或\fi
,那么这些可能会错误地匹配\ifx
所做的比较\measurelen
。
如果你真的想计算代币,无论
- 如果在扩展阶段以外的某个处理阶段,它们的处理产生的字符数量与标记数量不同——例如,.tex 输入 通常产生四个标记(、和一个明确的空格标记),这反过来又导致将单个字符放入输出文件/.pdf 文件中
\char65⟨space⟩
\char
6
5
A
- 计数结果可能会因使用的是 8 位 TeX 引擎还是 utf-8-TeX 引擎(如 XeTeX 或 LuaTeX)而有所不同
,同时有 e-TeX 扩展 ( \numexpr
)\expanded
可用,您可以尝试这样的事情:
\errorcontextlines=10000
\catcode`\@=11
%%=============================================================================
%% PARAPHERNALIA:
%% \UD@firstoftwo, \UD@secondoftwo, \UD@PassFirstToSecond, \UD@Exchange,
%% \UD@removespace, \UD@stopromannumeral, \UD@CheckWhetherNull,
%% \UD@CheckWhetherBrace, \UD@CheckWhetherLeadingExplicitSpace,
%% \UD@ExtractFirstArg
%%=============================================================================
\long\def\UD@firstoftwo#1#2{#1}%
\long\def\UD@secondoftwo#1#2{#2}%
\long\def\UD@PassFirstToSecond#1#2{#2{#1}}%
\long\def\UD@Exchange#1#2{#2#1}%
\UD@Exchange{ }{\def\UD@removespace}{}%
\chardef\UD@stopromannumeral=`\^^00%
%%-----------------------------------------------------------------------------
%% Check whether argument is empty:
%%.............................................................................
%% \UD@CheckWhetherNull{<Argument which is to be checked>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked is empty>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked is not empty>}%
%%
%% The gist of this macro comes from Robert R. Schneck's \ifempty-macro:
%% <https://groups.google.com/forum/#!original/comp.text.tex/kuOEIQIrElc/lUg37FmhA74J>
\long\def\UD@CheckWhetherNull#1{%
\romannumeral\expandafter\UD@secondoftwo\string{\expandafter
\UD@secondoftwo\expandafter{\expandafter{\string#1}\expandafter
\UD@secondoftwo\string}\expandafter\UD@firstoftwo\expandafter{\expandafter
\UD@secondoftwo\string}\expandafter\UD@stopromannumeral\UD@secondoftwo}{%
\expandafter\UD@stopromannumeral\UD@firstoftwo}%
}%
%%-----------------------------------------------------------------------------
%% Check whether argument's first token is a catcode-1-character
%%.............................................................................
%% \CheckWhetherBrace{<Argument which is to be checked>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked has a leading
%% explicit catcode-1-character-token>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked does not have a
%% leading explicit catcode-1-character-token>}%
\long\def\UD@CheckWhetherBrace#1{%
\romannumeral\expandafter\UD@secondoftwo\expandafter{\expandafter{%
\string#1.}\expandafter\UD@firstoftwo\expandafter{\expandafter
\UD@secondoftwo\string}\expandafter\UD@stopromannumeral\UD@firstoftwo}{%
\expandafter\UD@stopromannumeral\UD@secondoftwo}%
}%
%%-----------------------------------------------------------------------------
%% Check whether brace-balanced argument starts with a space-token
%%.............................................................................
%% \UD@CheckWhetherLeadingExplicitSpace{<Argument which is to be checked>}%
%% {<Tokens to be delivered in case <argument
%% which is to be checked> does have a
%% leading explicit space-token>}%
%% {<Tokens to be delivered in case <argument
%% which is to be checked> does not have a
%% a leading explicit space-token>}%
\long\def\UD@CheckWhetherLeadingExplicitSpace#1{%
\romannumeral\UD@CheckWhetherNull{#1}%
{\expandafter\UD@stopromannumeral\UD@secondoftwo}%
{%
% Let's nest things into \UD@firstoftwo{...}{} to make sure they are nested in braces
% and thus do not disturb when the test is carried out within \halign/\valign:
\expandafter\UD@firstoftwo\expandafter{%
\expandafter\expandafter\expandafter\UD@stopromannumeral
\romannumeral\expandafter\UD@secondoftwo
\string{\UD@CheckWhetherLeadingExplicitSpaceB.#1 }{}%
}{}%
}%
}%
\long\def\UD@CheckWhetherLeadingExplicitSpaceB#1 {%
\expandafter\UD@CheckWhetherNull\expandafter{\UD@firstoftwo{}#1}%
{\UD@Exchange{\UD@firstoftwo}}{\UD@Exchange{\UD@secondoftwo}}%
{\expandafter\expandafter\expandafter\UD@stopromannumeral
\expandafter\expandafter\expandafter}%
\expandafter\UD@secondoftwo\expandafter{\string}%
}%
%%-----------------------------------------------------------------------------
%% Extract first inner undelimited argument:
%%
%% \UD@ExtractFirstArg{ABCDE} yields {A}
%%
%% \UD@ExtractFirstArg{{AB}CDE} yields {AB}
%%
%% Due to \romannumeral-expansion the result is delivered after two
%% expansion-steps/after "hitting" \ExtractFirstArg with \expandafter
%% twice.
%%
%% \UD@ExtractFirstArg's argument must not be blank.
%%
%% Use frozen-\relax as delimiter for speeding things up.
%% I chose frozen-\relax because David Carlisle pointed out in
%% <https://tex.stackexchange.com/a/578877>
%% that frozen-\relax cannot be (re)defined in terms of \outer and cannot be
%% affected by \uppercase/\lowercase.
%%
%% \ExtractFirstArg's argument may contain frozen-\relax:
%% The only effect is that internally more iterations are needed for
%% obtaining the result.
%%
%%.............................................................................
\expandafter\expandafter\expandafter\UD@Exchange
\expandafter\expandafter\expandafter{%
\expandafter\expandafter\ifnum0=0\fi}%
{\long\def\UD@RemoveTillFrozenrelax#1#2}{{#1}}%
%
\expandafter\UD@PassFirstToSecond\expandafter{%
\romannumeral\expandafter
\UD@PassFirstToSecond\expandafter{\romannumeral
\expandafter\expandafter\expandafter\UD@Exchange
\expandafter\expandafter\expandafter{%
\expandafter\expandafter\ifnum0=0\fi}{\UD@stopromannumeral#1{}}%
}{%
\UD@stopromannumeral\romannumeral\UD@ExtractFirstArgLoop
}%
}{%
\long\def\UD@ExtractFirstArg#1%
}%
\long\def\UD@ExtractFirstArgLoop#1{%
\expandafter\UD@CheckWhetherNull\expandafter{\UD@firstoftwo{}#1}%
{\UD@stopromannumeral#1}%
{\expandafter\UD@ExtractFirstArgLoop\expandafter{\UD@RemoveTillFrozenrelax#1}}%
}%
%%=============================================================================
%% USER-MACRO:
%%=============================================================================
\long\def\strlen#1{%
%
\romannumeral\expandafter\tokencountloop\expanded{{#1}}{0}%
%
%{\edef\arg{#1}\expandafter}\expandafter
%\romannumeral\expandafter\tokencountloop\expandafter{\arg}{0}%
}%
%%=============================================================================
%% MAIN LOOP:
%%=============================================================================
\long\def\tokencountloop#1#2{%
\UD@CheckWhetherNull{#1}{\UD@stopromannumeral#2}{%
\expandafter\UD@PassFirstToSecond\expandafter{%
\number\numexpr#2+%
\UD@CheckWhetherBrace{#1}{%
2+% <-If you don't want curly braces to be counted, comment out.
\romannumeral\expandafter\expandafter\expandafter\tokencountloop\UD@ExtractFirstArg{#1}{0}%
}{1}%
\relax
}{%
\UD@CheckWhetherLeadingExplicitSpace{#1}{%
\expandafter\tokencountloop\expandafter{\UD@removespace#1}%
}{%
\expandafter\tokencountloop\expandafter{\UD@firstoftwo{}#1}%
}%
}%
}%
}%
\catcode`\@=12
O tamanho de "..." aberta é :
\strlen{Uma noite em Peron} %%%% outputs 18
\def\frase{Uma noite em Peron}
O tamanho de "\frase" é :
\strlen\frase %%%% outputs 18
\def\frase{Uma\ noite\ em\ Peron}
O tamanho de "\frase" é :
\strlen\frase %%%% outputs 18
O tamanho de "..." aberta é :
\strlen{Uma {{noite} }em Peron} %%%% outputs 22 which might make sense with things like
%%%% \strlen{#2}
%%%% \scantokens\expandafter{\string\verb*#1#2#1}
%%%% where #2 denotes a set of unexpandable explicit character tokens
%%%% and #1 denotes a verb-delimiter which does not occur within #2.
\end
假设 e-TeX 扩展 ( \numexpr
/ \detokenize
) 和\expanded
不可用,计数标记的工作将不是一件容易的事,而且不能仅靠可扩展的方法/例程来完成:
为了扩展您需要用于\edef
定义临时宏的参数。
- 定义临时宏本身并不是一个可扩展的方法。
- 在定义临时宏之前,参数中的每个哈希(类别 6(参数)的每个显式字符标记)都需要加倍。
我没有看到任何可靠的方法来检测类别 6(参数)的显式字符标记,而这种方法不需要任何 TeX 扩展。
(如果\detokenize
有的话,可以检查\string
应用于相关标记是否会产生单个标记,而应用\detokenize
(由于哈希加倍)会产生两个标记;但是,存在字符代码 32 和类别 6 的显式字符标记的极端情况需要特别注意……)