% Author     : C. Pierquet
% Credits    : V. Dao, aka ankaa3908, for improvements with latex3 code
% licence    : Released under the LaTeX Project Public License v1.3c or later, see http://www.latex-project.org/lppl.txt

\NeedsTeXFormat{LaTeX2e}
\ProvidesExplPackage{commalists-tools-l3}{2026-06-11}{0.20d}{Basic operations for numeral comma separated lists}

%------History
% 0.20d  Improvements with latex3 (tks to ankaa3908, again !)
% 0.20c  Shuffle list
% 0.20b  Extract element with cyclic version of list
% 0.20a  New commands (sublist / transform / slice / merge / intersection / ...)
% 0.1.9  Improvements with latex3 (tks to ankaa3908)
% 0.1.8  Get index 
% 0.1.7  Improvements with latex3
% 0.1.6  Initial version (prefixed macros due to legacy package)

%------Variables
\seq_new:N \l__commalists_tmpa_seq
\seq_new:N \l__commalists_tmpb_seq
\int_new:N   \l__commalists_tmpa_int
\int_new:N   \l__commalists_tmpb_int
\fp_new:N    \l__commalists_tmpa_fp

%------Show list
\NewDocumentCommand\ctshowlist{ O { , } m }
  {
    % #1 = sep
    % #2 = list
    \seq_set_split:Nne \l__commalists_tmpa_seq { , } { #2 }
    \seq_use:Nn \l__commalists_tmpa_seq { #1 }
  }

%------Sort list, reverse list
\NewDocumentCommand\ctsortasclist{ s m O { \mysortedlist } }
  {
    % #1 = star (just printing)
    % #2 = list
    % #3 = output macro (non-starred, default \mysortedlist)
    \group_begin:
    \seq_set_split:Nne \l__commalists_tmpa_seq { , } { #2 }
    \seq_sort:Nn \l__commalists_tmpa_seq {
      \fp_compare:nNnTF { ##1 } > { ##2 }
        { \sort_return_swapped: }
        { \sort_return_same: }
      }
    \IfBooleanTF { #1 } %if star := just printing // if not, storing
      { \seq_use:Nn \l__commalists_tmpa_seq { , } }
      { \tl_gset:Ne #3 { \seq_use:Nn \l__commalists_tmpa_seq { , } } }
    \group_end:
  }

\NewDocumentCommand\ctsortdeslist{ s m O { \mysortedlist } }
  {
    % #1 = star (just printing)
    % #2 = list
    % #3 = output macro (non-starred, default \mysortedlist)
    \group_begin:
    \seq_set_split:Nne \l__commalists_tmpa_seq { , } { #2 }
    \seq_sort:Nn \l__commalists_tmpa_seq {
      \fp_compare:nNnTF { ##1 } < { ##2 }
        { \sort_return_swapped: }
        { \sort_return_same: }
      }
    \IfBooleanTF { #1 } %if star := just printing // if not, storing
      { \seq_use:Nn \l__commalists_tmpa_seq { , } }
      { \tl_gset:Ne #3 { \seq_use:Nn \l__commalists_tmpa_seq { , } } }
    \group_end:
  }

\NewDocumentCommand\ctreverselist{ s m O { \reverselist } }
  {
    % #1 = star (just printing)
    % #2 = list
    % #3 = output macro (non-starred, default \reverselist)
    \group_begin:
    \seq_set_split:Nne \l__commalists_tmpa_seq { , } { #2 }
    \seq_reverse:N \l__commalists_tmpa_seq
    \IfBooleanTF { #1 } %if star := just printing // if not, storing
      { \seq_use:Nn \l__commalists_tmpa_seq { , } }
      { \tl_gset:Ne #3 { \seq_use:Nn \l__commalists_tmpa_seq { , } } }
    \group_end:
  }

%------Test in list
\NewDocumentCommand\ctboolvalinlist{ m m O { \resisinlist } }
  {
    % #1 = element to test
    % #2 = list
    % #3 = output macro (non-starred, default \resisinlist)
    \group_begin:
    \seq_set_split:Nne \l__commalists_tmpa_seq { , } { #2 }
    \seq_if_in:NnTF \l__commalists_tmpa_seq { #1 }
      { \tl_gset:Nn #3 { 1 } }
      { \tl_gset:Nn #3 { 0 } }
    \group_end:
  }

\NewDocumentCommand\cttestifvalinlist{ m m }
  {
    % #1 = element to test
    % #2 = list
    \seq_set_split:Nne \l__commalists_tmpa_seq { , } { #2 }
    \seq_if_in:NnTF \l__commalists_tmpa_seq { #1 }
      { \use_i:nn }
      { \use_ii:nn }
  }

%------Add, remove
\NewDocumentCommand\ctaddvalinlist{ s m m }
  {
    % #1 = star (just printing)
    % #2 = value to add
    % #3 = list
    \IfBooleanTF { #1 }
      { #3, #2 }
      { \tl_set:Ne #3 { \tl_use:N #3 , #2 } }
  }

\NewDocumentCommand\ctremovevalinlist{ s m m O { \mytmplist } }
  {
    % #1 = star (just printing)
    % #2 = value to remove
    % #3 = list
    % #4 = output macro (non-starred, default \mytmplist)
    \group_begin:
    \seq_set_split:Nne \l__commalists_tmpa_seq { , } { #3 }
    \seq_remove_all:Nn \l__commalists_tmpa_seq { #2 }
    \IfBooleanTF { #1 }
      { \seq_use:Nn \l__commalists_tmpa_seq { , } }
      { \tl_gset:Ne #4 { \seq_use:Nn \l__commalists_tmpa_seq { , } } }
    \group_end:
  }

%------Count
\NewDocumentCommand\ctcountvalinlist{ s m m O { \rescount } }
  {
    % #1 = star (just printing)
    % #2 = value to count
    % #3 = list
    % #4 = output macro (non-starred, default \rescount)
    \group_begin:
    \int_zero:N \l__commalists_tmpa_int
    \seq_set_split:Nne \l__commalists_tmpa_seq { , } { #3 }
    \seq_map_inline:Nn \l__commalists_tmpa_seq
      { \str_if_eq:nnT { ##1 } { #2 } { \int_incr:N \l__commalists_tmpa_int } }
    \IfBooleanTF{ #1 }
      { \int_use:N \l__commalists_tmpa_int }
      { \tl_gset:Ne #4 { \int_use:N \l__commalists_tmpa_int } }
    \group_end:
  }

%------Calculus
\NewDocumentCommand\ctminoflist{ s m O { \resmin } }
  {
    % #1 = star (just printing)
    % #2 = list
    % #3 = output macro (non-starred, default \resmin)
    \IfBooleanTF { #1 } 
      { \fp_eval:n { min(#2) } }
      { \tl_set:Ne #3 { \fp_eval:n { min(#2) } } }
  }

\NewDocumentCommand\ctmaxoflist{ s m O { \resmax } }
  {
    % #1 = star (just printing)
    % #2 = list
    % #3 = output macro (non-starred, default \resmax)
    \IfBooleanTF { #1 }
      { \fp_eval:n { max(#2) } }
      { \tl_set:Ne #3 { \fp_eval:n { max(#2) } } }
  }

\NewDocumentCommand\ctsumoflist{ s m O { \ressum } }
  {
    % #1 = star (just printing)
    % #2 = list
    % #3 = output macro (non-starred, default \ressum)
    \group_begin:
    \seq_set_split:Nne \l__commalists_tmpa_seq { , } { #2 }
    \fp_zero:N \l__commalists_tmpa_fp
    \fp_set:Nn \l__commalists_tmpa_fp 
      { \exp_args:Ne \fp_eval:n { \seq_use:Nn \l__commalists_tmpa_seq { + } } }
    \IfBooleanTF{ #1 }
      { \fp_use:N \l__commalists_tmpa_fp }
      { \tl_gset:Ne #3 { \fp_use:N \l__commalists_tmpa_fp } }
    \group_end:
  }

\NewDocumentCommand\ctprodoflist{ s m O { \resprod } }
  {
    % #1 = star (just printing)
    % #2 = list
    % #3 = output macro (non-starred, default \resprod)
    \group_begin:
    \seq_set_split:Nne \l__commalists_tmpa_seq { , } { #2 }
    \fp_zero:N \l__commalists_tmpa_fp
    \fp_set:Nn \l__commalists_tmpa_fp 
      { \exp_args:Ne \fp_eval:n { \seq_use:Nn \l__commalists_tmpa_seq { * } } }
    \IfBooleanTF{ #1 }
      { \fp_use:N \l__commalists_tmpa_fp }
      { \tl_gset:Ne #3 { \fp_use:N \l__commalists_tmpa_fp } }
    \group_end:
  }

\NewDocumentCommand\ctmeanoflist{ s m O { \resmean } }
  {
    % #1 = star (just printing)
    % #2 = list
    % #3 = output macro (non-starred, default \resmean)
    \group_begin:
    \seq_set_split:Nne \l__commalists_tmpa_seq { , } { #2 }
    \fp_zero:N \l__commalists_tmpa_fp
    \fp_set:Nn \l__commalists_tmpa_fp 
      { \exp_args:Ne \fp_eval:n 
        { 
          ( \seq_use:Nn \l__commalists_tmpa_seq { + } )
          /
          \seq_count:N \l__commalists_tmpa_seq
        } 
      }
    \IfBooleanTF{ #1 }
      { \fp_use:N \l__commalists_tmpa_fp }
      { \tl_gset:Ne #3 { \fp_use:N \l__commalists_tmpa_fp } }
    \group_end:
  }

\NewDocumentCommand\ctlenoflist{ s m O { \resmylen } }
  {
    % #1 = star (just printing)
    % #2 = list
    % #3 = output macro (non-starred, default \myreslen)
    \group_begin:
    \seq_set_split:Nne \l__commalists_tmpa_seq { , } { #2 }
    \IfBooleanTF { #1 }
      { \seq_count:N \l__commalists_tmpa_seq }
      { \tl_gset:Ne #3 { \seq_count:N \l__commalists_tmpa_seq } }
    \group_end:
  }

%------Get, index
\NewDocumentCommand\ctgetvaluefromlist{ s m m O { \resmyelt } }
  {
    % #1 = star (just printing)
    % #2 = list
    % #3 = index
    % #4 = output macro (non-starred, default \resmyelt)
    \group_begin:
    \seq_set_split:Nne \l__commalists_tmpa_seq { , } { #2 }
    \IfBooleanTF { #1 }
      { \seq_item:Nn \l__commalists_tmpa_seq { #3 } }
      { \tl_gset:Ne #4 { \seq_item:Nn \l__commalists_tmpa_seq { #3 } } }
    \group_end:
  }

% message d'erreur
\msg_new:nnn { commalist-tools } { index-zero }
  { ctgetvaluefromcycllist:~index~0~not~available }

\NewDocumentCommand\ctgetvaluefromcycllist{ s m m O { \resmyelt } }
  {
    % #1 = star (just printing)
    % #2 = list
    % #3 = index
    % #4 = output macro (default \resmyelt)
    \group_begin:
    \seq_set_split:Nne \l__commalists_tmpa_seq { , } { #2 }
    \int_compare:nNnTF { #3 } = { 0 }
      {
        \msg_warning:nn { commalist-tools } { index-zero }
        \tl_set_eq:Nn #4 \c_empty_tl
      }
      {
        \int_set:Nn \l__commalists_tmpa_int
          {
            \int_mod:nn
              { #3 }
              { \seq_count:N \l__commalists_tmpa_seq }
          }
      }
    \IfBooleanTF{ #1 }
      {
        \exp_args:NNV 
          \seq_item:Nn \l__commalists_tmpa_seq \l__commalists_tmpa_int 
      }
      { 
        \tl_gset:Ne #4 {
          \exp_args:NNV 
            \seq_item:Nn \l__commalists_tmpa_seq \l__commalists_tmpa_int } 
      }
    \group_end:
  }

\NewDocumentCommand\ctgetindexfromlist{ s m m O { \resmyindex } }
  {
    % #1 = star (just printing)
    % #2 = element
    % #3 = list
    % #4 = output macro (non-starred, default \resmyindex)
    \group_begin:
    \int_zero:N \l__commalists_tmpa_int
    \seq_set_split:Nne \l__commalists_tmpa_seq { , } { #3 }
    \seq_if_in:NnT \l__commalists_tmpa_seq { #2 }
      {
        \seq_map_inline:Nn \l__commalists_tmpa_seq
          {
            \int_incr:N \l__commalists_tmpa_int
            \str_if_eq:nnT { ##1 } { #2 }
              { \seq_map_break: }
          }
      }
    \IfBooleanTF { #1 }
      { \int_use:N \l__commalists_tmpa_int }
      { \tl_gset:Ne #4 { \int_use:N \l__commalists_tmpa_int } }
    \group_end:
  }

%------Sub-list
\seq_new:N \l__commalists_sub_seq
\int_new:N \l__commalists_begin_int
\int_new:N \l__commalists_end_int

\NewDocumentCommand\ctsublist{ s m m m O { \mysublist } }
  {
    % #1 = star (display only)
    % #2 = list
    % #3 = begin index (* = start from first)
    % #4 = end   index (* = go to last)
    % #5 = output macro (non-starred only, default \mysublist)
    \group_begin:
    \seq_set_split:Nne \l__commalists_tmpa_seq { , } { #2 }
    % --- resolve begin index
    \int_set:Nn \l__commalists_begin_int
      {
        \str_if_eq:nnTF { * } { #3 }
          { 1 }
          { #3 }
      }

    % --- resolve end index
    \int_set:Nn \l__commalists_end_int
      {
        \str_if_eq:nnTF { * } { #4 }
          { \seq_count:N \l__commalists_tmpa_seq }
          { #4 }
      }
      
    % --- iterate and collect items in [begin, end]
    \seq_map_indexed_inline:Nn \l__commalists_tmpa_seq
      {
        \bool_lazy_and:nnT
          { \int_compare_p:n { ##1 >= \l__commalists_begin_int } }
          { \int_compare_p:n { ##1 <= \l__commalists_end_int } }
          { \seq_put_right:Nn \l__commalists_sub_seq { ##2 } }
      }
    
    % --- output
    \IfBooleanTF { #1 }
      { \seq_use:Nn \l__commalists_sub_seq { , } }
      { \tl_gset:Ne #5 { \seq_use:Nn \l__commalists_sub_seq { , } } }
    \group_end:
  }

%------Slice-list
\seq_new:N \l__commalists_slice_seq
\tl_new:N \l__commalists_slice_formula_tl

\NewDocumentCommand\ctslicelist{ s m m O { \myslicelist } }
  {
    % #1 = star (display only)
    % #2 = list
    % #3 = formula in x (e.g. 2*x, 3*x+1, x^2)
    % #4 = output macro (non-starred only, default \myslicelist)
    \group_begin:
    \seq_set_split:Nne \l__commalists_tmpa_seq { , } { #2 }
    \seq_map_indexed_inline:Nn \l__commalists_tmpa_seq
      {
        \tl_set:Nn \l__commalists_slice_formula_tl { #3 }
        \tl_replace_all:Nnn \l__commalists_slice_formula_tl { x } { ##1 }
        \int_set:Nn \l__commalists_tmpa_int 
          { \fp_eval:n { \l__commalists_slice_formula_tl } }
        \int_compare:nNnTF
          { \l__commalists_tmpa_int }
          <
          { \seq_count:N \l__commalists_tmpa_seq +1 }
          {
            \seq_put_right:Ne \l__commalists_slice_seq
              { \seq_item:Nn \l__commalists_tmpa_seq 
                { \l__commalists_tmpa_int } 
              }
          }
          { \seq_map_break: }
      }
    % --- output
    \IfBooleanTF { #1 }
      { \seq_use:Nn \l__commalists_slice_seq { , } }
      { \tl_gset:Ne #4 { \seq_use:Nn \l__commalists_slice_seq { , } } }
    \group_end:
  }

%------Transform-list
\seq_new:N \l__commalists_transform_seq
\tl_new:N \l__commalists_transform_formula_tl

\NewDocumentCommand\cttransformlist{ s o m m O { \mytransformlist } }
  {
    % #1 = star (display only)
    % #2 = round
    % #3 = list
    % #4 = formula in x (e.g. 2*x+1, x^2, sqrt(x))
    % #5 = output macro (non-starred only, default \mytransformlist)
    \seq_clear:N \l__commalists_transform_seq
    \group_begin:
    \seq_set_split:Nne \l__commalists_tmpa_seq { , } { #3 }
    % --- iterate over each element, apply formula
    \seq_map_inline:Nn \l__commalists_tmpa_seq
      {
        % --- substitute x by current element value in formula
        \tl_set:Nn \l__commalists_transform_formula_tl { #4 }
        \tl_replace_all:Nnn \l__commalists_transform_formula_tl { x } { ##1 }
        % --- evaluate and collect result
        \IfNoValueTF{ #2 }
          {
            \seq_put_right:Ne \l__commalists_transform_seq
              { \fp_eval:n { \l__commalists_transform_formula_tl } }
          }
          {
            \seq_put_right:Ne \l__commalists_transform_seq
              { 
                \fp_eval:n {
                  round( \l__commalists_transform_formula_tl, #2 ) 
                } 
              }
          }
      }

    % --- output
    \IfBooleanTF { #1 }
      { \seq_use:Nn \l__commalists_transform_seq { , } }
      { \tl_gset:Ne #5 { \seq_use:Nn \l__commalists_transform_seq { , } } }
    \group_end:
  }

%------Unique list
\bool_new:N \l__commalists_uniq_asc_bool
\bool_new:N \l__commalists_uniq_des_bool

\keys_define:nn { commalists / uniq }
  {
    asc .bool_set:N   = \l__commalists_uniq_asc_bool,
    asc .default:n    = true,
    des .bool_set:N   = \l__commalists_uniq_des_bool,
    des .default:n    = true,
  }

\NewDocumentCommand\ctuniqlist{ s O { } m O { \myuniqlist } }
  {
    % #1 = star (display only)
    % #2 = options (asc, des)
    % #3 = list
    % #4 = output macro (non-starred only, default \myuniqlist)
    \group_begin:
    % parse keys
    \keys_set:nn { commalists / uniq } { #2 }

    \seq_set_split:Nne \l__commalists_tmpa_seq { , } { #3 }
    \seq_remove_duplicates:N \l__commalists_tmpa_seq

    % --- sort if requested
    \bool_case:n {
      { \l__commalists_uniq_asc_bool }
        {
          \seq_sort:Nn \l__commalists_tmpa_seq
            {
              \fp_compare:nNnTF { ##1 } > { ##2 }
                { \sort_return_swapped: }
                { \sort_return_same: }
            }
        }
      { \l__commalists_uniq_des_bool }
        {
          \seq_sort:Nn \l__commalists_tmpa_seq
            {
              \fp_compare:nNnTF { ##1 } < { ##2 }
                { \sort_return_swapped: }
                { \sort_return_same: }
            }
        }
    }

    % --- output
    \IfBooleanTF { #1 }
      { \seq_use:Nn \l__commalists_tmpa_seq { , } }
      { \tl_gset:Ne #4 { \seq_use:Nn \l__commalists_tmpa_seq { , } } }
    \group_end:
  }

%------Intersection
\seq_new:N \l__commalists_short_sequence_seq
\seq_new:N \l__commalists_long_sequence_seq
\seq_new:N \l__commalists_intersect_seq
\bool_new:N \l__commalists_intersect_asc_bool
\bool_new:N \l__commalists_intersect_des_bool

\keys_define:nn { commalists / intersect }
  {
    asc .bool_set:N   = \l__commalists_intersect_asc_bool,
    asc .default:n    = true,
    des .bool_set:N   = \l__commalists_intersect_des_bool,
    des .default:n    = true,
  }

\NewDocumentCommand\ctintersectlists{ s O { } m m O { \myintersectlist } }
  {
    % #1 = star (just printing)
    % #2 = options (asc, des)
    % #3 = list 1
    % #4 = list 2
    % #5 = output macro (non-starred, default \myintersectlist)
    \group_begin:
    % parse keys
    \keys_set:nn { commalists / intersect } { #2 }
    \seq_set_split:Nne \l__commalists_tmpa_seq { , } { #3 }
    \seq_set_split:Nne \l__commalists_tmpb_seq { , } { #4 }
    % --- keep the shortest list for iteration
    \int_set:Nn \l__commalists_tmpa_int { \seq_count:N \l__commalists_tmpa_seq }
    \int_set:Nn \l__commalists_tmpb_int { \seq_count:N \l__commalists_tmpb_seq }
    \int_compare:nNnTF { \l__commalists_tmpa_int } < { \l__commalists_tmpb_int }
      {
        \seq_set_eq:NN \l__commalists_short_sequence_seq \l__commalists_tmpa_seq
        \seq_set_eq:NN \l__commalists_long_sequence_seq  \l__commalists_tmpb_seq
      }
      {
        \seq_set_eq:NN \l__commalists_short_sequence_seq \l__commalists_tmpb_seq
        \seq_set_eq:NN \l__commalists_long_sequence_seq  \l__commalists_tmpa_seq
      }
    \seq_map_inline:Nn \l__commalists_short_sequence_seq
      {
        \seq_if_in:NnT \l__commalists_long_sequence_seq { ##1 }
          { \seq_put_right:Nn \l__commalists_intersect_seq { ##1 } }
      }
    \seq_remove_duplicates:N \l__commalists_intersect_seq
    % --- sort if requested
    \bool_case:n {
      { \l__commalists_intersect_asc_bool }
        {
          \seq_sort:Nn \l__commalists_intersect_seq
            {
              \fp_compare:nNnTF { ##1 } > { ##2 }
                { \sort_return_swapped: }
                { \sort_return_same: }
            }
        }
      { \l__commalists_intersect_des_bool }
        {
          \seq_sort:Nn \l__commalists_intersect_seq
            {
              \fp_compare:nNnTF { ##1 } < { ##2 }
                { \sort_return_swapped: }
                { \sort_return_same: }
            }
        }
    }
    % --- output
    \IfBooleanTF { #1 }
      { \seq_use:Nn \l__commalists_intersect_seq { , } }
      { \tl_gset:Ne #5 { \seq_use:Nn \l__commalists_intersect_seq { , } } }
    \group_end:
  }

%------Union
\seq_new:N \l__commalists_merge_seq
\bool_new:N \l__commalists_merge_asc_bool
\bool_new:N \l__commalists_merge_des_bool

\keys_define:nn { commalists / merge }
  {
    asc .bool_set:N   = \l__commalists_merge_asc_bool,
    asc .default:n    = true,
    des .bool_set:N   = \l__commalists_merge_des_bool,
    des .default:n    = true,
  }

\NewDocumentCommand\ctmergelists{ s O { } m m O { \mymergelist } }
  {
    % #1 = star (just printing)
    % #2 = options (asc, des)
    % #3 = list 1
    % #4 = list 2
    % #5 = output macro (non-starred, default \mymergelist)
    \group_begin:
    % keys reading
    \keys_set:nn { commalists / merge } { #2 }
    \seq_set_split:Nne \l__commalists_tmpa_seq { , } { #3 }
    \seq_set_split:Nne \l__commalists_tmpb_seq { , } { #4 }
    \seq_concat:NNN \l__commalists_merge_seq 
      \l__commalists_tmpa_seq 
      \l__commalists_tmpb_seq
    \seq_remove_duplicates:N \l__commalists_merge_seq
    % --- sort if requested
    \bool_case:n {
      { \l__commalists_merge_asc_bool }
        {
          \seq_sort:Nn \l__commalists_merge_seq
            {
              \fp_compare:nNnTF { ##1 } > { ##2 }
                { \sort_return_swapped: }
                { \sort_return_same: }
            }
        }
      { \l__commalists_merge_des_bool }
        {
          \seq_sort:Nn \l__commalists_merge_seq
            {
              \fp_compare:nNnTF { ##1 } < { ##2 }
                { \sort_return_swapped: }
                { \sort_return_same: }
            }
        }
    }
    % --- output
    \IfBooleanTF { #1 }
      { \seq_use:Nn \l__commalists_merge_seq { , } }
      { \tl_gset:Ne #5 { \seq_use:Nn \l__commalists_merge_seq { , } } }
    \group_end:
  }

%------Difference

\seq_new:N \l__commalists_diff_seq
\bool_new:N \l__commalists_diff_asc_bool
\bool_new:N \l__commalists_diff_des_bool

\keys_define:nn { commalists / diff }
  {
    asc .bool_set:N   = \l__commalists_diff_asc_bool,
    asc .default:n    = true,
    des .bool_set:N   = \l__commalists_diff_des_bool,
    des .default:n    = true,
  }

\NewDocumentCommand\ctdifflist{ s O { } m m O { \mydifflist } }
  {
    % #1 = star (display only)
    % #2 = options (asc, des)
    % #3 = list A
    % #4 = list B
    % #5 = output macro (non-starred, default \mydifflist)
    \group_begin:
    % parse keys
    \keys_set:nn { commalists / diff } { #2 }
    % --- iterate A, keep elements absent from B
    \seq_set_split:Nne \l__commalists_tmpa_seq { , } { #3 }
    \seq_set_split:Nne \l__commalists_tmpb_seq { , } { #4 }
    \seq_map_inline:Nn \l__commalists_tmpa_seq
      {
        \seq_if_in:NnF \l__commalists_tmpb_seq { ##1 }
          { \seq_put_right:Nn \l__commalists_diff_seq { ##1 } }
      }
    \seq_remove_duplicates:N \l__commalists_diff_seq
    % --- sort if requested
    \bool_case:n {
      { \l__commalists_diff_asc_bool }
        {
          \seq_sort:Nn \l__commalists_diff_seq
            {
              \fp_compare:nNnTF { ##1 } > { ##2 }
                { \sort_return_swapped: }
                { \sort_return_same: }
            }
        }
      { \l__commalists_diff_des_bool }
        {
          \seq_sort:Nn \l__commalists_diff_seq
            {
              \fp_compare:nNnTF { ##1 } < { ##2 }
                { \sort_return_swapped: }
                { \sort_return_same: }
            }
        }
    }
    % --- output
    \IfBooleanTF { #1 }
      { \seq_use:Nn \l__commalists_diff_seq { , } }
      { \tl_gset:Ne #5 { \seq_use:Nn \l__commalists_diff_seq { , } } }
    \group_end:
  }

%------Symmetric difference
\seq_new:N \l__commalists_symdiff_seq
\bool_new:N \l__commalists_symdiff_asc_bool
\bool_new:N \l__commalists_symdiff_des_bool

\keys_define:nn { commalists / symdiff }
{
  asc .bool_set:N   = \l__commalists_symdiff_asc_bool,
  asc .default:n    = true,
  des .bool_set:N   = \l__commalists_symdiff_des_bool,
  des .default:n    = true,
}

\NewDocumentCommand\ctsymdifflist{ s O { } m m O { \mysymdifflist } }
  {
    % #1 = star (display only)
    % #2 = options (asc, des)
    % #3 = list A
    % #4 = list B
    % #5 = output macro (non-starred, default \mysymdifflist)
    \group_begin:
    % parse keys
    \keys_set:nn { commalists / symdiff } { #2 }
    \seq_set_split:Nne \l__commalists_tmpa_seq { , } { #3 }
    \seq_set_split:Nne \l__commalists_tmpb_seq { , } { #4 }
    % --- elements in A absent from B
    \seq_map_inline:Nn \l__commalists_tmpa_seq
      {
        \seq_if_in:NnF \l__commalists_tmpb_seq { ##1 }
          {
            \seq_if_in:NnF \l__commalists_symdiff_seq { ##1 }
              { \seq_put_right:Nn \l__commalists_symdiff_seq { ##1 } }
          }
      }
    % --- elements in B absent from A
    \seq_map_inline:Nn \l__commalists_tmpb_seq
      {
        \seq_if_in:NnF \l__commalists_tmpa_seq { ##1 }
        {
          \seq_if_in:NnF \l__commalists_symdiff_seq { ##1 }
            { \seq_put_right:Nn \l__commalists_symdiff_seq { ##1 } }
        }
      }
    % --- sort if requested
    \bool_case:n {
      { \l__commalists_symdiff_asc_bool }
        {
          \seq_sort:Nn \l__commalists_symdiff_seq
            {
              \fp_compare:nNnTF { ##1 } > { ##2 }
                { \sort_return_swapped: }
                { \sort_return_same: }
            }
        }
      { \l__commalists_symdiff_des_bool }
        {
          \seq_sort:Nn \l__commalists_symdiff_seq
            {
              \fp_compare:nNnTF { ##1 } < { ##2 }
                { \sort_return_swapped: }
                { \sort_return_same: }
            }
        }
    }
    % --- output
    \IfBooleanTF { #1 }
      { \seq_use:Nn \l__commalists_symdiff_seq { , } }
      { \tl_gset:Ne #5 { \seq_use:Nn \l__commalists_symdiff_seq { , } } }
    \group_end:
  }

%------Shuffle list
\NewDocumentCommand\ctshufflelist{ s m O { \myshuffledlist } }
  {
    % #1 = star (just printing)
    % #2 = list (content)
    % #3 = output macro (non-starred, default \myshuffledlist)
    \group_begin:
    \seq_set_split:Nne \l__commalists_tmpa_seq { , } { #2 }
    \seq_shuffle:N \l__commalists_tmpa_seq
    % --- output
    \IfBooleanTF{ #1 }
      { \seq_use:Nn \l__commalists_tmpa_seq { , } }
      { \tl_gset:Ne #3 { \seq_use:Nn \l__commalists_tmpa_seq { , } } }
    \group_end:
  }

\endinput