11

Say I wanted to write a macro like this:

\makelinesegment{1:1-1:2-2:2}

And have it become something like:

\draw (1,1) -- (1,2) -- (2,2);

The question is, how to go about parsing that 1:1-1:2-2:2 as a single option.

In another language you could parse it by doing:

input.split('-').each(fn(frag){
  let parts = frag.split(':')
  # draw (parts[0], parts[1]), ...
})

Wondering how would you go about doing this in LaTeX.

  • What about negative coordinates? 1:1--1:-2-2:2 seems a bit weird. Maybe another character to separate fields? – Phelype Oleinik Mar 22 '18 at 11:40
  • Just whipped together a quick example. I basically just want to know how to implement parsing with this as a demo even though these edge cases are there :p – Lance Pollard Mar 22 '18 at 11:42
8

This is an expl3 implementation.

\lineparser_make_line should be quite self-explaining, it splits the parameter at -, does a map to transform a:b to (a,b) and then calls \draw with the sequence joined with --s. The splitting of the coordinate get a bit ugly because : is a letter in \ExplSyntaxOn\ExplSyntaxOff blocks, so if : would be used as a argument delimiter directly it would have the wrong catcode.

\documentclass{article}

\usepackage{xparse,expl3,tikz}

\ExplSyntaxOn
% We need
%\cs_new:Npn\__lineparser_transform_coords:w#1:#2\q_mark}{
% but the ":" is changed by\ExplSyntaxOn, so we have to use a trick to use the normal colon in the parameter
\use:x{\cs_new:Npn\exp_not:N\__lineparser_transform_coords:w##1\c_colon_str##2\exp_not:N\q_mark}{
  (#1,#2)
}
\seq_new:N\__lineparser_coordinates_seq
\seq_new:N\__lineparser_coordinates_transformed_seq
\cs_new:Nn\lineparser_make_line:n{
  \seq_set_split:Nnn\__lineparser_coordinates_seq{ - }{#1}
  \seq_set_map:NNn
    \__lineparser_coordinates_transformed_seq
    \__lineparser_coordinates_seq
    {
      \__lineparser_transform_coords:w##1\q_mark
    }
  \draw
    \seq_use:Nn
      \__lineparser_coordinates_transformed_seq
      { -- }
    ;
}
\NewDocumentCommand\makelinesegment{m}{
  \lineparser_make_line:n{#1}
}
\ExplSyntaxOff

\begin{document}
\pagenumbering{gobble}

\begin{tikzpicture}
  \makelinesegment{1:1-1:2-2:2}
\end{tikzpicture}

\end{document}

Similar to Phelype Oleiniks answer there are special functions to deal with comma lists, so if \makelinesegment{1:1,1:2,2:2} is to be parsed, you can for example use

\documentclass{article}

\usepackage{xparse,expl3,tikz}

\ExplSyntaxOn
% We need
%\cs_new:Npn\__lineparser_transform_coords:w#1:#2\q_mark}{
% but the ":" is changed by\ExplSyntaxOn, so we have to use a trick to use the normal colon in the parameter
\use:x{\cs_new:Npn\exp_not:N\__lineparser_transform_coords:w##1\c_colon_str##2\exp_not:N\q_mark}{
  (#1,#2)
}
\seq_new:N\__lineparser_coordinates_seq
\seq_new:N\__lineparser_coordinates_transformed_seq
\cs_new:Nn\lineparser_make_line:n{
  \seq_set_from_clist:Nn\__lineparser_coordinates_seq{#1}% <-- The change
  \seq_set_map:NNn
    \__lineparser_coordinates_transformed_seq
    \__lineparser_coordinates_seq
    {
      \__lineparser_transform_coords:w##1\q_mark
    }
  \draw
    \seq_use:Nn
      \__lineparser_coordinates_transformed_seq
      { -- }
    ;
}
\NewDocumentCommand\makelinesegment{m}{
  \lineparser_make_line:n{#1}
}
\ExplSyntaxOff

\begin{document}
\pagenumbering{gobble}

\begin{tikzpicture}
  \makelinesegment{1:1,1:2,2:2}
\end{tikzpicture}

\end{document}

This is a bit longer and more verbose than the primitive TeX solution by Phelype Oleinik, but I think it is easier to understand.

11

Version 1:

My answer is using mostly TeX primitives. I know there are other ways to do it, especially using expl3, but I'm not there yet, so...

I defined the main macro \makelinesegment that will take one argument, in the example, 1:1-1:2-2:2.

\makelinesegment appends will pass its argument to \@coordpair, which takes as argument something in the form x:y-. \@coordpair will take the x and y coordinates and add them to \this@path as (x,y)--.

When \@coordpair reaches the end of the input (i.e. finds \empty:\empty-), it finishes and return to \makelinesegment.

Up to now we have (1,1)--(1,2)--(2,2)--. \makelinesegment then calls \gobblemm to remove the final -- from \this@path.

Finally, \makelinesegment calls \draw\this@path;.

\documentclass{article}

\usepackage{tikz}

\makeatletter
\def\makelinesegment#1{%
  \def\this@path{}%
  \@coordpair#1-\empty:\empty-\@empty%
  \expandafter\gobblemm\this@path;
  \draw\this@path;
}
\def\gobblemm#1--;{\edef\this@path{#1}}
\def\@coordpair#1:#2-#3\@empty{%
  \ifx#1\empty
  \else
    \edef\this@path{\this@path(#1,#2)--}%
    \expandafter\@coordpair%
  \fi
  #3\@empty%
}
\makeatother

\begin{document}
\pagenumbering{gobble}

\begin{tikzpicture}
  \makelinesegment{1:1-1:2-2:2}
\end{tikzpicture}

\end{document}

Version 2:

As I said in the comments, this notation may (and will) cause ambiguity when using negative coordinates. So I propose a slight change: replace the - that separates the coordinate pairs by ,. Like this: 1:1,1:2,2:2.

The solution then becomes even simpler, for we can use LaTeX's \@for to iterate on a comma-separated list.

Now the \makelinesegment macro just passes its argument to \@for, who splits the coorinate pairs and passes them to \@pair. \@pair then appends the coordinate pair (x,y)-- to \this@list.

And the rest is like in the first version; \gobblemm removes the last -- and \makelinesegment calls \draw\this@path;.

\documentclass{article}

\usepackage{tikz}

\makeatletter
\def\makelinesegment#1{%
  \def\this@path{}%
  \@for\@pair:=#1\do{%
    \expandafter\splitpair\@pair\@empty%
  }%
  \expandafter\gobblemm\this@path;%
  \draw\this@path;
}
\def\gobblemm#1--;{\edef\this@path{#1}}
\def\splitpair#1:#2\@empty{%
  \edef\this@path{\this@path(#1,#2)--}%
}
\makeatother

\begin{document}
\pagenumbering{gobble}

\begin{tikzpicture}
  \makelinesegment{1:1,1:2,2:2}
\end{tikzpicture}

\end{document}
  • Ah that's cool you can pass a string like that to \draw. – Lance Pollard Mar 22 '18 at 13:00
  • 1
    This also helps tex.stackexchange.com/questions/188609/…. Basically, tikz does a ton of work to create its custom parser. – Lance Pollard Mar 22 '18 at 13:05
  • @LancePollard This, for example, is the reason that it's essential to have a semicolon after every TikZ command. They are defined (over simplifying things) with \def\path#1;{...}. When you forget the semicolon, it doesn't know what to do. Indeed, you can learn a lot (or get even more confused) about input parsing with TeX primitives by looking at how TikZ parses its commands. – Phelype Oleinik Mar 22 '18 at 14:36
6

There was no test file provided (again) so I borrowed one from one of the other answers. For such a simple transformation you don't really need any extra parsing code just define the expansion to happen inline:

enter image description here

\documentclass{article}

\usepackage{tikz}

\def\makelinesegment#1{\draw\xmakelinesegment#1-\empty;}
\def\xmakelinesegment#1:#2-#3{%
(#1,#2)
\ifx\empty#3\else--\expandafter\xmakelinesegment\fi
#3}


\begin{document}
\pagenumbering{gobble}

\begin{tikzpicture}
 \makelinesegment{1:1-1:2-2:2}
\end{tikzpicture}

\end{document}
5

An approach using pure expansion to allow the command to be inserted into a drawing but without 'hiding' the core TikZ commands:

\documentclass{article}
\usepackage{tikz}
\makeatletter
\newcommand*{\makelinesegment}[1]{%
  \makelinesegment@loop#1--\q@stop
}
\newcommand*{\makelinesegment@loop}{}
\def\makelinesegment@loop#1-#2-#3\q@stop{%
  \if\relax\detokenize{#1}\relax
    \expandafter\makelinesegment@end
  \else
    \makelinesegment@auxi{#1}%
    \if\relax\detokenize{#2}\relax
      \expandafter\expandafter\expandafter\makelinesegment@end
    \else
      --%
      \expandafter\expandafter\expandafter\makelinesegment@auxiii
    \fi
  \fi
    {#2}{#3}%
}
\newcommand*{\makelinesegment@auxi}[1]{%
  \makelinesegment@auxii#1:0:0:0\q@stop
}
\newcommand*{\makelinesegment@auxii}{}
\def\makelinesegment@auxii#1:#2:#3\q@stop{(#1,#2)}
\newcommand*{\makelinesegment@auxiii}[2]{\makelinesegment@loop#1-#2\q@stop}
\newcommand*{\makelinesegment@end}[2]{}
\makeatother
\begin{document}

\tikz\draw\makelinesegment{1:1-1:2-2:2};

\end{document}

The code is pretty standard for TeX: set up a delimited argument where we make sure that there will always be the matching token in the input stream, even if it's one we've supplied.

3

As you want to adapte your parser by yourself, here is (with @Manuel help) a simple but evolutive solution using expl3 and regular expression. This one solves the case mentionned by Phelype Oleinik : 1:1--1:-2-2:2 of 2 following -.

\documentclass{article}

\usepackage{tikz,xparse}

\ExplSyntaxOn

\tl_new:N \l_bob_func_tl

\NewDocumentCommand \makelinesegment { m }
 {
  \pgfextra
  \tl_set:Nn \l_bob_func_tl { #1 }
  \regex_replace_all:nnN { ([0-9])- } { \1)--( } \l_bob_func_tl
  \regex_replace_all:nnN { : } { , } \l_bob_func_tl
  \exp_last_unbraced:NNV
  \endpgfextra
  (\l_bob_func_tl)
 }

\ExplSyntaxOff

\begin{document}

\tikz\draw\makelinesegment{1:1--1:2-2:2};

\end{document}
3

This may be too basic which is why the other answers haven't mentioned it, but IMO it's worth pointing out explicitly: if you are not wedded to the LaTeX syntax with braces (i.e. you don't necessarily have to type exactly \makelinesegment{1:1-1:2-2:2} in your input), then TeX itself has a rudimentary way of doing this “parsing”, namely by matching tokens.

For example, if you know you'll always have three arguments, separated by -, then you can just do:

\def\makelinesegment#1-#2-#3!{...definition goes here...}

after which writing

\makelinesegment 1:1-1:2-2:2!

will result in the macro \makelinesegment getting expanded with 1:1 as #1, 1:2 as #2, and 2:2 as #3. (The reason for the trailing ! (which could be any delimiter) is so that #3 is 2:2 and not just the first token, 2.)

So the following will work (compile the document below with pdftex):

\input tikz

\def\makelinesegment#1-#2-#3!{\draw \point#1 -- \point#2 -- \point#3;}
\def\point#1:#2{(#1, #2)}

\tikzpicture
  \makelinesegment 1:1-1:2-2:2!
\endtikzpicture

\bye

Of course there are limitations to this approach (e.g. the above \point does the wrong thing if one of the coordinates has more than one digit, though again that's easy to fix), but if you know exactly what tokens you're going to encounter (e.g. you're simply setting up shortcuts for yourself, and not writing a library for others to use), this approach can be enough, instead of engineering an industrial-strength solution with battle-tested (but complicated) LaTeX macros.

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.