Smart-expansion of a range to a list of numbers
With a little bit of code you can make yourself a parser. I defined \makeProblems{<integer list>}{<code>}
for you, in which <integer list>
is a comma separated list of numbers where <x>-<y>
is parsed as the list of integers between <x>
and <y>
, inclusive. The function parses the list of numbers and then iterates over the generated list, and makes the current number available for <code>
as #1
. For example:
\makeProblems{1,3-7, 9, 14, 52}{Do something with #1.\par}
prints:
The code is long because, as the function takes user input, the function takes extra care to make sure that the <integer list>
doesn't contain wrong input.
\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\tl_new:N \l_ryanj_list_tl
\NewDocumentCommand \makeProblems { m +m }
{
\tl_clear:N \l_ryanj_list_tl
\exp_args:Nx \clist_map_function:nN {#1} \__ryanj_parse_item:n
\tl_map_inline:Nn \l_ryanj_list_tl {#2}
}
\cs_new_protected:Npn \__ryanj_add_item:n #1
{ \tl_put_right:Nn \l_ryanj_list_tl { {#1} } }
\cs_new_protected:Npn \__ryanj_parse_item:n #1
{
\__ryanj_if_number:nTF {#1}
{ \__ryanj_add_item:n {#1} }
{
\str_if_in:nnTF {#1} {-}
{ \exp_args:Nf \__ryanj_parse_range:n { \tl_to_str:n {#1} } }
{ \msg_error:nnn { ryanj } { invalid-number } {#1} }
}
}
\cs_new_protected:Npn \__ryanj_parse_range:n #1
{ \__ryanj_parse_range:nw {#1} #1 \q_mark }
\cs_new_protected:Npn \__ryanj_parse_range:nw #1#2-#3 \q_mark
{
\__ryanj_validate_number:nn {#1} {#2}
\__ryanj_validate_number:nn {#1} {#3}
\int_step_function:nnnN {#2} { 1 } {#3} \__ryanj_add_item:n
\use_none:n \q_stop
}
\cs_new_protected:Npn \__ryanj_validate_number:nn #1 #2
{
\__ryanj_if_number:nF {#2}
{
\msg_error:nnnn { ryanj } { invalid-number-in-range } {#2} {#1}
\use_none_delimit_by_q_stop:w
}
}
\msg_new:nnn { ryanj } { invalid-range } { Invalid~range~`#1'. }
\msg_new:nnn { ryanj } { invalid-number } { Invalid~number~`#1'. }
\msg_new:nnn { ryanj } { invalid-number-in-range } { Invalid~number~`#1'~in~range~`#2'. }
\prg_new_conditional:Npnn \__ryanj_if_number:n #1 { T, F, TF }
{
\tl_if_empty:oTF
{ \tex_romannumeral:D - 0#1 \exp_stop_f: }
{
\tl_if_empty:nTF {#1}
{ \prg_return_false: }
{ \prg_return_true: }
}
{ \prg_return_false: }
}
% For older expl3:
\prg_set_protected_conditional:Npnn \str_if_in:nn #1#2 { T , F , TF }
{
\use:x
{ \tl_if_in:nnTF { \tl_to_str:n {#1} } { \tl_to_str:n {#2} } }
{ \prg_return_true: } { \prg_return_false: }
}
\ExplSyntaxOff
\begin{document}
\makeProblems{1,3-7, 9, 14, 52}{Do something with #1.\par}
\end{document}
For the picky mammals, here's a version that understands negative numbers. Negative numbers can be input naturally with a sign, like -4
. In a number range the first -
right after the first number is parsed as the range indicator, not a sign, so 1-3
is 1,2,3
, 1--3
is 1,0,-1,-2,-3
, -1-3
is -1,0,1,2,3
, -1--3
is -1,-2,-3
, and -1---3--
is gibberish :-)
I wouldn't recommend using this syntax for it can be a tad confusing. I'd go for another character to denote the range, then negative numbers would "just work". I also enabled reversed ranges, so 1-3
yields 1,2,3
and 3-1
yields 3,2,1
.
\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\tl_new:N \l_ryanj_list_tl
\tl_new:N \l_ryanj_sign_tl
\tl_new:N \l_ryanj_parsing_tl
\bool_new:N \l_ryanj_first_bool
\bool_new:N \l_got_range_bool
\bool_new:N \l_ryanj_ranged_bool
\int_new:N \l_ryanj_rangea_int
\int_new:N \l_ryanj_rangeb_int
\NewDocumentCommand \makeProblems { m +m }
{
\tl_clear:N \l_ryanj_list_tl
\exp_args:Nx \clist_map_function:nN {#1} \__ryanj_parse_item:n
\tl_map_inline:Nn \l_ryanj_list_tl {#2}
}
\cs_new_protected:Npn \__ryanj_add_item:n #1
{ \tl_put_right:Nn \l_ryanj_list_tl { {#1} } }
\cs_generate_variant:Nn \__ryanj_add_item:n { V }
\cs_new_protected:Npn \__ryanj_parse_item:n #1
{
\tl_clear:N \l_ryanj_sign_tl
\bool_set_true:N \l_ryanj_first_bool
\bool_set_false:N \l_ryanj_ranged_bool
\bool_set_false:N \l_got_range_bool
\tl_set:Nn \l_ryanj_parsing_tl {#1}
\__ryanj_parse:w #1 ~ \q_recursion_tail \q_recursion_stop
}
\cs_new_protected:Npn \__ryanj_parse:w #1
{
\tl_if_single_token:nF {#1} { \__ryanj_abort_item:nnw { braced-item } {#1} }
\quark_if_recursion_tail_stop_do:Nn #1
{ \__ryanj_parse_terminate: }
\token_if_eq_charcode:NNTF - #1
{
\bool_if:NTF \l_ryanj_first_bool
{ \tl_set:Nn \l_ryanj_sign_tl { - } }
{
\bool_if:NTF \l_got_range_bool
{ \tl_set:Nn \l_ryanj_sign_tl { - } }
{ \bool_set_true:N \l_got_range_bool }
}
\__ryanj_parse:w
}
{ \__ryanj_grab_number:w #1 }
}
\cs_new_protected:Npn \__ryanj_grab_number:w #1
{
\quark_if_recursion_tail_stop_do:nn {#1} { \msg_error:nnn { ryanj } { invalid-number } {} }
\__ryanj_if_number:nF {#1} { \__ryanj_abort_item:nnw { invalid-number } {#1} }
\bool_if:NTF \l_ryanj_first_bool
{ \tex_afterassignment:D \__ryanj_after_first: \l_ryanj_rangea_int }
{ \tex_afterassignment:D \__ryanj_after_second: \l_ryanj_rangeb_int }
= \l_ryanj_sign_tl #1
}
\cs_new_protected:Npn \__ryanj_after_first:
{
\bool_set_false:N \l_ryanj_first_bool
\tl_clear:N \l_ryanj_sign_tl
\__ryanj_parse:w
}
\cs_new_protected:Npn \__ryanj_after_second:
{
\bool_set_true:N \l_ryanj_ranged_bool
\bool_set_false:N \l_got_range_bool
\tl_clear:N \l_ryanj_sign_tl
\__ryanj_parse_terminate:w
}
\cs_new_protected:Npn \__ryanj_parse_terminate:w #1 \q_recursion_stop
{ \tl_trim_spaces_apply:nN {#1} \__ryanj_ckeck_leftover:n }
\cs_new_protected:Npn \__ryanj_ckeck_leftover:n #1
{
\quark_if_recursion_tail_stop:n {#1}
\msg_error:nnn { ryanj } { invalid-number } {#1}
\use_none:n \q_recursion_stop
\__ryanj_parse_terminate:
}
\cs_new_protected:Npn \__ryanj_parse_terminate:
{
\bool_if:NT \l_got_range_bool
{ \msg_error:nnn { ryanj } { invalid-number } { - } }
\bool_if:NTF \l_ryanj_ranged_bool
{ \__ryanj_inject_range: }
{ \__ryanj_add_item:V \l_ryanj_rangea_int }
}
\cs_new:Npn \__ryanj_inject_range:
{
% To allow reversed ranges:
\int_compare:nNnT \l_ryanj_rangea_int > \l_ryanj_rangeb_int
{ \tl_set:Nn \l_ryanj_sign_tl { - } }
{ \tl_set:Nn \l_ryanj_sign_tl { } }
%
\int_step_function:nnnN
{ \l_ryanj_rangea_int }
{ \l_ryanj_sign_tl 1 }
{ \l_ryanj_rangeb_int }
\__ryanj_add_item:n
}
\cs_new:Npn \__ryanj_abort_item:nnw #1 #2 #3 \q_recursion_stop
{ \msg_error:nnn { ryanj } {#1} {#2} }
\prg_new_conditional:Npnn \__ryanj_if_number:n #1 { T, F, TF }
{
\tl_if_empty:oTF
{ \tex_romannumeral:D - 0#1 \exp_stop_f: }
{ \prg_return_true: }
{ \prg_return_false: }
}
\msg_new:nnn { ryanj } { invalid-number }
{ Invalid~number~`#1'~in~\tl_use:N \l_ryanj_parsing_tl. }
\msg_new:nnn { ryanj } { braced-item }
{ Invalid~braced~item~`#1'~in~\tl_use:N \l_ryanj_parsing_tl. }
\ExplSyntaxOff
\begin{document}
\makeProblems{-1,3-5,-3--5,-3-5,3--5,9,14,52}{Do something with #1.\par}
\end{document}
I map the given comma separated list; each item is examined and if it contains a hyphen, a loop is done; in any case, an integer is added to a sequence.
Finally the sequence is expanded with separators between the items; optionally this token list is saved to a macro.
\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\NewDocumentCommand{\expandlist}{om}
{
\ryanj_expandlist:n { #2 }
\IfNoValueTF { #1 }
{
\ryanj_expandlist_print:
}
{
\ryanj_expandlist_store:N #1
}
}
\tl_new:N \l_ryan_expandlist_tl
\seq_new:N \l__ryan_expandlist_seq
\cs_new_protected:Nn \ryanj_expandlist:n
{
\seq_clear:N \l__ryan_expandlist_seq
\clist_map_function:nN { #1 } \__ryan_expandlist_item:n
\tl_set:Nx \l_ryan_expandlist_tl
{
\seq_use:Nnnn \l__ryan_expandlist_seq {~and~} { ,~ } { ,~and~ }
}
}
\cs_new_protected:Nn \__ryan_expandlist_item:n
{
\__ryan_expandlist_item:w #1 - - \q_stop
}
\cs_new_protected:Npn \__ryan_expandlist_item:w #1 - #2 - #3 \q_stop
{
\tl_if_blank:nTF { #2 }
{
\seq_put_right:Nn \l__ryan_expandlist_seq { #1 }
}
{
\int_step_inline:nnn { #1 } { #2 } { \seq_put_right:Nn \l__ryan_expandlist_seq { ##1 } }
}
}
\cs_new:Nn \ryanj_expandlist_print:
{
\tl_use:N \l_ryan_expandlist_tl
}
\cs_new_protected:Nn \ryanj_expandlist_store:N
{
\tl_if_exist:NF #1
{
\tl_set_eq:NN #1 \l_ryan_expandlist_tl
}
}
\ExplSyntaxOff
\begin{document}
\expandlist{3-7, 9, 14, 52}
\expandlist{1}
\expandlist{1,4}
\expandlist{1-2}
\expandlist[\foo]{3-7, 9, 14, 52}
\texttt{\meaning\foo}
\end{document}
\documentclass{article}
\usepackage{listofitems,pgffor}
\newcommand\makeProblems[2]{%
\setsepchar{,/-}%
\readlist*\numlist{#1}%
\def\z##1{#2\par}%
\foreachitem\zz\in\numlist[]{%
\ifnum\listlen\numlist[\zzcnt]=1\relax\z{\zz}\else
\itemtomacro\numlist[\zzcnt,1]\tmpA
\itemtomacro\numlist[\zzcnt,2]\tmpB
\foreach\zzz in {\tmpA,...,\tmpB}{%
\z{\zzz}}%
\fi
}%
}
\begin{document}
\makeProblems{1,3-7, 9, 14-16, 52}{Do something with #1.}
\end{document}