32

How could LaTeX replace the tokens <= by the command \leq efficiently?

Example 1: I have this code:

\[
   2x <= 4x - 2
\]

And I want to get after the compilation this:

enter image description here

Example 2:

\[
     q --> q 
\]

Output:

enter image description here

Example 3:

\[
    Q ==> Q 
\]

Output:

enter image description here

  • 1
    This seems like more of an editor question to me - though you could call another program to parse the file before compiling. What are you trying to achieve? – Chris H Aug 9 '13 at 12:03
  • 1
    @Chris force of habit – Sean Allred Aug 9 '13 at 12:08
  • 1
    possible things to look at: a) make < active and then test for = as the next input token; b) if both glyphs are in the same font (they're not, in computer modern), try a ligature (but i don't remember whether ligatures work in math mode; i think not). i really don't recommend either method, but by investigating them, you'd probably learn something about tex's innards. – barbara beeton Aug 9 '13 at 12:21
  • 2
    Well your 3rd example changes things a bit (assuming you want \geq==> anyway. – Chris H Aug 9 '13 at 12:27
  • 2
    Considering what Chris pointed out, this idea in general may have unintended and hard-to-debug side-effects. – Sean Allred Aug 9 '13 at 13:36
45
+100

I assume, the replacements should be done in math mode only. Then the starting characters can be made active via a special value "8000 for \mathcode. The characters behave in text mode as usual, but they became special in math mode.

The following example document provides parsers for the following shorthands:

<< : \ll (latexsym/amsmath)
<> : \neq
<= : \leq
<== : \Leftarrow
<=> : \Leftrightarrow
<-- : \leftarrow
<-> : \leftrightarrow
>> : \gg (latexsym/amsmath)
>= : \geq
--> : \rightarrow
-+ : \pm
+- : \mp
... : \dots (amsmath)
== : \equiv
=. : \doteq
==> : \Rightarrow
=( : \subseteq (latexsym/amsmath)
=) : \supseteq (latexsym/amsmath)
=[ : \sqsubseteq (latexsym/amsmath)
=] : \sqsubseteq (latexsym/amsmath)

Example file:

\documentclass{article}

%\usepackage{latexsym}
% * because of \gg, \ll, \subseteq, \supseteq, \sqsubseteq, \sqsupseteq
% * not needed if amsmath is loaded

\usepackage{amsmath}% because of \dots

\makeatletter
% LaTeX's \@ifnextchar gobbles spaces, therefore
% \msh@ifnextchar is defined that keeps spaces
\newcommand*{\msh@ifnextchar}[3]{%
  \def\msh@temp{\msh@@ifnextchar{#1}{#2}{#3}}%
  \futurelet\msh@token\msh@temp
}
\newcommand*{\msh@@ifnextchar}[1]{%
  \ifx\msh@token#1%
    \expandafter\@firstoftwo
  \else
    \expandafter\@secondoftwo
  \fi
}

% <<
% <>
% <=
% <==
% <=>
% <--
% <->
% >>
% >=
% -->
% -+
% +-
% ...
% ==
% =.
% ==>
% =(
% =)
% =[
% =]

% Commands that take the original meanings of the special characters
\mathchardef\msh@code@less=\mathcode`\<\relax
\mathchardef\msh@code@greater=\mathcode`\>\relax
\mathchardef\msh@code@minus=\mathcode`\-\relax
\mathchardef\msh@code@plus=\mathcode`\+\relax
\mathchardef\msh@code@equal=\mathcode`\=\relax
\mathchardef\msh@code@dot=\mathcode`\.\relax

% Macro \resetmathshorthands resets the original meaning of the
% special characters by resetting their \mathcode values
\@ifdefinable{\resetmathshorthands}{%
  \edef\resetmathshorthands{%
    \mathcode\number`\<=\msh@code@less
    \mathcode\number`\>=\msh@code@greater
    \mathcode\number`\-=\msh@code@minus
    \mathcode\number`\+=\msh@code@plus
    \mathcode\number`\.=\msh@code@dot
    \mathcode\number`\==\msh@code@equal
  }%
}

% Macro \setmathshorthands activates and defines the special
% characters
\begingroup
  \catcode`\<=\active
  \catcode`\>=\active
  \catcode`\-=\active
  \catcode`\+=\active
  \catcode`\.=\active
  \catcode`\==\active
  \edef={\string=}%
  \@ifdefinable{\setmathshorthands}{%
    \xdef\setmathshorthands{%
      \mathcode\number`\<="8000 %
      \mathcode\number`\>="8000 %
      \mathcode\number`\-="8000 %
      \mathcode\number`\+="8000 %
      \mathcode\number`\.="8000 %
      \mathcode\number`\=="8000 %
      \let\noexpand<\noexpand\msh@less
      \let\noexpand>\noexpand\msh@greater
      \let\noexpand-\noexpand\msh@minus
      \let\noexpand+\noexpand\msh@plus
      \let\noexpand.\noexpand\msh@dot
      \let\noexpand=\noexpand\msh@equal
    }%
  }%
\endgroup

% The parsers for the math shorthands follow:

% <<
% <>
% <=
% <==
% <=>
% <--
% <->
\newcommand*{\msh@less}{%
  \msh@ifnextchar<{%
    \ll\@gobble
  }{%
    \msh@ifnextchar>{%
      \neq\@gobble
    }{%
      \msh@ifnextchar={%
        \expandafter\msh@less@equal\@gobble
      }{%
        \msh@ifnextchar-{%
          \expandafter\msh@less@minus\@gobble
        }{%
          \msh@code@less
        }%
      }%
    }%
  }%
}
\newcommand*{\msh@less@equal}{%
  \msh@ifnextchar={%
    \Leftarrow\@gobble
  }{%
    \msh@ifnextchar>{%
      \Leftrightarrow\@gobble
    }{%
      \leq
    }%
  }%
}
\newcommand*{\msh@less@minus}{%
  \msh@ifnextchar-{%
    \leftarrow\@gobble
  }{%
    \msh@ifnextchar>{%
      \leftrightarrow\@gobble
    }{%
      \msh@code@less\msh@code@minus
    }%
  }%
}

% >>
% >=
\newcommand*{\msh@greater}{%
  \msh@ifnextchar>{%
    \gg\@gobble
  }{%
    \msh@ifnextchar={%
      \geq\@gobble
    }{%
      \msh@code@greater
    }%
  }%
}

% -->
% -+
\newcommand*{\msh@minus}{%
  \msh@ifnextchar-{%
    \expandafter\msh@minus@minus\@gobble
  }{%
    \msh@ifnextchar+{%
      \mp\@gobble
    }{%
      \msh@code@minus
    }%
  }%
}
\newcommand*{\msh@minus@minus}{%
  \msh@ifnextchar>{%
    \rightarrow\@gobble
  }{%
    \msh@code@minus\msh@code@minus
  }%
}

% +-
\newcommand*{\msh@plus}{%
  \msh@ifnextchar-{%
    \pm\@gobble
  }{%
    \msh@code@plus
  }%
}

% ...
\newcommand*{\msh@dot}{%
  \msh@ifnextchar.{%
    \expandafter\msh@dot@dot\@gobble
  }{%
    \msh@code@dot
  }%
}
\newcommand*{\msh@dot@dot}{%
  \msh@ifnextchar.{%
    \expandafter\msh@dot@dot@dot\@gobble
  }{%
    \msh@code@dot
    \msh@code@dot
  }%
}
\newcommand*{\msh@dot@dot@dot}{%
  % remove space after "...", because a space would
  % disturb \dots' auto-positioning feature.
  \expandafter\dots\romannumeral-`\x
}

% ==
% =.
% ==>
% =(
% =)
% =[
% =]
\newcommand*{\msh@equal}{%
  \msh@ifnextchar={%
    \expandafter\msh@equal@equal\@gobble
  }{%
    \msh@ifnextchar.{%
      \doteq\@gobble
    }{%
      \msh@ifnextchar({%
        \subseteq\@gobble
      }{%
        \msh@ifnextchar){%
          \supseteq\@gobble
        }{%
          \msh@ifnextchar[{%
            \sqsubseteq\@gobble
          }{%
            \msh@ifnextchar]{%
              \sqsupseteq\@gobble
            }{%
              \msh@code@equal
            }%
          }%
        }%
      }%
    }%
  }%
}
\newcommand*{\msh@equal@equal}{%
  \msh@ifnextchar>{%
    \Rightarrow\@gobble
  }{%
    \equiv
  }%
}
\makeatother

% Activate math shorthands in the math modes
\everymath{\setmathshorthands}
\everydisplay{\setmathshorthands}

\begin{document}
\centering
\newcommand*{\test}[1]{%
  $#1$%
  \[#1\]%
}
\test{a << b < c <= d >= e > f >> g}
\test{a <> b = c =. d == e}
\test{a <== b <-- c <-> d <=> e --> f ==> g}
\test{a +- b = -(-a -+ +b)}
\test{a, ..., z <> a + ...+ z}
\test{a =( b =) c =[ e =] f}
\end{document}

Result

Remarks:

  • Macro \msh@ifnextchar looks up the next token. In opposite to LaTeX's \@ifnextchar it does not gobble spaces. For example, this is important for a + -b (a - b) that is different from a +- b (a ± b).

  • ... are replaced by \dots of package amsmath, because it has an auto-detection feature. The vertical position of the dots depends on the next token. For example, in a comma separated list, \dots become \ldots; if the next token is a +, then \cdots is used.

    Spaces are gobbled after a command token like \dots, but not after other characters like .... Therefore \msh@dot@dot@dot removes a following space before calling \dots. Otherwise \dots would see the space and become \ldots, even, if the token after the space is a +.

  • The suggested _C for \subseteq looks too ambiguous too me, because it looks like a normal subscript C. Also there is not a good ASCII letter for use in the shorthand of \supseteq. Therefore I have implemented the shorthands =(, =) and the pair =[, =] for the square forms.

    If round or square parentheses follows the equal sign, then the shorthand replacement can be prevented by a space, e.g. a = (b + c).

  • Undocumented feature: < > behaves differently than <> thanks to space-aware ifnextchar. – Qrrbrbirlbel Aug 9 '13 at 18:19
  • @Qrrbrbirlbel: Yes (poorly documented). It is also important for a + -b = a - b that is different from a +- b = a \pm b; answer updated. – Heiko Oberdiek Aug 9 '13 at 18:20
  • How could I do that three dots (...) are replaced by \cdots and _C tokens by \subseteq? – David Aug 11 '13 at 9:24
  • 6
    Will you provide a new package? – Marco Daniel Aug 11 '13 at 10:16
  • 1
    @David: Done with some changes: \dots of package amsmath is more powerful than \cdots. Because _C looks too ambiguous too me, I have implemented the pairs =(/=) and =[/=]. – Heiko Oberdiek Aug 11 '13 at 10:16
13

The following is taken partially from Define a command so that it is only active within the document environment:

enter image description here

\documentclass{article}
\makeatletter
\AtBeginDocument{
  \begingroup\lccode`~=`<
  \lowercase{\endgroup\def~{\@ifnextchar={\leq\@gobble}{<}}}%
  \catcode`<=\active
}
\makeatother
\begin{document}
$2x <= 4x - 2 \leq y$
\end{document}

But using an editor's search-and-replace seems just as appropriate.

12
+200

This is interesting problem which can be solved by more compact macros than in accepted answer:

\long\def\isnextchar#1#2#3{\begingroup\toks0={\endgroup#2}\toks1={\endgroup#3}%
   \let\tmp=#1\futurelet\next\isnextcharA
}
\def\isnextcharA{\the\toks\ifx\tmp\next0\else1\fi\space}

\def\skipnext#1#2{#1}    
\def\trynext#1{\trynextA#1\relax\relax}
\def\trynextA#1#2\relax#3\relax#4#5{%
   \ifx\relax#2\relax \def\next{\isnextchar#1{\skipnext{#4}}{#5#3}}\else
      \def\next{\isnextchar#1{\skipnext{\trynextA#2\relax#3#1\relax#4{#5}}}{#5#3}}\fi
   \next
}
\def\mspecdefA#1#2#3 : #4{\ifx#2\undefined
   \def#2{\trynext{#3}#4{#1}}\else
   \toks0={\trynext{#3}#4}\toks1=\expandafter{#2}%
   \edef#2{\the\toks0{\the\toks1}}\fi
}
\def\mspecdef#1{%
   \expandafter\ifx\csname m:#1\endcsname\relax
      \expandafter\mathchardef\csname m:#1\endcsname=\mathcode`#1
   \fi
   \mathcode`#1="8000 
   \begingroup \lccode`~=`#1 
   \lowercase{\endgroup\expandafter\mspecdefA\csname m:#1\endcsname~}%
}

\mspecdef << : \ll
\mspecdef <> : \neq
\mspecdef <= : \leq
\mspecdef <== : \Leftarrow
\mspecdef <=> : \Leftrightarrow
\mspecdef <-- : \leftarrow
\mspecdef <-> : \leftrightarrow
\mspecdef >> : \gg
\mspecdef >= : \geq
\mspecdef --> : \rightarrow
\mspecdef -+ : \pm
\mspecdef +- : \mp
\mspecdef ... : \dots
\mspecdef == : \equiv
\mspecdef =. : \doteq
\mspecdef ==> : \Rightarrow
\mspecdef =( : \subseteq
\mspecdef =) : \supseteq
\mspecdef =[ : \sqsubseteq
\mspecdef =] : \sqsubseteq

test:

$$ a << b < c <= d >= e > f >> g $$
$$ a <> b = c =. d == e $$
$$ a <== b <-- c <-> d <=> e --> f ==> g $$
$$ a +- b = -(-a -+ +b) $$
$$ a, ..., z <> a + ...+ z $$
$$ a =( b =) c =[ e =] f $$

\bye

The result is the same as in accepted answer.

Edit How it works? When we do

\mspecdef ax : \U    \mspecdef axy : \V    \mspecdef abcd : \W 

then the a character is set as math-active (i.e. \matcode is "8000) and it is defined as

\def a{\trynext{bcd}\W{\trynext{xy}\V{\trynext{x}\U{normal a}}}}

This macro does test if the following string is bcd (using repeatedly called \isnextchar). If it is true then next part of the macro is skipped and \W is processed. Else next part of the macro is processed. This means, that xy is tested. If fails then x is tested and if fails then normal a is printed.

We can do this only with TeX macros at primitive level without any non-TeX tools like lua code, without any obscure solutions like expl3.

  • \pm and \mp should be interchanged, I think. – Manuel May 25 '16 at 19:53
  • @Manuel I copied only the introduction table from accepted answer and added the \mspecdef in each line. – wipet May 25 '16 at 19:57
  • Well, his implementation do them the other way around. In any case, I think it's common for +- to be plus-minus \pm and -+ minus-plus \mp. – Manuel May 25 '16 at 20:04
  • @Manuel This is not the main point of my interest. You can correct this in accepted answer and in my answer too. I aimed at something different: that this general feature can be done by simple TeX macros. – wipet May 26 '16 at 4:08
  • It was a minor thing, don't worry, I like this answer and upvoted it. – Manuel May 26 '16 at 8:14
10

While this solution doesn't maintain the air of 'mathiness,' it could be extremely useful for aspiring literate programmers. The bonus? No TeX hackery (that you see) and thus easily customizable!

Using the listings package:

\documentclass{article}
\usepackage{listings,latexsym}

% Disclaimer: I don't actually know Pascal.  It's on my todo-list.
\lstset{language=Pascal}

\begin{document}
\begin{lstlisting}[literate={<=}{{$\leq$}}1
                            {>=}{{$\geq$}}1
                            {!=}{{$\neq$}}1
                            {<==}{{$\Longleftarrow$}}2 % note this width was increased
                            {==>}{{$\Longrightarrow$}}2
                            {<<}{{$\ll$}}1 % the only need for `latexsym`
                            {+-}{{$\pm$}}1
                            {in}{{$\in$}}1] % and so on
  if x <= 5 do stuff();
  if x in {4 +- 2}
  stuff << st
\end{lstlisting}
\end{document}

enter image description here

10

Here's a LuaLaTeX-based solution. It defines a Lua function, named do_subs, that performs substitutions on all the following two-letter and three-letter character combinations:

<== <=> ==> <= >= << >> <-- -->  -+ +- ... == <> =. =( =) =[ =]

By assigning this function to the "process_input_buffer" callback, the function acts as a pre-processor on the input stream -- TeX itself only ever sees and processes the corresponding math-mode macros.

Incidentally, it's assumed that these character combinations -- other than ... -- will only ever occur in math mode. Do let me know if this assumption isn't warranted. (... gets replaced with \dots, and \dots can occur in both text and math mode.)

A comment on the Lua code: Since the characters ., -, +, (, ), [, and ] are "special" in Lua search strings, they need to be escaped in order to lose the special meaning; this is done by prefixing % symbols to them. Fo instance, as . is the Lua "magic" character for any character, the pattern search string for ... must be written as %.%.%.. (This is also why the code shown below encases the Lua function in a luacode environment; in such an environment, % is not treated as a comment character.)

enter image description here

% !TEX TS-program = lualatex
\documentclass{article}
\usepackage{amsmath} % for context-sensitive handling of `\dots` macro

\usepackage{luacode}
\begin{luacode}
function do_subs ( buff )
  buff = string.gsub ( buff , "<==",   "\\Leftarrow ")
  buff = string.gsub ( buff , "<=>",   "\\Leftrightarrow ")
  buff = string.gsub ( buff , "<=",    "\\leq ")
  buff = string.gsub ( buff , "<<",    "\\ll ")
  buff = string.gsub ( buff , "<%-%-", "\\leftarrow ")
  buff = string.gsub ( buff , "<%->",  "\\leftrightarrow ")
  buff = string.gsub ( buff , ">>",    "\\gg ")
  buff = string.gsub ( buff , ">=",    "\\geq ")
  buff = string.gsub ( buff , "%-%->", "\\rightarrow ")
  buff = string.gsub ( buff , "%-%+",  "\\mp ")
  buff = string.gsub ( buff , "%+%-",  "\\pm ")
  buff = string.gsub ( buff , "%.%.%.","\\dots ")
  buff = string.gsub ( buff , "=%.",   "\\doteq ")
  buff = string.gsub ( buff , "==>",   "\\Rightarrow ")
  buff = string.gsub ( buff , "==",    "\\equiv ")
  buff = string.gsub ( buff , "<>",    "\\neq ")
  buff = string.gsub ( buff , "=%(",   "\\subseteq ")
  buff = string.gsub ( buff , "=%)",   "\\supseteq ")
  buff = string.gsub ( buff , "=%[",   "\\sqsubseteq ")
  buff = string.gsub ( buff , "=%]",   "\\sqsupseteq ")
  return buff
end
\end{luacode}
\AtBeginDocument{\directlua{luatexbase.add_to_callback ( 
    "process_input_buffer" , do_subs, "do_subs" )}}

\begin{document}
$2x <= 4x - 2$

$<== <=> ==> <= >= << >> <-- -->  -+ +- ... == <> =. =( =) =[ =] $

\bigskip
$a << b < c <= d >= e > f >> g$

$a <> b = c =. d == e$

$a <== b <-- c <-> d <=> e --> f ==> g$

$a +- b = -(-a -+ +b)$

$a, ..., z <> a + ...+ z$

$a =( b =) c =[ e =] f$
\end{document}
  • This matches everywhere, not only in mathmode, right? – Henri Menke May 25 '16 at 21:08
  • @HenriMenke - That's right. I noted in the answer that it's assumed that these character combinations will only ever occur in math mode. (However, it's OK for \dots to occur in text mode as well.) It's not too much extra work, actually, to add a test to check if the character combos occur inside or outside of math mode -- and to perform the substitutions only if a character combination occurs in math mode. – Mico May 25 '16 at 21:12
  • Is there no callback like process_math_list or so? – Henri Menke May 25 '16 at 21:23
  • Ah, there is mlist_to_hlist. Probably one could hook somehow into that. I hope that nothing has been boxed at that point. – Henri Menke May 25 '16 at 21:25
9

ConTeXt has this funny asciimath module, which has unfortunately completely different rules (which are highly inconsistent), but it still fits the general task though.

Probably there is something about it in Hans' article “When to stop ...” which are the proceedings of his talk at TUG 2015, where he also mentioned asciimath. (I've only seen the talk, the article is not free yet).

\usemodule[asciimath]

\starttext
$\asciimath{a << b < c <= d >= e > f >> g}$

$\asciimath{a <> b = c =. d == e}$

$\asciimath{a <== b <-- c <-> d <=> e --> f ==> g}$

$\asciimath{a +- b = -(-a -+ +b)}$

$\asciimath{a, ..., z <> a + ...+ z}$

$\asciimath{a =( b =) c =[ e =] f}$
\stoptext

enter image description here

Something similar can however be achieved in LaTeX using expl3. Here we can also implement the correct substitutions.

With some trickery we can even make it automatic. I do not recommend this though. There are surely corner cases. For example, it does not recurse subgroups. To see what this means, try \test{a =( {b =) c =[ e} =] f} with the example below.

\documentclass{article}
\usepackage{amsmath}
\usepackage{xparse}

\ExplSyntaxOn

\seq_new:N \l_math_subs_seq

\cs_new_protected:Npn \math_add_sub:nn #1 #2
 {
   \seq_put_right:Nn \l_math_subs_seq { { #1 } { #2 } }
 }

\cs_new_protected:Npn \math_ascii_sub:n #1
 {
  \tl_set:Nn \l_tmpa_tl { #1 }
  \seq_map_inline:Nn \l_math_subs_seq
   {
    \tl_replace_all:Nnn \l_tmpa_tl ##1
   }
  \tl_use:N \l_tmpa_tl
 }

\cs_new_protected:Npn \math_grabinline:w #1 $
 {
  \math_ascii_sub:n { #1 } $
 }

\cs_new_protected:Npn \math_grabdisplay:w #1 \]
 {
  \math_ascii_sub:n { #1 } \]
 }

% Set substitutions (be careful with order!)
% Three letter sequences first
\math_add_sub:nn { <== } { \Leftarrow }
\math_add_sub:nn { <=> } { \Leftrightarrow }
\math_add_sub:nn { <-- } { \leftarrow }
\math_add_sub:nn { <-> } { \leftrightarrow }
\math_add_sub:nn { --> } { \rightarrow }
\math_add_sub:nn { ==> } { \Rightarrow }
\math_add_sub:nn { ... } { \dots }
% Then two letter sequences
\math_add_sub:nn { << } { \ll  }
\math_add_sub:nn { <> } { \neq }
\math_add_sub:nn { <= } { \leq }
\math_add_sub:nn { >> } { \gg  }
\math_add_sub:nn { >= } { \geq }
\math_add_sub:nn { -+ } { \mp }
\math_add_sub:nn { +- } { \pm }
\math_add_sub:nn { == } { \equiv }
\math_add_sub:nn { =. } { \doteq }
\math_add_sub:nn { =( } { \subseteq }
\math_add_sub:nn { =) } { \supseteq }
\math_add_sub:nn { =[ } { \sqsubseteq }
\math_add_sub:nn { =] } { \sqsubseteq }

% Enable substitutions for $...$ and \[...\]
\everymath { \math_grabinline:w }
\tl_put_right:Nn \[ { \math_grabdisplay:w }

\ExplSyntaxOff

\begin{document}
\centering
\newcommand*{\test}[1]{%
  $#1$%
  \[#1\]%
}
\test{a << b < c <= d >= e > f >> g}
\test{a <> b = c =. d == e}
\test{a <== b <-- c <-> d <=> e --> f ==> g}
\test{a +- b = -(-a -+ +b)}
\test{a, ..., z <> a + ...+ z}
\test{a =( b =) c =[ e =] f}
\end{document}

enter image description here

  • You have to be careful to change before ==> and then ==. If you change the other way around you get unwanted symbols. An option is to change them with a space { ~==~ } and { ~==>~ } (and the same for the rest), that way there's no problem. – Manuel May 25 '16 at 19:51
  • @Manuel The additional spaces might gain you some safety, but do not rectify the issue that this in general a bad idea. – Henri Menke May 25 '16 at 20:22
  • In the screenshot, each line appears to be in duplicate. Is this intentional? – Mico May 25 '16 at 21:46
  • @Mico Yes: \newcommand*{\test}[1]{$#1$\[#1\]} (like in Heiko's answer) – Henri Menke May 25 '16 at 21:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.