15

I want to write a macro in LaTeX2e that can pass \textcolor, which is embedded in another macro, the values of the colour model gray, rgb, or cmyk. The number of arguments specified implies the colour model to be used; 1 argument for gray, 3 for the rgb, and 4 for the cmyk model. I was able to write a macro that does what I want, but with the arguments in the standard manner in braces. This is the code I was able to write by modifying the answers here.

\makeatletter  
\def\setmycolour#1{%  
\@ifnextchar\bgroup%  
    {\docolour{#1}}  
    {\dogray{#1}}  
}  
\def\dogray#1{This is gray hue #1.}  
\def\docolour#1#2#3{%  
\@ifnextchar\bgroup%  
    {\docmyk{#1}{#2}{#3}}  
    {\dorgb{#1}{#2}{#3}}  
}  
\def\dorgb#1#2#3{This is rgb colour #1,#2,#3.}  
\def\docmyk#1#2#3#4{This is cmyk colour #1,#2,#3,#4.}  
\makeatother

I use the macro as

\setmycolour{0.85}\\
\setmycolour{1}{0}{0}\\
\setmycolour{1}{0}{0}{0}\

I want to use the macro, for example, as \setmycolour{1,0,0} or \setmycolour{0.85}. How do I parse the arguments in the macro definition to do this? Is the above code the best way to get the effect I want?

David Carlisle
  • 757,742

5 Answers5

10

The comments what color we have is only for demonstration here.

\documentclass{article}
\usepackage[T1]{fontenc}

\makeatletter  
\def\setmycolour#1{\expandafter\setmycolour@i#1,,,,\@nil}
\def\setmycolour@i#1,#2,#3,#4,#5\@nil{% 
  \ifx$#2$ we have gray => #1 \else
    \ifx$#3$ we have a wrong color setting \else
      \ifx $#4$ we have a rgb setting => #1,#2,#3\else
                we have a cmyk setting =>#1,#2,#3,#4
      \fi
    \fi
  \fi 
}  
\makeatother

\begin{document}

\setmycolour{0.5}\par
\setmycolour{0.5,0.6}\par
\setmycolour{0.5,0.6,0.7}\par
\setmycolour{0.5,0.6,0.7,0.8}\par

\end{document}
  • Thanks Herbert, this works fine. I had to add another \if to disallow more than 5 arguments (I'm just splitting hairs now :-)). – Luis Costa Apr 13 '11 at 14:44
  • 3
    Could you explain the solution? I assume you trickily pass four commas to setmycolour@i to ensure that this macro call always succeeds but depending on the number of commas in #1, you might pass too many (which does not hurt). – Christian Lindig Apr 13 '11 at 15:27
  • \def\setmycolour@i#1,#2,#3,#4,#5\@nil expects at least 4 commas, the reason why i have t add these ones if there is only one argument, then #2#3#4#5 are all empty. When there are four comma separated arguments given, #5 collects all trhe rest, in this case three commas. #5 is like a garbage can. The other cases are between these two ones. –  Apr 13 '11 at 15:34
  • What about \ifx$#2$? This seems tricky as well. Is this evaluated as \ifx$$ (true) when #2 is empty? But why does it evaluate to false when #2 is not empty? Could you have used something other than $? – Christian Lindig Apr 13 '11 at 18:10
  • \ifx\relax#2\relax for example. –  Apr 13 '11 at 18:43
  • Is the \expandafter in \setmycolour really necessary? The code seems to work fine without it. Am I missing something? – Luis Costa Apr 18 '11 at 09:16
  • @Luis: not really, but it allows also \setmycolour{{0.5,0.6,0.7,0.8}}, which gives a wrong result without expanding it. –  Apr 18 '11 at 10:09
  • @Herbert: I get the same wrong result with or without \expandafter when doing \setmycolour{{0.5,0.6,0.7,0.8}}; the grouping around the arguments isn't lost after the expansion. Or so it seems. – Luis Costa Apr 18 '11 at 12:43
  • @Luis: you are right, I was talking nonsense ... –  Apr 18 '11 at 13:08
  • 1
    @Christian: I guess you could put any character (a-z or A-Z) in place of $. The key, I think, is that two same tokens are place around the argument being tested, say #2. For example, you write \ifx a#2a with "a" in place of "$". So when #2 is empty \ifx will test left-hand "a" and right-hand "a" and evaluate to true and to false when testing left-hand "a" and "contents-of-#2" when #2 isn't empty whence the right-hand "a" is ignored, now it being part of the 'then' execution. – Luis Costa Apr 18 '11 at 14:52
  • @Loius One last question: in \ifx a#2a with #2 not being empty, why is the right-hand a not showing up somewhere in the output but seems to be consumed by \ifx? – Christian Lindig Apr 18 '11 at 17:36
  • @Christian: You cannot use normal characters. If #2 is a then also an a is seen in the output. Only macros or active characters make sense here. –  Apr 18 '11 at 17:45
  • @Herbert, normal characters work, at least in this situation, although it may not be a good idea to do so in general. – Luis Costa Apr 19 '11 at 14:15
  • @Christian: \ifx tests the next two tokens (i.e. 2 characters or macros), in this case "a" and #2. Only when #2 contains "a" the test evaluates as true and executes the then stuff which contains the right-hand "a", like Herbert said, and "a" and whatever else is in the branch will appear. – Luis Costa Apr 19 '11 at 14:20
  • @Luis: you can not say that normal characters work when there are some cases where they didn't work ... –  Apr 19 '11 at 14:25
  • @Herbert: as far as I understand, the syntax of \ifx is \ifx<token1><token2> do if token test is true \fi, and a token is either a character (+ category code) or a control sequence (macro). I'm fairly new to TeX, so correct me if I'm wrong. When I replaced the character "a" for "$" and then again with "1" in your code above it worked fine both times. – Luis Costa Apr 20 '11 at 13:38
  • @Luis: \ifx a#2a true\else false\fi as I already wrote, you get "atrue" instead of "true" if #2=a. for \ifx $#2$ true\else false\fi causes an error if #2=$. Only macros or active characters make some sense. –  Apr 20 '11 at 14:52
  • @Herbert: I fully agree with you on this point. Testing a macro is the most sensible thing to do. I do say so in my comment above and I do mention clearly in another comment above that the "a" will appear in the "then" branch. However, the fact is that one can use characters in the test and the code writer is responsible for what happens. For example, you may want to evaluate an if statement to "chilly" or "hilly" (\ifx c#2#c hilly \else hilly\fi, far-fetched, I acknowlegde, but possible. – Luis Costa Apr 21 '11 at 08:13
  • @Luis: #2=cc and your test fails ... –  Apr 21 '11 at 12:24
  • @Herbert: You're right.If the \if evaluates to true the code won't always work as desired when testing characters. – Luis Costa May 02 '11 at 11:30
5

A LaTeX3 solution:

\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\NewDocumentCommand{\setmycolour}{ m }
  {
   \prg_case_int:nnn { \clist_length:n { #1 } }
     {
      {1}{ \dogray{#1} }
      {3}{ \dorgb{#1} }
      {4}{ \docmyk{#1} }
     }
     {OOPS}
  }
\ExplSyntaxOff

\def\dogray#1{This is gray hue #1.}
\def\dorgb#1{This is rgb colour #1.}
\def\docmyk#1{This is cmyk colour #1.}

\begin{document}
\setmycolour{0.85}\\
\setmycolour{1,0,0}\\
\setmycolour{1,0,0,0}

\end{document}

Customizations of the commands performed in the admissible cases are, of course, possible.

Important change

Due to the changes made to expl3 in Summer 2012, the functions

\prg_case_int:nnn
\clist_length:n

should be changed into

\int_case:nnn
\clist_count:n

with the same syntax.

David Carlisle
  • 757,742
egreg
  • 1,121,712
4
\documentclass{minimal}

\makeatletter

\def \setmycolour #1{
    \newcount \n
    \n = 0
    \setmycolour@ #1,\stopmarker ,
    \ifnum \n = 1
        This is gray hue (#1).
    \else \ifnum \n = 3
        This is rgb colour (#1).
    \else \ifnum \n = 4
        This is cmyk colour (#1).
    \else
        \message{Wrong number of values.}
    \fi\fi\fi
}

\def \stopmarker{EOV}

\def \setmycolour@ #1,{
    \edef \colorvalue{#1}
    \ifx \colorvalue \stopmarker
        \let \next = \relax
    \else
        \advance \n by 1
        \let \next = \setmycolour@
    \fi
    \next
}
\makeatother

\begin{document}

    \setmycolour{0.5}\par
    \setmycolour{0.5,0.6}\par
    \setmycolour{0.5,0.6,0.7}\par
    \setmycolour{0.5,0.6,0.7,0.8}\par

\end{document}
  • This solution is good and can be improved by using \ifcase\n rather than the multiple \ifnum tests. It will scale nicely. – Bruno Le Floch Apr 13 '11 at 20:21
  • @Bruno: after posting this solution, I've tried to rewrite it using ifcase. Unfortunately, I found it looking awkward, because - as far as I know - there's no possibility to explicitly set constants for ifcase, so you have to write multiple redundant ors. – Dmitrii Volosnykh Apr 13 '11 at 20:33
  • It seems that \ifcase\n \or "gray" \or \ERROR \or "rgb" \or "cmyk" \else \ERROR \fi is shorter than what you have. But in general, it is true that there is no way of doing anything else than contiguous cases with \ifcase. – Bruno Le Floch Apr 13 '11 at 23:26
  • This is a very clear and readable solution. – Luis Costa Apr 18 '11 at 09:23
  • 1
    @DmitryF.Volosnykh: \value is a LaTeX2e command. It is dangerous to redefine primitives or kernel commands outside local groups. – Ahmed Musa Oct 04 '12 at 11:14
  • @AhmedMusa, thak you. Updated my answer. – Dmitrii Volosnykh Aug 09 '13 at 13:34
1

A variant from the Dmitry's code without counter

\documentclass{minimal}

\makeatletter
\def\expr@{This is gray hue}
\def\expr@@{Error with} 
\def\expr@@@{This is rgb colour}
\def\expr@@@@{This is cmyk colour}
\def\expr@@@@@{Error with} 

\def \setmycolour #1{
    \def\tmp{} 
    \setmycolour@ #1,\stopmarker , 
    \@nameuse{expr\tmp} (#1)
}

\def \stopmarker{EOV}

\def \setmycolour@ #1,{
    \edef \value{#1}
    \ifx \value \stopmarker
        \let \next = \relax
    \else
        \expandafter\def\expandafter\tmp\expandafter{\tmp @}% 
        \let \next = \setmycolour@
    \fi
    \next
}
\makeatother

\begin{document}
    \setmycolour{0.5}\par
    \setmycolour{0.5,0.6}\par
    \setmycolour{0.5,0.6,0.7}\par
    \setmycolour{0.5,0.6,0.7,0.8}\par
    \setmycolour{0.5,0.6,0.7,0.8,0.9}\par  
\end{document} 
Alain Matthes
  • 95,075
1

Just for fun, a solution with the lpeg parser included in luaTeX.

\documentclass{standalone}
\usepackage{luacode}

\begin{luacode*}
  lpeg = require('lpeg')

  digit = lpeg.R('09')
  dot = lpeg.P('.')
  number = (digit^1 * dot * digit^1) + digit^1
  comma = lpeg.P(',')

  csv = lpeg.Ct(lpeg.C(number) * (comma * lpeg.C(number))^0) / function (t)
     if #t == 1 then
        return '\\dogray{' .. tostring(t[1]) .. '}'
     else if #t == 3 then
           return '\\dorgb{' .. tostring(t[1]) .. '}{' .. tostring(t[2]) .. '}{' .. tostring(t[3]) .. '}'
        else if #t == 4 then
              return '\\docmyk{' .. tostring(t[1]) .. '}{' .. tostring(t[2]) .. '}{' .. tostring(t[3]) .. '}{'  .. tostring(t[4]) .. '}'
           end
        end
     end
     end

  function parse_and_make(s)
     tex.sprint(lpeg.match(csv,s))
  end
\end{luacode*}

\def\dogray#1{This is gray hue #1.}  
\def\dorgb#1#2#3{This is rgb colour #1,#2,#3.}  
\def\docmyk#1#2#3#4{This is cmyk colour #1,#2,#3,#4.}  

\def\setmycolor#1{%
  \directlua{parse_and_make("#1")}}

\begin{document}

\setmycolor{0.85}\\
\setmycolor{1,0,0}\\
\setmycolor{1,0,0,0}\
\end{document}
David Carlisle
  • 757,742
cjorssen
  • 10,032
  • 4
  • 36
  • 126