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:

enter image description here

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}

enter image description here


\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}

enter image description here