3

I am working on an extension to the exam class, and would like to add optional arguments to the \question command. Currently, the command takes one optional argument, the number of points the question should have.

The implementation of \question comes down to a \@doitem command (line 3584 of exam.cls):

\def\@doitem{\@ifnextchar[{\@readpoints}% {\item@points@pageinfo}% } 

That is, if there is an optional argument given, it is passed to \@readpoints, and if not, \item@points@pageinfo is expanded instead. The \@readpoints command reads the points and then expands \item@points@pageinfo. The latter command generates the question number and point designation.

Instead of a “free text” optional argument for the points, I would like to pass it to a points keyword argument. I would also prefer to set a question label with a label keyword argument, instead of an explicit \label command. The first argument needs to be processed before \@readpoints, and the second after \item@points@pageinfo. [Another use case is given in this question, where OP wants to add question metadata like the learning objective assessed.]

Here's what I have so far:

\tl_new:N \l_exam_points_tl \RenewDocumentCommand{\@doitem}{={points} +O{}}{ \tl_clear:N \l_exam_points_tl \keys_set_groups:nnn {exam/item} {points} {#1} \tl_if_empty:NTF \l_exam_points_tl { \item@points@pageinfo }{ \@readpoints[\l_exam_points_tl] } \keys_set_exclude_groups:nnn {exam/item} {points} {#1} } \keys_define:nn {exam/item} { points .tl_set:N = \l_exam_points_tl, points .groups:n = {points}, hello .code:n = {[Hello,~#1!]},% just for testing label .code:n = {\label{#1}}, unknown .meta:n = {points=\l_keys_key_str}, unknown .groups:n = {points}, } 

The argument specification to the new \@doitem command allows for an optional argument either as free text or as a series of keyvals (Section 2.9 of usrguide.pdf). The keys are scanned for any which set the points (Section 27.7 of interface3.pdf). If so, the value is saved in \l_exam_points_tl for passing to \@readpoints. The rest are deferred until after \item@points@pageinfo.

This works pretty well! If the optional argument text is points=5 or just 5, the points are set correctly. If the optional argument is label=foo, a label is set after the question counter is incremented. And label=foo,points=5 does both.

What doesn't work yet is an optional argument like label=foo,5. I was hoping the unknown key handler could handle that, by rerouting it to points. I tried to put the unknown key handler in the group that is processed before \@readpoints. But what I wrote has no effect.

I also tried unknown .code:n = {\tl_set:Nx \l_exam_points_tl {\l_keys_key_str}}, and I can tell by \cs_show:N that it does read and set \l_exam_points_tl to the correct value of the unknown key string. But it doesn't do it until after \item@points@pageinfo, which is too late. Without the unknown key handler, I get an error about an unknown key (naturally!)

So is there a way for unknown keys to be handled within a group, or otherwise have an unknown key processed in advance? “No” is a reasonable answer. If I am adding keyword arguments to a question that already has a point value as a free text argument, I can simultaneously add the points keyword. But I thought I'd ask to see if I understand the situation well.

Here is a complete document that illustrates my question.

\documentclass{exam} \title{Know your US States and Capitals!} \makeatletter \ExplSyntaxOn \DeclareCommandCopy\examorig@doitem\@doitem \tl_new:N \l_exam_points_tl \RenewDocumentCommand{\@doitem}{={points} +O{}}{ \tl_clear:N \l_exam_points_tl \keys_set_groups:nnn {exam/item} {points} {#1} \tl_if_empty:NTF \l_exam_points_tl { \item@points@pageinfo }{ \@readpoints[\l_exam_points_tl] } \keys_set_exclude_groups:nnn {exam/item} {points} {#1} \cs_show:N \l_exam_points_tl } \keys_define:nn {exam/item} { points .tl_set:N = \l_exam_points_tl, points .groups:n = {points}, hello .code:n = {[Hello,~#1!]}, label .code:n = {\label{#1}}, unknown .meta:n = {points=\l_keys_key_str}, % unknown .code:n = {\tl_set:Nx \l_exam_points_tl {\l_keys_key_str}}, unknown .groups:n = {points}, } \ExplSyntaxOff \makeatother \begin{document} \begin{questions} \question[label=CA,points=3] What is the capital of California? \begin{choices} \choice Los Angeles \choice Sacramento \choice San Francisco \choice San Diego \end{choices} \question[hello=world,label=TX] Which city serves as the capital of Texas? \begin{choices} \choice Dallas \choice Houston \choice Austin \choice San Antonio \end{choices} % freetext argument `2` is converted to `points=2` and processed \question[2] What is the capital of Florida? \begin{choices} \choice Miami \choice Orlando \choice Jacksonville \choice Tallahassee \end{choices} % non-keyword argument '3' is ignored, or rather it is read at the wrong time \question[label=NY,3] Which city is the capital of New York? \begin{choices} \choice Buffalo \choice Albany \choice New York City \choice Syracuse \end{choices} \question[hello=Donald,points=7] What is the capital of Nevada? \begin{choices} \choice Reno \choice Henderson \choice Carson City \choice Las Vegas \end{choices} % non-keyword argument '6' is ignored, or rather it is read at the wrong time \question[hello=Duck,6] Which city serves as the capital of Colorado? \begin{choices} \choice Denver \choice Boulder \choice Colorado Springs \choice Aurora \end{choices} \end{questions} \end{document} 
11
  • Note that if you have label=foo,5 as an argument, 5 would be a key, not a value. So, you would need to take care that you only pass keys to \@doitem if they consist of digits only. Commented Apr 16 at 20:39
  • 1
    The problem with \keys_set_groups:nnn is that (citing the l3keys manual) "[u]nknown keys are not assigned to any group and are thus never set". I assume that the assignment to a group does not work for the unknown key. Maybe use different branches rather than groups. Commented Apr 16 at 20:53
  • 1
    is there any reason you can't do all the option processing up front and just save the stuff you aren't ready to use yet? Commented Apr 16 at 20:54
  • 1
    @JasperHabicht: I think I asked an XY problem and your citation proves the answer to X is no. Thank you. Commented Apr 16 at 21:04
  • 1
    or just save it to a variable and then use the value of that variable to create the label later? Commented Apr 16 at 23:44

3 Answers 3

4

Note that I will probably end up deleting this because I'm not sure I understand the question ...

I think you can just save the values into variables and then simply use them. You do not seem to need to defer anything (which makes me suspect I've not understood the point of the question):

\documentclass{exam} \title{Know your US States and Capitals!} \makeatletter \ExplSyntaxOn \DeclareCommandCopy\examorig@doitem\@doitem \tl_new:N \l_exam_points_tl \RenewDocumentCommand{\@doitem}{={points} +O{}}{ \tl_clear:N \l_exam_points_tl \tl_clear:N \l_exam_label_tl \keys_set:nn {exam/item} {#1} \tl_if_empty:NTF \l_exam_points_tl { \item@points@pageinfo }{ \@readpoints[\l_exam_points_tl] } \clist_map_inline:nn { hello , label } { \tl_if_empty:cF {l_exam_##1_tl} { \use:c { __exam_##1:v } {l_exam_##1_tl} } } } \cs_new_protected:Npn \__exam_hello:n #1 { Hello~#1! } \cs_new_eq:NN \__exam_label:n \label \cs_generate_variant:Nn \__exam_hello:n { v } \cs_generate_variant:Nn \__exam_label:n { v } \keys_define:nn {exam/item} { points .tl_set:N = \l_exam_points_tl, hello .tl_set:N = \l_exam_hello_tl, label .tl_set:N = \l_exam_label_tl, unknown .code:n = {\tl_set:NV \l_exam_points_tl \l_keys_key_str}, } \ExplSyntaxOff \makeatother \begin{document} \begin{questions} \question[label=CA,points=3] What is the capital of California? \begin{choices} \choice Los Angeles \choice Sacramento \choice San Francisco \choice San Diego \end{choices} \question[hello=world,label=TX] Which city serves as the capital of Texas? \begin{choices} \choice Dallas \choice Houston \choice Austin \choice San Antonio \end{choices} % freetext argument `2` is converted to `points=2` and processed \question[2] What is the capital of Florida? \begin{choices} \choice Miami \choice Orlando \choice Jacksonville \choice Tallahassee \end{choices} % non-keyword argument '3' is ignored, or rather it is read at the wrong time \question[label=NY,3] Which city is the capital of New York? \begin{choices} \choice Buffalo \choice Albany \choice New York City \choice Syracuse \end{choices} \question[hello=Donald,points=7] What is the capital of Nevada? \begin{choices} \choice Reno \choice Henderson \choice Carson City \choice Las Vegas \end{choices} % non-keyword argument '6' is ignored, or rather it is read at the wrong time \question[hello=Duck,6] Which city serves as the capital of Colorado? \begin{choices} \choice Denver \choice Boulder \choice Colorado Springs \choice Aurora \end{choices} \end{questions} \end{document} 
6
  • 2
    Upvoting—please don't delete! Commented Apr 19 at 15:36
  • @MatthewLeingang thanks. well, I won't if it is useful. I wasn't sure I'd understood the question. Commented Apr 19 at 18:12
  • I'm still trying to learn l3, and a lot of its idioms don't come naturally to me. If I understand correctly, you copy the \label command to a local version \__exam_label:n, then generate a variant to accept a variable name instead, then use the command with the name of the variable (if the variable isn't empty). What are the advantages of doing it that way? I might have just written \tl_if_empty:NT \l_exam_label_tl {\label{\l_exam_label_tl}} Commented Apr 21 at 12:58
  • 1
    @MatthewLeingang it let me use \clist_map_inline:nn { hello , label } { \tl_if_empty:cF {l_exam_##1_tl} { \use:c { __exam_##1:v } {l_exam_##1_tl} } } to iterate over the list of functions. so you can add a new one and get it processed by just adding bye, say, to hello, label. \__exam_bye:n might do something much more complicated with the value of \l_exam_bye_tl than just printing it or whatever. so the mechanism is over-the-top for hello, say, but you indicated you probably wanted to handle more/different options this way, too. Commented Apr 21 at 14:51
  • 1
    @MatthewLeingang the lowercase v means: construct the name of the variable first, then get its value and pass it to the base function. an uppercase V would take the name of the variable directly. also, this way, \label should receive exactly what it would usually receive, so hopefully less likely to break in edge cases. (maybe - I'm no expl3 expert.) Commented Apr 21 at 14:54
3

A version that uses expkv (via expkv-def -- disclaimer: I'm the author of expkv and family) instead of l3keys (and for the sake of it implements everything in 2e/plain style instead of L3).

Niceties in this answer:

  • The undefined keys are checked whether they are numeric input (\myexam@ifnumber)
  • For undefined keys macros will be examined and not stringified (expkv feature)
  • Only keys which didn't get a value will be handled this way, keys that got a value will directly throw an error
  • I use the data and dataT handlers by expkv which not only store data but at the same time work as a boolean switch tracking whether they were set or not (since groups will not work we have to reset them manually, but that'd be the case with clearing them as well)

Aside: You shouldn't name the internals of your code as belonging to the exam module, I'd consider that somewhat taken if I hack into an exam class, and would use a different name (hence the following uses myexam in all internals).

\documentclass{exam} \title{Know your US States and Capitals!} \makeatletter \long\protected\def\myexam@ifnumber#1% {% \begingroup \afterassignment\myexam@ifnumber@a \count0=0\iffalse{\fi#1}{#1}% } \long\protected\def\myexam@ifnumber@a {% \endgroup \expandafter\myexam@ifnumber@b\expandafter{\iffalse}\fi } \long\def\myexam@ifnumber@b#1#2% {% \if\relax\detokenize{#1}\relax \if\relax\detokenize\expandafter{\romannumeral`\^^@#2}\relax \expandafter\@gobble \fi \else \expandafter\@secondofthree \fi \@firstoftwo } \RequirePackage{expkv-def} \ekvsetdef\myexam@setitem{myexam/item} \ekvdefinekeys{myexam/item} { data points = \myexam@points ,dataT label = \myexam@label@data ,dataT hello = \myexam@hello@data ,protected unknown noval = \myexam@ifnumber{#2} {\myexam@setitem{points={#2}}} {\PackageError{myexam}{Unknown key or missing value for `#1'}{}} } \protected\def\myexam@init {% \let\myexam@points\@secondoftwo \let\myexam@label@data\@gobble \let\myexam@hello@data\@gobble } \newcommand*\myexam@label{\label} \newcommand \myexam@hello[1]{[Hello, #1!]} \NewCommandCopy\myexam@orig@doitem\@doitem \RenewDocumentCommand{\@doitem}{+O{}} {% \myexam@init \myexam@setitem{#1}% \myexam@points {\myexam@orig@doitem[}{\expandafter\myexam@orig@doitem\@gobble}]% \myexam@label@data\myexam@label \myexam@hello@data\myexam@hello } \makeatother \begin{document} \begin{questions} \question[label=CA,points=3] What is the capital of California? \begin{choices} \choice Los Angeles \choice Sacramento \choice San Francisco \choice San Diego \end{choices} \question[hello=world,label=TX] Which city serves as the capital of Texas? \begin{choices} \choice Dallas \choice Houston \choice Austin \choice San Antonio \end{choices} \question[2] What is the capital of Florida? \begin{choices} \choice Miami \choice Orlando \choice Jacksonville \choice Tallahassee \end{choices} % non-keyword argument '3' is interpreted as points \question[label=NY,3] Which city is the capital of New York? \begin{choices} \choice Buffalo \choice Albany \choice New York City \choice Syracuse \end{choices} \question[hello=Donald,points=7] What is the capital of Nevada? \begin{choices} \choice Reno \choice Henderson \choice Carson City \choice Las Vegas \end{choices} % non-keyword argument '6' is ignored, or rather it is read at the wrong time \question[hello=Duck,6] Which city serves as the capital of Colorado? \begin{choices} \choice Denver \choice Boulder \choice Colorado Springs \choice Aurora \end{choices} \end{questions} See Question~\ref{NY} for New York. \end{document} 
2

My original post was a bit of an XY Problem. What I want (Y) is to set several keys in the optional argument to the \question command, and have them be processed (or effectively processed) at different times. I also would like to keep as much of the “old” functionality as possible, in that non-keyword arguments are assumed to be a point value.

The question I asked (X) was in the title: Can I set a group of the unknown key handler? And as Jasper pointed out in the comments, the answer is no. It's in the manual, in fact on the very page I had open when I wrote my original post. Oops.

As for Y, there are several ways to do it.

With key groups

The partial solution in my original post is to declare a group of keys to be processed first, to set the point value and determine if \@readpoints is expanded. The rest of the keys are processed after \item@points@pageinfo.

Handling unknown keys as point values

The groups can be flipped: declare the group of keys to be processed last instead, and the excluded ones can be set using \keys_set_exclude_groups. This would handle unknown keys first.

The disadvantage would be having to purposefully set a key group for what I expect will be the much larger group.

Without handling unknown keys as point values

Maybe just don't do it? If I'm editing a question that has a free text optional argument, and I'm adding keyword arguments to it, I can at the same time change the free text argument to a points keyval. And letting the kernel complain about an unknown key passed is a reasonable way to remind me to do that.

I could customize the error message to remind users that if keys are passed in the optional argument, points need to be set with a points key.

Without key groups

Another way is to process all the keys at the beginning, and let the key handlers make sure the values are used at the right time.

With variables

User @cfr has contributed an answer like this, where the label key sets a variable \l_exam_label_tl. After \item@points@pageinfo is expanded, the code checks whether this variable is set, and calls a copy of \label on it (IIRC).

I like it. On the other hand, it seems like a lot of setup code.

With hooks

Like in cfr's answer, keys only need to be set at the beginning of the command. But hooks can be used to pass their effects to the right time. The unknown key handler makes a new call to key_set, passing the “key” as the value of a points key.

\keys_define:nn {exam/item} { points .tl_set:N = \l_exam_points_tl, hello .code:n = {\AddToHookNext{cmd/item@points@pageinfo/after}{[Hello,~#1!]}}, label .code:n = {\AddToHookNext{cmd/item@points@pageinfo/after}{\label{#1}}}, unknown .code:n = { \keys_set:ne {exam/item} {points=\l_keys_key_str} }, } 

This seems like an odd combination of high- and low-level code, but it seems to work and it's not too cumbersome.

I still haven't decided which implementation to go with. But I appreciate the suggestions.

3
  • 1
    you can use the l3 interface for hooks if you don't want to mix high/low level. you'd still be mixing l2/l3, but at least that's only 2 coding styles rather than 3. see lthooks-doc. Commented Apr 19 at 18:11
  • @cfr Is that \hook_gput_next_code:nn? Commented Apr 19 at 22:00
  • 1
    yes. .......... Commented Apr 19 at 22:03

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.