TeX 语言中的 strlen

TeX 语言中的 strlen

尝试在 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}\strlenA 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⟩\char65A
  • 计数结果可能会因使用的是 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 的显式字符标记的极端情况需要特别注意……)

相关内容