6

I've tried pairing this down, but each time I do that I'm either correcting my mistake or something. So here's a slightly unwieldy MWE.

In a nutshell, what I'm trying to do is....

I'm writing a quiz that has multiple version. I've got some clunky LaTeX code that already handles versioning, but I decided I could probably get cleaner looking document code if I tried writing something using LaTeX3.

Each quiz has a version attached to it. I'm writing a \choice macro that's given a ; separated list of possible output which will change based upon the version of the quiz.

For example if I write \choice{x;y;z;w} then on version A of the quiz \choice will provide x, on version B of the quiz \choice will provide y, etc.

I'm not anywhere near getting this up and running. I'm actually not interested in someone telling me how to achieve my ultimate goal: I think I'll learn a fair amount if I try it on my own. Also, I know I'm not following notational conventions quite correctly, but it's giving me a headache trying to balance notation (which I still find difficult to read) and mastering syntax (which I find slightly obscure, but beginning to get the hang of).

\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
%-@-(1)---------------------------------------------------------------------
%% --- VERSIONING --- %%
%% store what the possible version are
\tl_new:N  \g__version_types
\tl_set:Nn \g__version_types { a;b;c;d }
\tl_show:N \g__version_types
%% make a sequence of the possible versions
\seq_new:N \g__all_possible_versions_seq
\seq_gset_split:Nnn \g__all_possible_versions_seq { ; } {\g__version_types}
\seq_show:N \g__all_possible_versions_seq
%% allow the user to define what the versions are
\NewDocumentCommand{\defineversions}{ O{;} m }{
    \tl_set:Nn \g__version_types { #2 }
    \seq_set_split:Nnn \g__all_possible_versions_seq { #1 } \g__version_types
}
%-@-(2)---------------------------------------------------------------------
%% --- GETTING/SETTING VERSIONS ---%%
\tl_new:N   \g__tl_current_doc_version 

\cs_new:Npn \mv_set_version:n #1 
    {
        \tl_set:Nn \g__tl_current_doc_version { #1 }
    }

\cs_new:Npn \mv_get_version 
    {
        \tl_use:N \g__tl_current_doc_version
    }

\newcommand{\setversion}[1]{\mv_set_version:n {#1}}
\newcommand{\getversion}{ \mv_get_version }

\cs_new:Npn \mv_test_version:n #1 {
    \str_if_eq:VnTF \g__tl_current_doc_version { #1 } { HELLO } { BYE}
}
\newcommand{\testversion}[1]{\mv_test_version:n {#1}}

%-@-(3)---------------------------------------------------------------------
%% --- CREATING THE USER INTERFACE --- %%
%% I'm going to destructive examine the sequence, so make 
%% a copy of it and work with copy
\seq_new:N     \g__copy_all_possible_versions_seq
\seq_set_eq:NN \g__copy_all_possible_versions_seq \g__all_possible_versions_seq
\tl_new:N      \g__current_possible_version_tl
%% Information that the user passes to us
\seq_new:N \g__user_defined_choice_seq
\tl_new:N  \g__current_possible_choice_tl

%% This "cs" assumes that the user choices have been translated
%% into a sequence
\cs_new:Npn \__test_current_choice_against_version:n #1 { 
    \seq_pop_left:NN \g__user_defined_choice_seq        
                     \g__current_possible_choice_tl

    \seq_pop_left:NN \g__copy_all_possible_versions_seq 
                     \g__current_possible_version_tl

    \tl_use:N        \g__current_possible_choice_tl --
    \tl_use:N        \g__current_possible_version_tl \par

    \str_if_eq:VVTF  \g__current_possible_choice_tl 
                     \g__current_possible_version_tl
                     { \tl_use:N \g__current_possible_choice_tl  }
                     { #1 }
}

\cs_new:Npn \mv_make_choice:n #1 {
    \__test_current_choice_against_version:n
        { \__test_current_choice_against_version:n 
            { \__test_current_choice_against_version:n 
                { FAIL }}}
}

\NewDocumentCommand{\choices}{ O{;} m }{
    \texttt{#2}\par
    \seq_gset_split:Nnn \g__user_defined_choice_seq  {#1}  {#2}
    \mv_make_choice:n {#1}
}

\ExplSyntaxOff
\begin{document}

Hello:  I'm setting the version \setversion{b}

I'm getting the version \textbf{\getversion}!

Choosing \choices{x;y;z;w}
\end{document}

I'm getting an error about an empty sequence. I've tried showing the sequence, and tokens used to create it, but nothing seems to be right.

Sean Allred
  • 27,421
A.Ellett
  • 50,533
  • 1
    typo: \str_if_equ:VVTF should be \str_if_eq:VVTF – Scott H. Jan 22 '13 at 00:10
  • 1
    \str_if... should be \tl_if.... There are many more glitches. – egreg Jan 22 '13 at 00:21
  • @egreg. Really? That was the first bit of code I wrote and it worked (or I thought it did). :( – A.Ellett Jan 22 '13 at 00:27
  • 1
    This seems hopeless. I fix one error and another pops up. Why is this so difficult???? – A.Ellett Jan 22 '13 at 00:28
  • It's not clear why you maintain a copy of the sequence, but then destroy also the copy with \seq_pop_left:NN also on it. You're still mixing global and local, which is a no-no. – egreg Jan 22 '13 at 00:32
  • @egreg. I want to cycle through the elements of the sequence. The only way I could see how to do that was to pop elements off of the sequence. This seemed destructive. So, I made a copy of the original thinking I'd only be destroying the copy and not the original. – A.Ellett Jan 22 '13 at 00:37
  • One thing: when you use \seq_gset_split:Nnn \g__... { ; } {\g__...} for example, the third n in Nnn specifies that the third argument is to be untouched. You want to use \seq_gset_split:NnV \g__... { ; } \g__... which replaces the third argument with its Value. At the moment you're trying to split the string \g__... not what it contains. – Scott H. Jan 22 '13 at 00:41
  • To cycle through the elements of a sequence, see the \seq_map commands in the documentation, they're very useful! – Scott H. Jan 22 '13 at 00:44
  • @scottH. I tried \seq_gset_split:NnV but I got an error that the command doesn't exist. – A.Ellett Jan 22 '13 at 00:46
  • Odd, that command exists. Incidentally, if it didn't, then you could create it with \cs_generate_variant:Nn \seq_gset_split:Nnn {NnV} You may be using an outdated version of the l3 packages. – Scott H. Jan 22 '13 at 00:48
  • 2
    Doesn't any of the several exam packages do what you want (or near)? This way you'll only go prematurely bald. – vonbrand Jan 22 '13 at 01:50
  • @vonbrand. Thanks for making me laugh. I'm sure one of those packages does. This was really more an excuse to teach myself something. – A.Ellett Jan 22 '13 at 01:54
  • @A.Ellett On the functions available, it would be useful to know which version of expl3 you've got installed (the version is in your log). We update CTAN several times a year, as the language is still evolving. – Joseph Wright Jan 22 '13 at 08:17

2 Answers2

5

I've tried to correct it, but I don't know if it does exactly what you want:

\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
%-@-(1)---------------------------------------------------------------------
%% --- VERSIONING --- %%
%% store what the possible version are
\tl_new:N  \g__version_types_tl
\tl_set:Nn \g__version_types_tl { a;b;c;d }
%% make a sequence of the possible versions
\seq_new:N \g__all_possible_versions_seq
\seq_new:N \g__copy_all_possible_versions_seq
\cs_generate_variant:Nn \seq_gset_split:Nnn { NnV }
\seq_gset_split:NnV \g__all_possible_versions_seq { ; } \g__version_types_tl
\seq_gset_eq:NN \g__copy_all_possible_versions_seq \g__all_possible_versions_seq
%% allow the user to define what the versions are
\NewDocumentCommand{\defineversions}{ O{;} m }
 {
  \tl_gset:Nn \g__version_types_tl { #2 }
  \seq_gset_split:NnV \g__all_possible_versions_seq { #1 } \g__version_types_tl
  \seq_gset_eq:NN \g__copy_all_possible_versions_seq \g__all_possible_versions_seq
 }

%-@-(2)---------------------------------------------------------------------
%% --- GETTING/SETTING VERSIONS ---%%

\tl_new:N   \g__current_doc_version_tl

\cs_new_protected:Npn \mv_set_version:n #1 
 {
  \tl_set:Nn \g__current_doc_version_tl { #1 }
 }

\cs_new:Npn \mv_get_version:
 {
  \tl_use:N \g__current_doc_version_tl
 }

\NewDocumentCommand{\setversion}{m}
 {
  \mv_set_version:n {#1}
 }
\NewDocumentCommand{\getversion} {}
 {
  \mv_get_version:
 }

\cs_new:Npn \mv_test_version:n #1
 {
  \tl_if_eq:VnTF \g__tl_current_doc_version { #1 } { HELLO } { BYE}
 }
\NewDocumentCommand{\testversion}{m}
 {
  \mv_test_version:n {#1}
 }

%-@-(3)---------------------------------------------------------------------
%% --- CREATING THE USER INTERFACE --- %%
%% I'm going to destructively examine the sequence, so make 
%% a copy of it and work with copy
\tl_new:N  \g__current_possible_version_tl
%% Information that the user passes to us
\seq_new:N \g__user_defined_choice_seq
\tl_new:N  \g__current_possible_choice_tl

%% This "cs" assumes that the user choices have been translated
%% into a sequence
\cs_new_protected:Npn \__test_current_choice_against_version: 
 {
  %I'm working 
  \seq_gpop_left:NN \g__user_defined_choice_seq        
                    \g__current_possible_choice_tl

  \seq_gpop_left:NN \g__copy_all_possible_versions_seq 
                    \g__current_possible_version_tl

  \tl_if_eq:NNTF    \g__current_possible_choice_tl 
                    \g__current_possible_version_tl
                    { \tl_use:N \g__current_possible_choice_tl  }
                    { FAIL }
 }

\cs_new:Npn \mv_make_choice:n #1
 {
  \__test_current_choice_against_version:
 }

\NewDocumentCommand{\choices}{ O{;} m }
 {
  \texttt{#2}\par
  \seq_gset_split:Nnn \g__user_defined_choice_seq  {#1}  {#2}
  \mv_make_choice:n {#1}
 }

\ExplSyntaxOff
\begin{document}

Hello:  I'm setting the version \setversion{b}

I'm getting the version \textbf{\getversion}!

Choosing \choices{x;y;z;w}
\end{document}

I fixed the names and some programming glitches. For instance you're comparing token lists with \str_if_eq:... but \tl_if_eq:... should be used. In particular \tl_if_eq:NNTF and not \str_if_eq:VVTF which does more work for nothing. It's quite obscure why you define \mv_make_choice:n with an argument that you don't use. Just use \__test_current_choice_against_version: as the final instruction in \choices.

Other points.

  1. When a variable is declared global, use only global assignments to it.
  2. Don't forget that all functions should have a colon in their name.
  3. Use \cs_new_protected:Npn when the function does unexpandable jobs (such as setting token lists or sequences).
egreg
  • 1,121,712
  • This whole notion of unexpandable jobs is murky to me. I tried your version, but it's not working for me, I get an Undefined Control Sequence error with \seq_gset_split:NnV – A.Ellett Jan 22 '13 at 00:50
  • Sorry about being obscure with \mv_make_choice:n. Originally I meant it to take an argument. But things weren't working correctly and so I wound up not using the argument. :( – A.Ellett Jan 22 '13 at 01:01
4

I greatly appreciate all the feed back folks gave me. I'm not sure I could have come up with a working example without your suggestions. Because of the time and effort you all put in I thought I'd post what I came up with.

At this point, if you see things you really don't like stylistically, I would love feedback. I think I understand much better what's happening. @egreg. I tried to follow your advise about \tl_if_eq and \str_if_eq, I think I really do need that later in the following code. On account of your comments and suggestions, I'll mark your answer as the correct answer.

Edit

Given that I've presented as much of my solution as I have, I figured I'd update it to a version that does some simple error checking. It could still use some improvement, but I think this will be my final edit.

\documentclass{article}
\pagestyle{empty}
\usepackage{amsmath,amssymb,xcolor}
\usepackage{xparse}
\ExplSyntaxOn
%% ----------------- PARAMETERS ------------------ %%
%% Creating versions and their default values      %%
\seq_new:N          \g__possible_version_types_seq
\seq_gset_split:Nnn \g__possible_version_types_seq {;} { a;b;c;d }
\tl_new:N           \g__current_version_tl
\tl_set:Nn          \g__current_version_tl {a}
%% Creating choices and a means saving choice for  %%
%% later use: even if defined within a group       %%
\seq_new:N  \g__user_provided_choices_seq
\tl_new:N   \g__selected_choice_tl
%% this next token is to allow `\selectedchoice`   %%
%% to be definable from within a grouping.         %%
\tl_new:N   \g__callable_selected_choice_tl
%% a function to select the choice corresponding   %%
%% to the current version                          %%
\cs_new:Npn \fnc_match_version_to_choices:nn #1#2 {
    \str_if_eq:nVT {#1} \g__current_version_tl {\tl_gset:Nn \g__selected_choice_tl {#2}} 
}
%% --------------- ERROR CHECKING ---------------  %%
%% Make sure you don't try to pass a version that  %%
%% has not yet been defined.                       %%
\msg_new:nnnn {mymodule}{invalid version call}{You've\ called\ for\ a\ non-existant\ version.}{}
\cs_new:Nn \err_am_i_defining_valid_version: {
        \seq_if_in:NVF \g__possible_version_types_seq \g__current_version_tl
                { \msg_error:nn{mymodule}{invalid version call}}
}
%% Make sure you provide choices to correspond     %%
%% with the number of versions.  Having more       %%
%% choices than versions will not signal an error. %%
\msg_new:nnnn {mymodule}{unequalchoices}{There\ are\ more\ versions\ than\ choices}{}
\cs_new:Nn \err_fewer_choices_than_versions:   {
      \int_compare:nT  {
                           \seq_length:N \g__possible_version_types_seq 
                           >
                           \seq_length:N \g__user_provided_choices_seq  
                       }
                       {\msg_error:nn{mymodule}{unequalchoices}}
    }
%% ----------------  USER INTERFACE ---------------%%
%% allow the user to set the version types         %%
\NewDocumentCommand{\setpossibleversions}{O{;} m}{ 
      \seq_gset_split:Nnn \g__possible_version_types_seq {#1} {#2}
    }
%% allow the user to set the version manually      %%
\NewDocumentCommand{\setversion}{m}{
        \tl_gset:Nn \g__current_version_tl {#1}
        \err_am_i_defining_valid_version:
    }
%% user interface to map choices to version of quiz   %%
%%                                                    %%
%% make sure that you're not expecting more versions  %%
%% than you've provided choices for.                  %%
%%                                                    %%
%% (1) pair "versions" with "choices"                 %%
%% (2) match "version" against "current version" to   %%
%%     select the desired choice                      %%
%% (3) if the same, return "selected choice"          %%
%%                                                    %%
%% NOTE:  the starred version of `\choice` saves the  %%
%% value of the choice to `\selectedchoice` so you    %%
%% can access it later                                %%
\NewDocumentCommand{\choices}{ s O{;} m }{
    \seq_gset_split:Nnn \g__user_provided_choices_seq {#2} {#3}
    \err_fewer_choices_than_versions:
    \seq_mapthread_function:NNN \g__possible_version_types_seq 
                                \g__user_provided_choices_seq 
                                \fnc_match_version_to_choices:nn
    \tl_use:N \g__selected_choice_tl
    \IfBooleanT #1 {\tl_gset:NV \g__callable_selected_choice_tl \g__selected_choice_tl}
    \tl_gset:Nn \g_selected_choice_tl {}
}
\newcommand{\selectedchoice}{\tl_use:N \g__callable_selected_choice_tl}
%% you might want access to the current version to    %%
%% set page numbers                                   %%
\NewDocumentCommand{\getversion}{}{\tl_use:N \g__current_version_tl}
\ExplSyntaxOff
\begin{document}
\setpossibleversions[,]{a,b,c,d,e}

\setversion{a}

If $m\angle\choices{1;2;3;4;5}=\choices[,]{40,42,38,45,25}^\circ$, then the
\choices*{supplement;complement;complement;supplement;supplement} of the angle has measure\ldots

The measure of the \selectedchoice\ is...

\setversion{d}

If $m\angle\choices*{1;2;3;4;5}=\choices[,]{40,42,38,45,25}^\circ$, then the
\choices{supplement;complement;complement;supplement;supplement} of the angle has measure\ldots

Notice how the measure of $\angle{\selectedchoice}$ is smaller than 90.
This is a clue.

\end{document}

Regardless of style though, thanks to LaTeX3 I am able to write cleaner looking documents than the loopier solutions I've been having to put up with.

enter image description here

A.Ellett
  • 50,533
  • One thing that is mostly visual, you can, if you prefer, use \str_if_eq:nVT {#1} \g__current_version {#2} and eliminate the empty false branch. I'm quite certain that \seq_gset_split:NnV should be defined without generating a variant...if it's not, then either your packages aren't up to date, or it's been removed very recently. – Scott H. Jan 22 '13 at 02:12
  • @scottH. Thanks for your input. I'm working on a Mac and though I updated LaTeX about 3 weeks ago, it's from the version as of June or July or sometime around then. I've still got to figure out how to get more up-to-date versions. (Someone made a comment about how to do that a while ago, I'll have to look it up again---I always find updating scary). – A.Ellett Jan 22 '13 at 02:15
  • Rather than storing the user input in \g__possible_version_types_tl then splitting it with \seq_gset_split:NnV you can split it directly with \seq_gset_split:Nnn (with second and third arguments {#1} {#2}). – Bruno Le Floch Jan 22 '13 at 14:01
  • Rather than \seq_map_function:NN with a function defined separately, you can use \seq_map_inline:Nn \g__possible_version_types_seq { \str_if_eq:nVT {#1} \g__current_version {\bool_gset_true:N ...}}. In fact, you can do simply \seq_if_in:NVT \g__possible_version_types_seq \g_current_version {\msg_error:nnn ... } and get rid of the boolean altogether. Slight subtlety: \seq_if_in takes category codes into account. – Bruno Le Floch Jan 22 '13 at 14:06
  • \int_value:w has been removed from more recent LaTeX3. In fact, it is not needed in your setting, since \seq_length:N (now renamed \seq_count:N) directly expands to an integer. – Bruno Le Floch Jan 22 '13 at 14:07
  • Do you mean to test with != rather than >, since hte numbers should be equal? – Bruno Le Floch Jan 22 '13 at 14:08
  • @BrunoLeFloch. Thank you for your input. Regarding \int_value:w I seem to need it for the current version on my machine. Regarding your last comment, I do in fact mean >. I'm not worried about there being more choices than versions. In fact, I might only want a printed copy with four versions and a fifth potential version waiting for make up quizzes and the like. I'd prefer to have the choices already set up for that possibility at the time I create the document – A.Ellett Jan 22 '13 at 15:09
  • @BrunoLeFloch. Turns out I don't need \int_value:w (I must have been doing something really wonky yesterday to think I needed it.) Thank you for suggesting \seq_if_in. I had tried to search the documentation for something like \seq_if_member etc. Cleans up the code even better. – A.Ellett Jan 22 '13 at 17:11
  • @BrunoLeFloch. Do you know why the name change from \seq_length:N to \seq_count:N which seems a bit unclear? Also, is there aliasing of names in LaTeX3? What I mean is: e.g., so that in a context where length seems more appropriate you call \seq_length:N, in a context where count makes better sense you call \seq_count:N but where the underlying macro/function is the same. – A.Ellett Jan 22 '13 at 17:15
  • @A.Ellett \cs_new_eq:NN could be used although it throws an error in this case because (at least on my machine) \seq_length:N hasn't been removed yet. – Scott H. Jan 22 '13 at 19:10
  • A.Ellet, the reason \seq_count:N is preferred is that \tl_count:N is preferred. In the context of a typesetting system, one could expect \tl_length:n { Hello } to return the size of Hello once typeset, in meters/points/whatever unit you use for, well, lengths. On the other hand, \tl_count:n { Hello } is clearly going to give a number. Also, we have \tl_count_tokens:n (compare \tl_count_tokens:n { a{bc} } versus \tl_count:n { a{bc} }), and that rolls better than length_tokens. – Bruno Le Floch Jan 22 '13 at 20:03
  • @ScottH. \seq_length:N has been deprecated for some time, as part of the "small bang", a reflexion on the names of the functions provided in expl3. It has not yet been removed in public releases, but will be in the next update. Users are not advised to introduce new commands whose module part (the part before the first underscore) is already in use by another package or by the kernel, so \cs_new_eq:NN \seq_length:N \seq_count:N (currently done by the kernel) is bad practice on the part of the user. – Bruno Le Floch Jan 22 '13 at 20:05
  • @BrunoLeFloch Ah, indeed. I was more pointing to \cs_new_eq:NN and hadn't thought through the consequences of the use case. Thanks for pointing that out, it hadn't occurred to me. – Scott H. Jan 22 '13 at 21:04
  • Instead of \tl_gset:NV \g_some_tl \l_another_tl you can do \tl_gset_eq:NN \g_some_tl \l_another_tl. To empty a token list, the dedicated \tl_gclear:N \g_list_to_empty_tl is clearer (more explicit) than \tl_gset:Nn \g_list_to_empty_tl { }. – Bruno Le Floch Jan 22 '13 at 21:19
  • \setversion should use \tl_gset:Nn (or the variable should be made local: that choice is a design decision of whether the version is allowed to change within a special environment...). – Bruno Le Floch Jan 22 '13 at 21:32
  • Let me clarify my comment stating that users should not introduce new functions within a reserved module name (for instance \seq_length:N as a synonym of \seq_count:N). Variants, generated with \cs_generate_variant:Nn are perfectly advisable! (See a followup question for details.) – Bruno Le Floch Jan 24 '13 at 00:13