tags:

views:

68

answers:

2

I'm trying to write a command in LaTeX that takes a string such as 8:00A and converts it into the number of minutes, as part of a script to draw a class schedule using TikZ. However, I'm running into some issues - it seems that LaTeX doesn't actually evaluate the contents of a command.

My command is currently:

\newcommand{\timetominutes}[1]{
  \IfSubStr{#1}{P}{720}{0}+\IfSubStr{#1}{P}{\StrBetween{#1}{:}{P}}{\StrBetween{#1}{:}{A}}+60*\StrBefore{#1}{:}
}

If you print out the text from it, it will correctly calculate the number of minutes from midnight. However, if used inside another function, it becomes apparent that it doesn't actually run any of those commands - it merely returns text including those commands. So if I write:

\myfunc{\timetominutes{8:00A}}

Instead of \myfunc seeing something useful like 0+00+60*8, it sees \IfSubStr{8:00A}{P}{720}{0}+\IfSubStr{8:00A}{P}{\StrBetween{8:00A}{:}{P}}{\StrBetween{8:00A}{:}{A}}+60*\StrBefore{8:00A}{:}. This is absolutely useless to me and I can't seen to find a way to force LaTeX to execute subcommands before the main one. I assume there's a way to do it, but LaTeX documentation is scarce and I can't seem to find anything.

Alternatively, if there's a way to get LaTeX to stop complaining about too many }s (when I have the correct number), that could work.

+1  A: 

Unfortunately, no. As the xstring package states:

The macros of this package are not purely expandable

(Section 3.2 of xtring_doc_en.pdf.)

This "expandable" concept, if you're not familiar with it, is quite a hairy subject in TeX. Simply put, something that is not expandable cannot be evaluated as an argument. Anything that uses an assignment somewhere is guaranteed not to be expandable in most TeX varieties, but other non-expandable triggers exist as well. The solution to such problems is rather difficult for anyone that's not familiar with the inner workings of TeX's "mouth" (the part of TeX that handles things like expansion).

Hint: If the LaTeX code is generated by a script: use the script to convert the time expressions, because just about any programming language is easier to use than TeX when it comes to string manipulation. (Or just about anything else for that matter.)


The xstring package does hint at a way out: You can store the result of most operations in a variable, by adding [\variable] to the end of the calls. This means you'd need to rewrite \timetominutes to something that builds up the result piece by piece, and then store that result in a command sequence for use later on.

Usage:

\timetominutesinto\somevar{8:00A} % \somevar contains 48
\expandafter\myfunc\expandafter{\somevar} % calls \myfunc{48}

Note the use of \expandafter, which tells TeX to do a simple one-level expansion (evaluation) of a command sequence after the next. If you didn't use the two \expandafters, you'd get \somevar as an argument to \myfunc, not 48.

(Caution: ugly TeX code ahead!)

\makeatletter % allow @ in command names
\def\timetominutesinto#1#2{% 
  % #1 = command to store the result in
  % #2 = the text to parse
  % \ttm@tempa and \ttm@tempb are temporary variables for this macro
  \let\ttm@tempa\empty % make the command empty
  \IfSubStr{#2}{P}{%
    \def\ttm@tempa{720+}% set tempa to "720+"
    \StrBetween{#2}{:}{P}[\ttm@tempb]% store substring between : and P into tempb
    \edef\ttm@tempa{\ttm@tempa \ttm@tempb}% set tempa to tempa + tempb
  }{%
    \def\ttm@tempa{0+}% set tempa to 0+
    \StrBetween{#2}{:}{A}[\ttm@tempb]% store substring between : and A into tempb
    \edef\ttm@tempa{\ttm@tempa \ttm@tempb}% set tempa to tempa + tempb
  }%
  \edef\ttm@tempa{\ttm@tempa+60*}% set tempa to tempa + "+60*"
  \StrBefore{#2}{:}[\ttm@tempb]% store substring before : into tempb
  % now, set #1 to the result of evaluating the formula returned by concatenating
  % tempa and tempb
  \edef#1{\numexpr \ttm@tempa \ttm@tempb}%
}

\def is the TeX primitive corresponding to LaTeX's \newcommand/\renewcommand. \edef means Expanding \def, which evaluates the definition before assigning the result to a command sequence. \numexpr evaluates a simple number expression, like x + m + h * 60 created by the command above.

It is also possible to calculate the result immediately as a number, without building up the formula, by using integer arithmetic. But that would make the code even more remote from your original intent.

It is possible to do these string manipulations through TeX itself, without using the xstring package (even expandible in this particular case). But that's pretty low level stuff, which cannot easily be repeated if you're not a TeX wizzard.

Ruben
The LaTeX code wasn't going to be generated by a script, but at this point I think it'll be easier to write the parsing code in C# and use that for generation. Luckily, this will be the only item in the entire LaTeX document, so I can directly print the code from C# and compile it. I am curious, though, as to how this sort of thing works - could you recommend any books or websites that are comprehensive guides to LaTeX, and, specifically, to programming in LaTeX?
Ethan
The TeXbook (Computers and Typesetting, Part A) by D.E. Knuth remains one of the best resources to learn the insides of TeX. Next, The LaTeX Companion would probably be a good resource of leaning the LaTeX side of things. Mind you, there's a lot of information on CTAN as well.
Ruben
A: 

I am a big fan of PerlTeX for programmatic interaction in LaTeX. Using Perl do define functions is often easier than programming in TeX and for more programmatic manipulation it can be far more powerful. PerlTeX gives you a two-way communication between perl and latex, meaning that you embed snippets of Perl code in your LaTeX source, rather than having to write a script that creates your entire source file.

Here is an example of what you wanted. Compile with perltex --latex=pdflatex test.tex where the file has been saved as test.tex and you may omit the optional --latex=pdflatex for dvi generation.

\documentclass{article}
\usepackage{perltex}

\perlnewcommand{\timetominutes}[1]{
  #Argument is [h]h:mm[ ][a/p[m]] ie 8:00am or 4:05 P or 15:30 (24hr fmt)
  my $input = shift;
  my ($hours, $minutes, $AMPM) = $input =~ /(\d+)\:(\d+)\ *(\w*)/;
  if ($AMPM =~ /p/i) {
    $hours += 12;
  } elsif ($AMPM =~ /a/i and $hours == 12) {
    $hours = 0;
  }
  my $numberofminutes = 60*$hours + $minutes;
  return $numberofminutes;
  ## or you could
  # return '\myfunc{' . $numberofminutes . '}';
  ## or simply
  # return "\\myfunc\{$numberofminutes\}"; 
  ## I can't remember if you need to escape the curly braces
}

\begin{document}

It has been \timetominutes{8:00 pm} minutes since midnight.

\end{document}

As far as expansion goes, if you still have problems with \myfunc, whatever it is, you can define it or a wrapper function for it as a perltex function or use the alternate returns provided.

Joel