4

I created a TikZ coordinate system to solve a positioning problem I had. The code is presented below. However, the code doesn't work; if you run it, you will get the

Too many .'s

error message even though there are, in fact, the correct number of dots in each coordinate in the test tikzpicture. Careful investigation reveals that the macro \sidecs@numberofats is not being populated with the correct value: it always ends up being zero.

Since I have used \makeatletter before defining the coordinate system code, but "@" is an "other" character in the tikzpicture where the coordinates appear, my assumption is that this is a catcode issue. This supposition is backed up by the fact that if you change @ to : everywhere it appears (apart from in macro names, obviously), the document compiles successfully and has the expected appearance.

How can I get XString to count the @ characters? Most XString macros have a starred version that (as the manual explains) disregards catcodes, but for whatever reason, \StrCount does not.

(I wanted to use the built-in PGF parsing library to do this rather than XString, but it isn't full-featured enough. Specifically, it appears that you can't provide a rule that works for "any character not amongst those separately specified", and since I want to accept node names - which might contain almost any character at all - this is no good.)

\documentclass{standalone}

\usepackage{tikz, xstring}

\makeatletter
    \def\sidecs@expandtotop{top}
    \def\sidecs@expandtobottom{bottom}
    \def\sidecs@expandtoleft{left}
    \def\sidecs@expandtoright{right}

    \def\sidecs@trimspacesleft#1{%
        \IfBeginWith{#1}%
                    { }%
                    {\StrGobbleLeft{#1}{1}[#1]\sidecs@trimspacesleft{#1}}%
                    {}%
    }
    \def\sidecs@trimspacesright#1{%
        \IfEndWith{#1}%
                  { }%
                  {\StrGobbleRight{#1}{1}[#1]\sidecs@trimspacesright{#1}}%
                  {}%
    }

    \tikzdeclarecoordinatesystem{side}{
        \begingroup
            \fullexpandarg
            \edef\sidecs@expandedarg{#1}
            \StrCount{\sidecs@expandedarg}{@}[\sidecs@numberofats]
            \if\sidecs@numberofats0
                \let\sidecs@partbeforeat\sidecs@expandedarg
                \def\sidecs@pos{0.5}
                \def\sidecs@posreversed{0.5}
            \else\if\sidecs@numberofats1
                \StrCut{\sidecs@expandedarg}{@}{\sidecs@partbeforeat}{\sidecs@pos}
                \pgfmathsubtract{1}{\sidecs@pos}
                \let\sidecs@posreversed\pgfmathresult
            \else
                \errmessage{Too many @'s in side cs coordinate}
            \fi\fi
            \StrCount{\sidecs@partbeforeat}{.}[\sidecs@numberofdots]
            \if\sidecs@numberofdots0
                \errmessage{Not enough .'s in side cs coordinate}
            \else\if\sidecs@numberofdots1
            \else
                \errmessage{Too many .'s in side cs coordinate}
            \fi\fi
            \StrCut{\sidecs@partbeforeat}{.}{\sidecs@node}{\sidecs@side}
            \sidecs@trimspacesleft{\sidecs@node}
            \sidecs@trimspacesright{\sidecs@node}
            \sidecs@trimspacesleft{\sidecs@side}
            \sidecs@trimspacesright{\sidecs@side}
            \ifx\sidecs@side\sidecs@expandtotop
                \def\sidecs@anchorone{north west}
                \def\sidecs@anchortwo{north east}
            \else\ifx\sidecs@side\sidecs@expandtobottom
                \def\sidecs@anchorone{south west}
                \def\sidecs@anchortwo{south east}
            \else\ifx\sidecs@side\sidecs@expandtoleft
                \def\sidecs@anchorone{north west}
                \def\sidecs@anchortwo{south west}
            \else\ifx\sidecs@side\sidecs@expandtoright
                \def\sidecs@anchorone{north east}
                \def\sidecs@anchortwo{south east}
            \else
                \errmessage{Unknown side}
            \fi\fi\fi\fi
            \pgfpointadd%
                {\pgfpointscale{\sidecs@posreversed}%
                               {\pgfpointanchor{\sidecs@node}{\sidecs@anchorone}}}%
                {\pgfpointscale{\sidecs@pos}%
                               {\pgfpointanchor{\sidecs@node}{\sidecs@anchortwo}}}
            \global\pgf@x=\pgf@x
            \global\pgf@y=\pgf@y
        \endgroup
    }
\makeatother

\begin{document}
    \begin{tikzpicture}
        \begin{scope}[x = 35mm, y = 3mm, every node/.style = draw]
            \node (x) at (0,  0) {XXXXXXXXXX};

            \node (a) at (1,  7) {AAAAAAAAAA};
            \node (b) at (1,  5) {BBBBBBBBBB};
            \node (c) at (1,  3) {CCCCCCCCCC};
            \node (d) at (1,  1) {DDDDDDDDDD};
            \node (e) at (1, -1) {EEEEEEEEEE};
            \node (f) at (1, -3) {FFFFFFFFFF};
            \node (g) at (1, -5) {GGGGGGGGGG};
            \node (h) at (1, -7) {HHHHHHHHHH};
        \end{scope}

        \begin{scope}[->,
                      tl/.style = {out = 90, in = 180},
                      bl/.style = {out = 270, in = 180}]
            \draw [tl] (side cs: x.top    @ 0.5) to (side cs: a.left @ 0.5);
            \draw [tl] (side cs: x.top    @ 0.6) to (side cs: b.left @ 0.5);
            \draw [tl] (side cs: x.top    @ 0.7) to (side cs: c.left @ 0.5);
            \draw [tl] (side cs: x.top    @ 0.8) to (side cs: d.left @ 0.5);
            \draw [bl] (side cs: x.bottom @ 0.8) to (side cs: e.left @ 0.5);
            \draw [bl] (side cs: x.bottom @ 0.7) to (side cs: f.left @ 0.5);
            \draw [bl] (side cs: x.bottom @ 0.6) to (side cs: g.left @ 0.5);
            \draw [bl] (side cs: x.bottom @ 0.5) to (side cs: h.left @ 0.5);
        \end{scope}
    \end{tikzpicture}
\end{document}
  • 3
    \expandafter\StrCut\expandafter\sidecs@expandedarg\expandafter{\string@} – egreg May 30 '15 at 17:03
3

There are several ways to solve the problem.

  1. Since you're under \fullexpandarg you can simply do

    \StrCut{\sidecs@expandedarg}{\string @}{\sidecs@partbeforeat}{\sidecs@pos}
    
  2. If not under \fullexpandarg, you can use a string of \expandafter

    \@xp\StrCut\@xp{\@xp\sidecs@expandedarg\@xp}\@xp{\string @}{\sidecs@partbeforeat}{\sidecs@pos}
    

    (where I use \@xp for \expandafter, that's available if you load amsmath)

  3. Alternatively, the \lowercase trick:

    \begingroup\lccode`?=`@ \lowercase{\endgroup
      \StrCut{\sidecs@expandedarg}{?}{\sidecs@partbeforeat}{\sidecs@pos}%
    }
    

    which exploits the fact that \lowercase doesn't change category code.

Similarly for the \StrCount line:

\StrCount{\sidecs@expandedarg}{\string @}[\sidecs@numberofats]

or

\@xp\StrCount\@xp{\@xp\sidecs@expandedarg\@xp}\@xp{\string @}[\sidecs@numberofats]

or

\begingroup\lccode`?=`@ \lowercase{\endgroup
  \StrCount{\sidecs@expandedarg}{?}[\sidecs@numberofats]%
}
3

Per comment by user egreg, one possible approach is to use \string on @ in two places in the code for "side cs". This turns it (at execution time) into an "@" with the catcode "other", which is what XString is looking for in this case.

Add the following line at or near the beginning of the code:

\let\ea\expandafter

Replace the first \StrCount line with the following:

\ea\StrCount\ea{\ea\sidecs@expandedarg\ea}\ea{\string @}[\sidecs@numberofats]

Replace the first \StrCut line (in the conditional) with the following:

\ea\StrCut\ea{\ea\sidecs@expandedarg\ea}\ea{\string @}{\sidecs@partbeforeat}{\sidecs@pos}

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.