% Features % -------- % * List elements are parsed in the same familiar way that TeX macro % arguments are parsed. Thus: % % * Any brace-balanced token sequence can be treated as a list. % * Leading/trailing spaces and spaces between elements are ignored. % * Lists do not need to contain separators or terminators. % * The macros have powerful features to support flexible use. % % * Templates are used for handling the results of the macros, as % well as for handling each list element in \dolist. For example, % the token sequence % % \listcase{{head of list}{tail}{of}{list}}{nil case}{\foo{#2{#1}}} % % has the same effect as % % \foo{{tail}{of}{list}{head of list}} % % because #1 gets replaced by "head of list" and #2 gets replaced % by "{tail}{of}{list}". % % * \dolist has support for maintaining 7 loop variables, % accessing the values from the previous iteration via #2 through % #8, and passing values to the next iteration via \DLNext. For % example, the token sequence % % \dolist{{abc}{def}{ghi}}{{initial}} % {previous value: #2; current item: #1. \DLNext{{#1}}} % {last value: #2.} % % has the same effect as: % % previous value: initial; current item: abc. % previous value: abc; current item: def. % previous value: def; current item: ghi. % last value: ghi. % % This supports powerful list transformations. For example, % define the following macro: % % \newcommand{\ReverseList}[2] % {\dolist{#1}{}{\DLNext{{{##1}##2}}}{#2}} % % Then we can observe these effects: % % \ReverseList{{1}{2}{3}}{\foo{#1}} -> \foo{{3}{2}{1}} % \ReverseList{palindrome}{#1} -> {e}{m}{o}{r}{d}{n}{i}{l}{a}{p} % % (Thus revealing that "palindrome" is not a palindrome.) % This works because #1 in the continuation gets replaced by the % accumulated reversed value. In fact, the following macro % definition is equivalent: % % \newcommand{\ReverseList}[1] % {\dolist{#1}{}{\DLNext{{{##1}##2}}}} % % As another example, we can define a macro to put commas or % spaces in numbers every three places to the left of the decimal % mark: % % \newcommand{\InsertThousandsMarks}[2] % {% #1 is THOUSANDS_SEPARATOR % % #2 is NUMBER % % Remaining input prefix: CONTINUATION (which may mention #1) % \dolist{#2}{} % {% ##1 is DIGIT % % ##2 is digits before decimal mark in form 1 234 567 890 % % ##3 is digits before decimal mark in form 12 345 678 90 % % ##4 is digits before decimal mark in form 123 456 789 0 % % ##5 is decimal mark and everything after it % \ifthenelse{\equal{##5}{}\and % \(\equal{##1}{0}\or\equal{##1}{1}\or % \equal{##1}{2}\or\equal{##1}{3}\or % \equal{##1}{4}\or\equal{##1}{5}\or % \equal{##1}{6}\or\equal{##1}{7}\or % \equal{##1}{8}\or\equal{##1}{9}\)} % {\ifthenelse{\equal{##2}{}} % % Don't put separators before first digit. % {\DLNext{{##1}{##1}{##1}}} % % We've seen some digits, add separator to ##4. % {\DLNext{{##3##1}{##4##1}{##2#1##1}}}} % % We've seen the decimal mark. % {\DLNext{{##2}{##3}{##4}{##5##1}}}} % {\usetemplate{{##2##5}}}} % % With this macro, we have these results: % % \InsertThousandsMarks{,}{7419876.4235}{The result: [#1]} % -> The result: [7,419,876.4235] % * Missing drawbacks: % % * None of the macros execute their arguments inside a group. % * Nested invocations work correctly. % * No registers are used. % * Macros that need it have been defined with \DeclareRobustCommand. % Lists % ----- % Any brace-balanced sequence of tokens (including annoying tokens % such as "#", \iffalse, \else, \fi, etc.) is a valid list. % The elements of a list can be determined as follows. Any initial % space tokens (anything that would be skipped by TeX when looking for % an undelimited macro parameter) are ignored. If that is all the % list contains, then the list is empty and has zero elements. % Otherwise, the head (first element) of the list is the argument that % would be found by TeX when looking for an undelimited macro % parameter with the list being the next input and the tail is the % input that would be left over. % This means that if the list is of the form "{XXX}YYY'' where XXX is % brace-balanced, then the head is XXX and the tail is YYY. It is % generally wisest to write a list in the following form with braces % around each element: % % {ELEM_1}{ELEM_2}...{ELEM_n} % % Leading/trailing spaces and spaces in between the elements are okay. % Known Problems % -------------- % * LaTeX's implementation (or probably, any possible implementation) % of optional arguments has the result that a list of length 1 used as % an optional argument will have any braces around its sole element % stripped off. If that element consists of more than one token, then % the element's contents will be treated as a list, which is almost % certainly the wrong thing to do and will cause havoc. The way % around this problem is to always wrap lists with an extra pair of % braces when passing them as optional arguments. A macro taking an % optional list argument will then be responsible for removing the % extra braces. % * Algorithms using \listcase which do something for each element of % a list will have time complexity that is at least quadratic in the % length of the lists. There seems to be no way to make \listcase any % more efficient. If an algorithm needs to work with long lists, it % is better to structure it using \dolist instead. % Implementation Dependencies % --------------------------- % The implementation depends on this LaTeX internal macro: % % \@gobble % % As this macro is undocumented, future LaTeX implementations might % not supply a macro with the same name and compatible behavior, in % which case this implementation will fail. % Design Issues % ------------- % In TeX, Whenever the token sequence matching a parameter is of the % shape {} where is some sequence % of tokens which is balanced w.r.t. braces, then the actual argument % becomes just the subsequence. There is no way to % test whether this has happened that will work in all circumstances. % In the implementation of \listcase, we would like to match against a % list of the form "{ELEM_1}{ELEM_2}...{ELEM_n}" and separately % extract the head "{ELEM_1}" and the tail "{ELEM_2}...{ELEM_n}". The % problem is when the list is two elements long, the tail is just % "{ELEM_2}" and if the tail token sequence matches an argument, then % the braces will be stripped off. % We get around this problem by extracting the tail from the list % separately, after having determined that the list is not empty. If % #1 contains the list, then the tail can be extracted via: % % \expandafter{\@gobble#1} % % This makes sure the tail is always surrounded by an extra pair of % braces, so that when the tail is a list of length 1 and the % expansion of the above is used as an argument, the braces around the % tail's sole element are protected. % Historical note: A way to get around this which was used in an % earlier implementation is to have #2 match the entire list, #3 match % the head (losing its braces), and #4 match the tail. We then test % if "#2" and "{#3}#4" are the same, allowing us to give special % treatment to the case when the tail loses the braces around its only % element. The problem with this approach is that testing if "#2" and % "{#3}#4" are the same requires putting them in the body of macro % definitions, which can fail when the list contains "#" tokens. Plus % it is more expensive. % Notation for Describing Substitutions % ------------------------------------- % % If X is some sequence of tokens that contains some number (possibly % zero) of occurrences of the token # followed by a token from the set % {1, 2, ..., n} where 0 <= n <= 9, and Y_1, Y_2, ..., Y_n are token % sequences, then X(Y_1,...,Y_n) stands for the token sequence % resulting from transforming X by replacing all occurrences of #i by % Y_i for 1 <= i <= n. % ---------------------------------------------------------------------- \RequirePackage{jbw-prog-util} \newcommand{\jbwl@PackageName}{jbw-list} % ---------------------------------------------------------------------- \DeclareRobustCommand{\listcase}[1] % % Invoke as follows: % % \listcase{LIST}{NIL_CASE}{CONS_CASE} % % CONS_CASE is allowed to contain #1 and #2 (which will usually have % to be written as ##1 and ##2 in the invocation). % % The behavior is as follows: % % \listcase{} {NIL_CASE}{CONS_CASE} -> NIL_CASE % \listcase{{HEAD}TAIL}{NIL_CASE}{CONS_CASE} -> CONS_CASE(HEAD,TAIL) % {% #1 is LIST % Remaining input prefix: {NIL_CASE}{CONS_CASE} \ParseArgs {#1\jbwl@Mark\jbwl@Mark*\jbwl@Mark{#1}} {##1##2\jbwl@Mark##3##4\jbwl@Mark##5##6##7} {% If LIST is empty: % ##1 is \jbwl@Mark % ##2 is empty % ##3 is * % ##4 is empty % If LIST is POSSIBLY_WRAPPED_HEAD TAIL % ##1 is HEAD % ##2 is junk (POSSIBLY_STRIPPED_TAIL) % ##3 is \jbwl@Mark % ##4 is * % ##5 is LIST % ##6 is NIL_CASE % ##7 is CONS_CASE % % It is important that ##4 is guaranteed not to contain \ifXXX, % \else, \fi, or !. If ##4 is not empty, then the second ! will % actually be part of the "then" branch of the \if, but that's okay % because in that case the "else" branch is taken. \IfChar{!##4!} {% LIST is empty. ##6} {% LIST is non-empty. \ExpandFirst{{\@gobble##5}{##1}} {% ####1 is TAIL % ####2 is HEAD \usetemplate{{####2}{####1}}}% % indented arg of \usetemplate, though not of \ExpandFirst {##7}}}} % ********************************************************************** \DeclareRobustCommand{\OLDlistcase}[1] % % Invoke as follows: % % \listcase{LIST}{NIL_CASE}{CONS_CASE} % % CONS_CASE is allowed to contain #1 and #2 (which will usually have % to be written as ##1 and ##2 in the invocation). % % The behavior is as follows: % % \listcase{} {NIL_CASE}{CONS_CASE} -> NIL_CASE % \listcase{{HEAD}TAIL}{NIL_CASE}{CONS_CASE} -> CONS_CASE(HEAD,TAIL) % {% #1 is LIST % Remaining input prefix: {NIL_CASE}{CONS_CASE} \jbwl@ListCaseAuxA#1\jbwl@Mark\jbwl@Mark*\jbwl@Mark{#1}} \def\jbwl@ListCaseAuxA#1#2\jbwl@Mark#3#4\jbwl@Mark#5#6#7% {% If LIST is empty: % #1 is \jbwl@Mark % #2 is empty % #3 is * % #4 is empty % If LIST is POSSIBLY_WRAPPED_HEAD TAIL % #1 is HEAD % #2 is junk (POSSIBLY_STRIPPED_TAIL) % #3 is \jbwl@Mark % #4 is * % #5 is LIST % #6 is NIL_CASE % #7 is CONS_CASE % % It is important that #4 is guaranteed not to contain \ifXXX, % \else, \fi, or !. If #4 is not empty, then the second ! will % actually be part of the "then" branch of the \if, but that's okay % because in that case the "else" branch is taken. \IfChar{!#4!} {% LIST is empty. #6} {% LIST is non-empty. \ExpandFirst{{\@gobble#5}{#1}} {% ##1 is TAIL % ##2 is HEAD \usetemplate{{##2}{##1}}}% % indented because arg of \usetemplate, though not of \ExpandFirst {#7}}} %********************************************************************** % \def\OLDjbwl@ListCaseAuxA#1#2\jbwl@Mark#3#4\jbwl@Mark{% % % If LIST is empty: % % #1 is \jbwl@Mark % % #2 is empty % % #3 is * % % #4 is empty % % If LIST is POSSIBLY_WRAPPED_HEAD TAIL % % #1 is HEAD % % #2 is junk (POSSIBLY_STRIPPED_TAIL) % % #3 is \jbwl@Mark % % #4 is * % % Remaining input prefix: {LIST}{NIL_CASE}{CONS_CASE} % % % % It is important for the next line that #4 is guaranteed not to % % contain \ifXXX, \else, or \fi. % \if!#4!% % % LIST is empty. % % This will have the effect of: NIL_CASE % \expandafter\jbwl@ThirdOfFour % \else % % LIST is non-empty. % \expandafter\jbwl@ListCaseAuxB % \fi % {#1}} % \newcommand{\jbwl@ThirdOfFour}[4]{#3} % \newcommand{\jbwl@ListCaseAuxB}[2] % {% #1 is HEAD % % #2 is LIST % % Remaining input prefix: {NIL_CASE}{CONS_CASE} % \expandafter\jbwl@ListCaseAuxC\expandafter{\@gobble#2}{#1}} % \newcommand{\jbwl@ListCaseAuxC}[3] % {% #1 is TAIL % % #2 is HEAD % % #3 is junk (NIL_CASE) % % Remaining input prefix: {CONS_CASE} % \usetemplate{{#2}{#1}}} % ---------------------------------------------------------------------- \DeclareRobustCommand{\ziplist} % % Invoke as follows: % % \ziplist{LIST_1}{LIST_2}{CONTINUATION} % % LIST_1 and LIST_2 must be valid lists. % Let LIST_1 be: {ELEM_1_1} ... {ELEM_1_n} % Let LIST_2 be: {ELEM_2_1} ... {ELEM_2_m} % Let p = min(n,m). % For 1 <= i <= p, let ELEM_3_i be: {ELEM_1_i}{ELEM_2_i} % Let LIST_3 be: {ELEM_3_1} ... {ELEM_3_p} % % CONTINUATION is allowed to contain the token #1 (which will % usually have to be written as ##1 in the invocation). % % The behavior is as follows: % % \ziplist{LIST_1}{LIST_2}{CONTINUATION} -> CONTINUATION(LIST_3) % {% Remaining input prefix: {LIST_1}{LIST_2}{CONTINUATION} % Start the loop with an empty accumulator. \jbwl@ZipListLoop{}} \newcommand{\jbwl@ZipListLoop}[3] % #1 is RESULT (the accumulated zipped list) % #2 is LIST_1 % #3 is LIST_2 % Remaining input prefix: {CONTINUATION} {\listcase{#2}% Test whether LIST_1 is empty. {\usetemplate{{#1}}}% If LIST_1 is empty, pass result to CONTINUATION. {% ##1 is HEAD_1 % ##2 is TAIL_1 \listcase{#3}% Test whether LIST_2 is empty. {\usetemplate{{#1}}}% If LIST_2 is empty, pass result to CONTINUATION. {% ####1 is HEAD_2 % ####2 is TAIL_2 % Append pair {{HEAD_1}{HEAD_2}} to end of result. % Process the list tails recursively. \jbwl@ZipListLoop{#1{{##1}{####1}}}{##2}{####2}}}} % ---------------------------------------------------------------------- \DeclareRobustCommand{\dolist}[3] % % Invoke as: % % \dolist{LIST}{VALUES}{ELEM_HANDLER}{CONTINUATION} % % LIST is {ELEM_1}...{ELEM_m} where m >= 0 % VALUES is {VAL_2}...{VAL_n} where 1 <= n <= 8 % ELEM_HANDLER may contain #1, ..., #8 % CONTINUATION may contain #1, ..., #8 % VAL_i is nothing for i in {n+1, ..., 8} % % There is support for 7 loop values. The values from the previous % iteration (or from the invocation in the case of the first % iteration) are accessed in ELEM_HANDLER and CONTINUATION as #2, % ... #8. In CONTINUATION, the value for #2 is also available as % #1. Values are passed to the next iteration (or the continuation) % via \DLNext and \DLLast. % % The action goes as follows. % % 1. If m = 0, i.e., LIST is empty, then do: % % CONTINUATION(VAL_2,VAL_2,...,VAL_8) % % 2. Otherwise, do the following, which we will call *: % % ELEM_HANDLER(ELEM_1,VAL_2,...,VAL_8) % % The subsequent action depends on what * does. % % A. If * ends with \DLNext{VALUES}, then do (the equivalent of): % % \dolist{{ELEM_2}...{ELEM_m}}{VALUES}{ELEM_HANDLER}{CONTINUATION} % % B. If * ends with \DLLast{VALUES}, then do (the equivalent of): % % \dolist{}{VALUES}{ELEM_HANDLER}{CONTINUATION} % % C. If * ends without invoking \DLNext or \DLLast, then act as % if it ended with \DLNext{}. % % D. If * looks at any input following it, all hell will break % loose. The execution of * must not do this. % % Here are some examples. A simple usage is % % \dolist{{A}{B}{C}}{}{#1!}{} % % which when executed does: % % A!B!C! % % A more complicated usage is % % \dolist{{B}{C}{D}}{{A}}{(#1,#2)\DLNext{{#1}}}{ last letter: #2} % % which when executed does: % % (B,A)(C,B)(D,C) last letter: D % {% #1 is LIST % #2 is VALUES % #3 is ELEM_HANDLER % Remaining input prefix: {CONTINUATION} \DLNext{#2}\jbwl@MarkA{#3}#1\jbwl@MarkB} % % STATE_LAYOUT is CODE\jbwl@MarkA{ELEM_HANDLER}LIST\jbwl@MarkB{CONTINUATION} % \newcommand{\jbwl@MarkA} {% If this is executed, then we pretend the last thing ELEM_HANDLER % did was \DLNext{}. \DLNext{}\jbwl@MarkA} % The definition of \jbwl@MarkB ensures that no other token will be % considered the same as \jbwl@MarkB by \ifx. It is small (two % tokens), so it can be compared quickly. If it is ever executed or % expanded in an \edef, an error will result. \newcommand{\jbwl@MarkB}{\jbwl@MarkBError:} \newcommand{\jbwl@MarkBError}[1] {% #1 is : (the bogus extra token added to force expansion death) \PackageError{\jbwl@PackageName} {Something horrible has happened: jbwl@MarkB control sequence used!} {No additional help.}} \newcommand{\jbwl@CheckMarkA}[1] {% #1 is CONTROL_SEQUENCE % Die if CONTROL_SEQUENCE is not \ifx-equal to \jbwl@MarkA. \ifx\jbwl@MarkA#1\else\jbwl@CheckMarkAError\fi} % The error message is in separate macro to make expansion of % \jbwl@CheckMarkA go faster in the normal case. \newcommand{\jbwl@CheckMarkAError} {\PackageError{\jbwl@PackageName} {Something horrible has happened: jbwl@MarkA control sequence not in expected place!} {No additional help.}} \newcommand{\DLNext}[1] {% #1 is VALUES % If LIST non-empty, remaining input prefix is: % \jbwl@MarkA{ELEM_HANDLER}{HEAD}TAIL\jbwl@MarkB{CONTINUATION} % If LIST empty: % \jbwl@MarkA{ELEM_HANDLER}\jbwl@MarkB{CONTINUATION} %\TraceOn \Grab{\jbwl@Tok} {\jbwl@CheckMarkA{\jbwl@Tok}% \ParseArgs{{#1}}{##1##2##3} {% ##1 is VALUES % ##2 is ELEM_HANDLER % If LIST non-empty: % ##3 is HEAD % Remaining input prefix is TAIL\jbwl@MarkB{CONTINUATION} % If LIST empty: % ##3 is \jbwl@MarkB % Remaining input prefix is TAIL\jbwl@MarkB{CONTINUATION} % We are going to place ##3 back on the input. We % only picked it up to get rid of any leading spaces so we % can then test whether it is \jbwl@MarkB. We have to test % it carefully because HEAD can contain any token. \Peek{\jbwl@Tok} {\IfX{\jbwl@Tok\jbwl@MarkB} {\EatTokens{2} {\jbwl@DoListInvokeContinuation{##1}}} {\ParseArgs{{##1}{##2}{##3}}{####1####2####3####4\jbwl@MarkC} {% ####1 is VALUES % ####2 is ELEM_HANDLER % ####3 is HEAD % ####4 is junk \usetemplate{{####3}####1}{####2}% \jbwl@MarkA{####2}}}}% ##3\jbwl@MarkC}}} %********************************************************************** \newcommand{\OLDDLNext}[4] {% #1 is VALUES % #2 is \jbwl@MarkA % #3 is ELEM_HANDLER % If LIST non-empty: % #4 is HEAD % Remaining input prefix: TAIL\jbwl@MarkB{CONTINUATION} % If LIST empty: % #4 is \jbwl@MarkB % Remaining input prefix: {CONTINUATION} \jbwl@CheckMarkA{#2}% % We have to do this careful checking of whether #4 is \jbwl@MarkB % because if #4 is HEAD, it can contain \ifXXX, \else, and \fi % tokens which could screw up \iffalse ... \else skipping. % Fortunately, #4 is guaranteed to be brace-balanced, so we can % skip it as an argument. \futurelet\jbwl@MarkBTemp\jbwl@NextInt#4\jbwl@MarkC{#1}{#3}{#4}} \def\jbwl@NextInt#1\jbwl@MarkC {% If LIST non-empty: % \jbwl@MarkBTemp is \let-equal to the first token of HEAD % #1 is junk (HEAD, but may have had braces stripped, so can't use it) % Remaining input prefix: % {VALUES}{ELEM_HANDLER}{HEAD}TAIL\jbwl@MarkB{CONTINUATION} % If LIST empty: % \jbwl@MarkBTemp is \let-equal to \jbwl@MarkB % #1 is junk (\jbwl@MarkB) % Remaining input prefix: % {VALUES}{ELEM_HANDLER}{\jbwl@MarkB}{CONTINUATION} \ifx\jbwl@MarkB\jbwl@MarkBTemp \expandafter\jbwl@DoListDone \else \expandafter\jbwl@DoListMore \fi} \newcommand{\jbwl@DoListDone}[3] {% #1 is VALUES % #2 is junk (ELEM_HANDLER) % #4 is junk (\jbwl@MarkB) % Remaining input prefix: {CONTINUATION} \jbwl@DoListInvokeContinuation{#1}} \newcommand{\jbwl@DoListMore}[3] {% #1 is VALUES % #2 is ELEM_HANDLER % #3 is HEAD % Remaining input prefix: TAIL\jbwl@MarkB{CONTINUATION} % This restores the STATE_LAYOUT documented above. \usetemplate{{#3}#1}{#2}\jbwl@MarkA{#2}} \def\DLLast#1#2#3#4\jbwl@MarkB{% % #1 is VALUES % #2 is \jbwl@MarkA % #3 is junk (ELEM_HANDLER) % #4 is junk (LIST) % Remaining input prefix: {CONTINUATION} \jbwl@CheckMarkA{#2}% \jbwl@DoListInvokeContinuation{#1}} \newcommand{\jbwl@DoListInvokeContinuation}[1] {% #1 is VALUES is {VAL_2}...{VAL_n} where 1 <= n <= 8 % Remaining input prefix: {CONTINUATION} % VAL_i is nothing for i in {n+1, ..., 8} % This will invoke CONTINUATION(VAL_2,VAL_2,...,VAL_8) % Note that VAL_2 becomes both #1 and #2 in CONTINUATION. \expandafter % The {} on the next line is in case #1 is empty. \usetemplate\expandafter{\jbwl@DuplicateArg#1{}}} \newcommand{\jbwl@DuplicateArg}[1]{{#1}{#1}} % Local variables: % tex-main-file: "jbw-list-test.tex" % end: