% Author     : C. Pierquet
% 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-03-24}{0.20b}{Basic operations for numeral comma separated lists}

%------History
% 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
\clist_new:N \l__commalists_var_clist
\clist_new:N \l__commalists_varb_clist
\int_new:N   \l__commalists_tmpc_int
\fp_new:N    \l__commalists_tmpa_fp

%------Show list
\NewDocumentCommand\ctshowlist{ O { , } m }
% #1 = sep
% #2 = list
{
  \clist_set:Ne \l__commalists_var_clist { #2 }
  \clist_use:Nn \l__commalists_var_clist { #1 }
}

%------Sort list, reverse list
\NewDocumentCommand\ctsortasclist{ s m O { \mysortedlist } }
% #1 = star (just printing)
% #2 = list
% #3 = output macro (non-starred, default \mysortedlist)
{
  \clist_set:Ne \l__commalists_var_clist {#2}
  \clist_sort:Nn \l__commalists_var_clist {
    \fp_compare:nNnTF { ##1 } > { ##2 }
      { \sort_return_swapped: }
      { \sort_return_same: }
  }
  \IfBooleanTF { #1 } %if star := just printing // if not, storing
    {
      \clist_use:Nn \l__commalists_var_clist { , }
    }
    {
      \tl_gset:Ne \g_tmpa_tl { \l__commalists_var_clist }
      \tl_gset:NV #3 \g_tmpa_tl
    }
}

\NewDocumentCommand\ctsortdeslist{ s m O { \mysortedlist } }
% #1 = star (just printing)
% #2 = list
% #3 = output macro (non-starred, default \mysortedlist)
{
  \clist_set:Ne \l__commalists_var_clist { #2 }
  \clist_sort:Nn \l__commalists_var_clist {
    \fp_compare:nNnTF { ##1 } < { ##2 }
      { \sort_return_swapped: }
      { \sort_return_same: }
  }
  \IfBooleanTF { #1 } %if star := just printing // if not, storing
    {
      \clist_use:Nn \l__commalists_var_clist { , }
    }
    {
      \tl_gset:Ne \g_tmpa_tl { \l__commalists_var_clist }
      \tl_gset:NV #3 \g_tmpa_tl
    }
}

\NewDocumentCommand\ctreverselist{ s m O { \reverselist } }
% #1 = star (just printing)
% #2 = list
% #3 = output macro (non-starred, default \reverselist)
{
  \clist_set:Ne \l__commalists_var_clist { #2 }
  \IfBooleanTF { #1 } %if star := just printing // if not, storing
  {
    \clist_reverse:N \l__commalists_var_clist
    \clist_use:Nn \l__commalists_var_clist { , }
  }
  {
    \clist_set_eq:NN #3 \l__commalists_var_clist
    \clist_greverse:N #3
  }
}

%------Test in list
\NewDocumentCommand\ctboolvalinlist{ m m O { \resisinlist } }
% #1 = element to test
% #2 = list
% #3 = output macro (non-starred, default \resisinlist)
{
  \clist_set:Ne \l__commalists_var_clist {#2}
  
  \tl_gset:Nn #3 { 0 }
  
  \clist_map_inline:Nn \l__commalists_var_clist
  {
    \tl_if_eq:neT { ##1 } { #1 }
    {
      \tl_gset:Nn #3 { 1 }
      \clist_map_break:
    }
  }
}

\NewDocumentCommand\cttestifvalinlist{ m m m m }
% #1 = element to test
% #2 = list
% #3 = true branch
% #4 = false branch
{
  \clist_set:Ne \l__commalists_var_clist { #2 }
  
  \bool_set_false:N \l_tmpa_bool
  
  \clist_map_inline:Nn \l__commalists_var_clist
  {
    \tl_if_eq:neT { ##1 } { #1 }
    {
      \bool_set_true:N \l_tmpa_bool
      \clist_map_break:
    }
  }
  \bool_if:NTF \l_tmpa_bool { #3 } { #4 }
}

%------Add, remove
\NewDocumentCommand\ctaddvalinlist{ s m m }
% #1 = star (just printing)
% #2 = value to add
% #3 = list
{
  \IfBooleanTF { #1 }
  {
    #3, #2
  }
  {
    \tl_gset: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)
{
  \clist_set:Ne \l__commalists_var_clist { #3 }
  
  \clist_clear_new:N \l_tmpb_clist
  
  \clist_map_inline:Nn \l__commalists_var_clist
  {
    
    \tl_if_eq:neF { ##1 } { #2 }
    {
      \clist_put_right:Nn \l_tmpb_clist { ##1 }
    }
  }
  
  \IfBooleanTF { #1 }
  {
    \clist_use:Nn \l_tmpb_clist { , }
  }
  {
    \tl_gset:Ne \g_tmpa_tl { \clist_use:Nn \l_tmpb_clist { , } }
    \tl_gset:NV #4 \g_tmpa_tl
  }
}

%------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)
{
  \clist_set:Ne \l__commalists_var_clist { #3 }
  
  \int_zero:N \l_tmpa_int
  \clist_map_inline:Nn \l__commalists_var_clist
  {
    \tl_if_eq:neT { ##1 } { #2 }
    {
      \int_incr:N \l_tmpa_int
    }
  }
  \IfBooleanTF { #1 }
  {
    \int_use:N \l_tmpa_int
  }
  {
    \tl_gset:NV #4 \l_tmpa_int
  }
}

%------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_gset: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_gset: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)
{
  \clist_set:Ne \l__commalists_var_clist { #2 }
  \fp_set:Nn \l__commalists_tmpa_fp { 0 }
  \clist_map_inline:Nn \l__commalists_var_clist
  {
    \fp_set:Nn \l__commalists_tmpa_fp 
      { \fp_eval:n { \l__commalists_tmpa_fp + (##1) } }
  }
  \IfBooleanTF { #1 }
  {
    \fp_use:N \l__commalists_tmpa_fp
  }
  {
    \tl_gset:Ne #3 { \fp_use:N \l__commalists_tmpa_fp }
  }
}

\NewDocumentCommand\ctprodoflist{ s m O { \resprod } }
% #1 = star (just printing)
% #2 = list
% #3 = output macro (non-starred, default \resprod)
{
  \clist_set:Ne \l__commalists_var_clist { #2 }
  \fp_set:Nn \l__commalists_tmpa_fp { 1 }
  \clist_map_inline:Nn \l__commalists_var_clist
  {
    \fp_set:Nn \l__commalists_tmpa_fp
      { \fp_eval:n { \l__commalists_tmpa_fp * (##1) } }
  }
  \IfBooleanTF { #1 }
  {
    \fp_use:N \l__commalists_tmpa_fp
  }
  {
    \tl_gset:Ne #3 { \fp_use:N \l__commalists_tmpa_fp }
  }
}

\NewDocumentCommand\ctmeanoflist{ s m O { \resmean } }
% #1 = star (just printing)
% #2 = list
% #3 = output macro (non-starred, default \resmean)
{
  \clist_set:Ne \l__commalists_var_clist { #2 }
  \fp_set:Nn \l__commalists_tmpa_fp { 0 }
  \clist_map_inline:Nn \l__commalists_var_clist
  {
    \fp_set:Nn \l__commalists_tmpa_fp
      { \fp_eval:n { \l__commalists_tmpa_fp + (##1) } }
  }
  \fp_set:Nn \l__commalists_tmpa_fp
    {
      \fp_eval:n {
        ( \l__commalists_tmpa_fp )
        /
        ( \clist_count:N \l__commalists_var_clist ) 
      } 
    }
  \IfBooleanTF { #1 }
  {
    \fp_use:N \l__commalists_tmpa_fp
  }
  {
    \tl_gset:Ne #3 { \fp_use:N \l__commalists_tmpa_fp }
  }
}

\NewDocumentCommand\ctlenoflist{ s m O { \resmylen } }
% #1 = star (just printing)
% #2 = list
% #3 = output macro (non-starred, default \myreslen)
{
  \clist_set:Ne \l__commalists_var_clist { #2 }
  \IfBooleanTF { #1 }
  {
    \clist_count:N \l__commalists_var_clist
  }
  {
    \tl_gset:Ne #3 { \clist_count:N \l__commalists_var_clist }
  }
}

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

\NewDocumentCommand\ctgetvaluefromcycllist{ s m m O { \resmyelt } }
  % #1 = star (just printing)
  % #2 = list
  % #3 = index
  % #4 = output macro (default \resmyelt)
{
    \clist_set:Ne \l__commalists_var_clist { #2 }
    \int_set:Nn \l_tmpa_int { \clist_count:N \l__commalists_var_clist }
    %\int_set:Ne \l_tmpb_int { #3 }
    \int_set:Nn \l_tmpb_int { \int_eval:n { #3 } }
    % test indice 0
    \int_compare:nNnTF { \l_tmpb_int } = { 0 }
      {
        \msg_warning:nn { commalist-tools } { index-zero }
        \tl_gset:Nn #4 { }
      }
      {
        % modulo positif
        \int_compare:nNnTF { \l_tmpb_int } > { 0 }
          { \int_set:Nn \l_tmpb_int { \l_tmpb_int - 1 } }
          { }
        \int_set:Nn \l__commalists_tmpc_int
          { \int_mod:nn { \l_tmpb_int } { \l_tmpa_int } }
        \int_compare:nNnT { \l__commalists_tmpc_int } < { 0 }
          { \int_add:Nn \l__commalists_tmpc_int { \l_tmpa_int } }
        \int_add:Nn \l__commalists_tmpc_int { 1 }
        % récupération
        \IfBooleanTF { #1 }
          {
            \clist_item:Nn \l__commalists_var_clist { \l__commalists_tmpc_int } 
          }
          {
            \tl_gset:Ne #4 
              {
                \clist_item:Nn 
                  \l__commalists_var_clist 
                  { \l__commalists_tmpc_int }
              }
          }
      }
}
% message d'erreur
\msg_new:nnn { commalist-tools } { index-zero }
  { ctgetvaluefromcycllist:~index~0~not~available }

\NewDocumentCommand\ctgetindexfromlist{ s m m O { \resmyindex } }
% #1 = star (just printing)
% #2 = list
% #3 = element
% #3 = output macro (non-starred, default \resmyindex)
{
  \IfBooleanTF { #1 }
  {
    \ct_get_index:nn { #3 } { #2 }
  }
  {
    \ct_get_index:nnN { #3 } { #2 } #4
  }
}

\cs_new:Npn \ct_get_index:nn #1 #2
{
  \int_zero:N \l_tmpa_int
  \bool_set_false:N \l_tmpa_bool
  \tl_set:Ne \l_tmpb_tl { #2 }
  \exp_args:No \clist_map_inline:nn { #1 }
  {
    \int_incr:N \l_tmpa_int
    \str_if_eq:nVT { ##1 } \l_tmpb_tl
    {
      \bool_set_true:N \l_tmpa_bool
      \clist_map_break:
    }
  }
  \bool_if:NTF \l_tmpa_bool
  { \int_use:N \l_tmpa_int }
  { 0 }
}

\cs_new:Npn \ct_get_index:nnN #1 #2 #3
{
  \int_zero:N \l_tmpa_int
  \bool_set_false:N \l_tmpa_bool
  \tl_set:Ne \l_tmpb_tl { #2 }
  \exp_args:No \clist_map_inline:nn { #1 }
  {
    \int_incr:N \l_tmpa_int
    \str_if_eq:nVT { ##1 } \l_tmpb_tl
    {
      \bool_set_true:N \l_tmpa_bool
      \clist_map_break:
    }
  }
  \bool_if:NTF \l_tmpa_bool
  { \tl_gset:Ne #3 { \int_use:N \l_tmpa_int } }
  { \tl_gset:Nn #3 { 0 } }
}

%------Sub-list
\clist_new:N \l__commalists_sub_clist
\int_new:N \l__commalists_begin_int
\int_new:N \l__commalists_end_int
\int_new:N \l__commalists_idx_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)
{
  \clist_set:Ne \l__commalists_var_clist { #2 }
  \clist_clear:N \l__commalists_sub_clist
  
  % --- resolve begin index
  \tl_if_eq:nnTF { #3 } { * }
  { \int_set:Nn \l__commalists_begin_int { 1 } }
  { \int_set:Nn \l__commalists_begin_int { #3 } }
    
  % --- resolve end index
  \tl_if_eq:nnTF { #4 } { * }
  { \int_set:Nn \l__commalists_end_int 
    { \clist_count:N \l__commalists_var_clist } 
  }
  { \int_set:Nn \l__commalists_end_int { #4 } }
    
  % --- iterate and collect items in [begin, end]
  \int_set:Nn \l__commalists_idx_int { 0 }
  \clist_map_inline:Nn \l__commalists_var_clist
  {
    \int_incr:N \l__commalists_idx_int
    \int_compare:nTF
    { \l__commalists_idx_int >= \l__commalists_begin_int }
    {
       \int_compare:nTF
        { \l__commalists_idx_int <= \l__commalists_end_int }
        {
          \clist_put_right:Nn \l__commalists_sub_clist { ##1 }
        }
        { \clist_map_break: }  % early exit : idx > end, stop
    }
    { }
  }
  
  % --- output
  \IfBooleanTF { #1 }
  {
    \clist_use:Nn \l__commalists_sub_clist { , }
  }
  {
    \tl_gset:Ne #5 { \clist_use:Nn \l__commalists_sub_clist { , } }
  }
}

%------Slice-list
\clist_new:N \l__commalists_slice_clist
\int_new:N \l__commalists_slice_x_int
\int_new:N \l__commalists_slice_idx_int
\int_new:N \l__commalists_slice_len_int
\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)
{
  \clist_set:Ne \l__commalists_var_clist { #2 }
  \clist_clear:N \l__commalists_slice_clist
  \int_set:Nn \l__commalists_slice_len_int 
    { \clist_count:N \l__commalists_var_clist }
  \int_set:Nn \l__commalists_slice_x_int { 1 }
  
  % --- iterate x = 1..len, evaluate formula, collect item if index in range
  \clist_map_inline:Nn \l__commalists_var_clist
  {
    % substitute x by current value in formula, then evaluate
    \tl_set:Nn \l__commalists_slice_formula_tl { #3 }
    \tl_replace_all:Nne \l__commalists_slice_formula_tl { x }
      { \int_use:N \l__commalists_slice_x_int }
    \int_set:Nn \l__commalists_slice_idx_int
      { \fp_eval:n { \l__commalists_slice_formula_tl } }
    
    % stop as soon as the computed index goes out of range
    \int_compare:nTF
      { \int_use:N \l__commalists_slice_idx_int >= 1 }
    {
      \int_compare:nTF
        { \int_use:N \l__commalists_slice_idx_int 
          <= 
          \int_use:N \l__commalists_slice_len_int }
      {
        \clist_put_right:Ne \l__commalists_slice_clist
          { \clist_item:Nn \l__commalists_var_clist 
            { \l__commalists_slice_idx_int } 
          }
        \int_incr:N \l__commalists_slice_x_int
      }
      {
        \clist_map_break:
      }
    }
    {
      \clist_map_break:
    }
  }
  
  % --- output
  \IfBooleanTF { #1 }
  {
    \clist_use:Nn \l__commalists_slice_clist { , }
  }
  {
    \tl_gset:Ne #4 { \clist_use:Nn \l__commalists_slice_clist { , } }
  }
}

%------Transform-list
\clist_new:N \l__commalists_transform_clist
\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)
{
  \clist_set:Ne \l__commalists_var_clist { #3 }
  \clist_clear:N \l__commalists_transform_clist
  % --- iterate over each element, apply formula
  \clist_map_inline:Nn \l__commalists_var_clist
  {
    % --- 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 }
    {
      \clist_put_right:Ne \l__commalists_transform_clist
      { \fp_eval:n { \l__commalists_transform_formula_tl } }
    }
    {
      \clist_put_right:Ne \l__commalists_transform_clist
      { \fp_eval:n { round( \l__commalists_transform_formula_tl , #2 ) } }
    }
  }
  
  % --- output
  \IfBooleanTF { #1 }
  {
    \clist_use:Nn \l__commalists_transform_clist { , }
  }
  {
    \tl_gset:Ne #5 { \clist_use:Nn \l__commalists_transform_clist { , } }
  }
}

%------Unique list
\clist_new:N \l__commalists_uniq_clist
\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)
{
  % reset booleans
  \bool_set_false:N \l__commalists_uniq_asc_bool
  \bool_set_false:N \l__commalists_uniq_des_bool
  
  % parse keys
  \keys_set:nn { commalists / uniq } { #2 }
  
  \clist_set:Ne \l__commalists_var_clist { #3 }
  \clist_clear:N \l__commalists_uniq_clist
  
  % --- iterate and collect unique elements
  \clist_map_inline:Nn \l__commalists_var_clist
  {
    \clist_if_in:NnF \l__commalists_uniq_clist { ##1 }
    {
      \clist_put_right:Nn \l__commalists_uniq_clist { ##1 }
    }
  }
  
  % --- sort if requested
  \bool_if:NT \l__commalists_uniq_asc_bool
  {
    \clist_sort:Nn \l__commalists_uniq_clist
    {
      \fp_compare:nNnTF { ##1 } > { ##2 }
        { \sort_return_swapped: }
        { \sort_return_same: }
    }
  }
  \bool_if:NT \l__commalists_uniq_des_bool
  {
    \clist_sort:Nn \l__commalists_uniq_clist
    {
      \fp_compare:nNnTF { ##1 } < { ##2 }
        { \sort_return_swapped: }
        { \sort_return_same: }
    }
  }
  
  % --- output
  \IfBooleanTF { #1 }
  {
    \clist_use:Nn \l__commalists_uniq_clist { , }
  }
  {
    \tl_gset:Ne #4 { \clist_use:Nn \l__commalists_uniq_clist { , } }
  }
}

%------Intersection
\clist_new:N \l__commalists_intersect_clist
\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)
{
  % set lists
  \clist_set:Ne \l__commalists_var_clist { #3 }
  \clist_set:Ne \l__commalists_varb_clist { #4 }
  
  % reset booleans
  \bool_set_false:N \l__commalists_intersect_asc_bool
  \bool_set_false:N \l__commalists_intersect_des_bool

  % parse keys
  \keys_set:nn { commalists / intersect } { #2 }

  % clear res list
  \clist_clear:N \l__commalists_intersect_clist

  % --- iterate and collect common elements
  \clist_map_inline:Nn \l__commalists_var_clist
  {
    \clist_if_in:NnT \l__commalists_varb_clist { ##1 }
    {
      % if ok, add
      \clist_if_in:NnF \l__commalists_intersect_clist { ##1 }
      {
        \clist_put_right:Nn \l__commalists_intersect_clist { ##1 }
      }
    }
  }

  % --- sort if asked
  \bool_if:NT \l__commalists_intersect_asc_bool
  {
    \clist_sort:Nn \l__commalists_intersect_clist
    {
      \fp_compare:nNnTF { ##1 } > { ##2 }
        { \sort_return_swapped: }
        { \sort_return_same: }
    }
  }
  \bool_if:NT \l__commalists_intersect_des_bool
  {
    \clist_sort:Nn \l__commalists_intersect_clist
    {
      \fp_compare:nNnTF { ##1 } < { ##2 }
        { \sort_return_swapped: }
        { \sort_return_same: }
    }
  }

  % --- output
  \IfBooleanTF { #1 }
  {
    \clist_if_empty:NTF \l__commalists_intersect_clist
    { }
    { \clist_use:Nn \l__commalists_intersect_clist { , } }
  }
  {
    \tl_gset:Ne #5 { \clist_use:Nn \l__commalists_intersect_clist { , } }
  }
}

%------Union
\clist_new:N \l__commalists_merge_clist
\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)
{
  % reinit of booleans
  \bool_set_false:N \l__commalists_merge_asc_bool
  \bool_set_false:N \l__commalists_merge_des_bool

  % keys reading
  \keys_set:nn { commalists / merge } { #2 }

  % set lists
  \clist_set:Ne \l__commalists_var_clist { #3 }
  \clist_set:Ne \l__commalists_varb_clist { #4 }
  
  % init merge list
  \clist_clear:N \l__commalists_merge_clist

  % add element list1, w/o doublons
  \clist_map_inline:Nn \l__commalists_var_clist
  {
    \clist_if_in:NnF \l__commalists_merge_clist { ##1 }
    {
      \clist_put_right:Nn \l__commalists_merge_clist { ##1 }
    }
  }

  % add element list2, w/o doublons
  \clist_map_inline:Nn \l__commalists_varb_clist
  {
    \clist_if_in:NnF \l__commalists_merge_clist { ##1 }
    {
      \clist_put_right:Nn \l__commalists_merge_clist { ##1 }
    }
  }

  % sort if asked
  \bool_if:NT \l__commalists_merge_asc_bool
  {
    \clist_sort:Nn \l__commalists_merge_clist
    {
      \fp_compare:nNnTF { ##1 } > { ##2 }
        { \sort_return_swapped: }
        { \sort_return_same: }
    }
  }
  \bool_if:NT \l__commalists_merge_des_bool
  {
    \clist_sort:Nn \l__commalists_merge_clist
    {
      \fp_compare:nNnTF { ##1 } < { ##2 }
        { \sort_return_swapped: }
        { \sort_return_same: }
    }
  }

  % --- Output
  \IfBooleanTF { #1 }
  {
    \clist_use:Nn \l__commalists_merge_clist { , }
  }
  {
    \tl_gset:Ne #5 { \clist_use:Nn \l__commalists_merge_clist { , } }
  }
}

%------Difference
\clist_new:N \l__commalists_diff_clist
\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)
{
  % set lists
  \clist_set:Ne \l__commalists_var_clist  { #3 }
  \clist_set:Ne \l__commalists_varb_clist { #4 }

  % reset booleans
  \bool_set_false:N \l__commalists_diff_asc_bool
  \bool_set_false:N \l__commalists_diff_des_bool

  % parse keys
  \keys_set:nn { commalists / diff } { #2 }

  % clear result list
  \clist_clear:N \l__commalists_diff_clist

  % --- iterate A, keep elements absent from B
  \clist_map_inline:Nn \l__commalists_var_clist
  {
    \clist_if_in:NnF \l__commalists_varb_clist { ##1 }
    {
      % avoid duplicates in result
      \clist_if_in:NnF \l__commalists_diff_clist { ##1 }
      {
        \clist_put_right:Nn \l__commalists_diff_clist { ##1 }
      }
    }
  }

  % --- sort if asked
  \bool_if:NT \l__commalists_diff_asc_bool
  {
    \clist_sort:Nn \l__commalists_diff_clist
    {
      \fp_compare:nNnTF { ##1 } > { ##2 }
        { \sort_return_swapped: }
        { \sort_return_same: }
    }
  }
  \bool_if:NT \l__commalists_diff_des_bool
  {
    \clist_sort:Nn \l__commalists_diff_clist
    {
      \fp_compare:nNnTF { ##1 } < { ##2 }
        { \sort_return_swapped: }
        { \sort_return_same: }
    }
  }

  % --- output
  \IfBooleanTF { #1 }
  {
    \clist_use:Nn \l__commalists_diff_clist { , }
  }
  {
    \tl_gset:Ne #5 { \clist_use:Nn \l__commalists_diff_clist { , } }
  }
}

%------Symmetric difference
\clist_new:N \l__commalists_symdiff_clist
\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)
{
  % set lists
  \clist_set:Ne \l__commalists_var_clist  { #3 }
  \clist_set:Ne \l__commalists_varb_clist { #4 }

  % reset booleans
  \bool_set_false:N \l__commalists_symdiff_asc_bool
  \bool_set_false:N \l__commalists_symdiff_des_bool

  % parse keys
  \keys_set:nn { commalists / symdiff } { #2 }

  % clear result list
  \clist_clear:N \l__commalists_symdiff_clist

  % --- elements in A absent from B
  \clist_map_inline:Nn \l__commalists_var_clist
  {
    \clist_if_in:NnF \l__commalists_varb_clist { ##1 }
    {
      \clist_if_in:NnF \l__commalists_symdiff_clist { ##1 }
      {
        \clist_put_right:Nn \l__commalists_symdiff_clist { ##1 }
      }
    }
  }

  % --- elements in B absent from A
  \clist_map_inline:Nn \l__commalists_varb_clist
  {
    \clist_if_in:NnF \l__commalists_var_clist { ##1 }
    {
      \clist_if_in:NnF \l__commalists_symdiff_clist { ##1 }
      {
        \clist_put_right:Nn \l__commalists_symdiff_clist { ##1 }
      }
    }
  }

  % --- sort if asked
  \bool_if:NT \l__commalists_symdiff_asc_bool
  {
    \clist_sort:Nn \l__commalists_symdiff_clist
    {
      \fp_compare:nNnTF { ##1 } > { ##2 }
        { \sort_return_swapped: }
        { \sort_return_same: }
    }
  }
  \bool_if:NT \l__commalists_symdiff_des_bool
  {
    \clist_sort:Nn \l__commalists_symdiff_clist
    {
      \fp_compare:nNnTF { ##1 } < { ##2 }
        { \sort_return_swapped: }
        { \sort_return_same: }
    }
  }

  % --- output
  \IfBooleanTF { #1 }
  {
    \clist_use:Nn \l__commalists_symdiff_clist { , }
  }
  {
    \tl_gset:Ne #5 { \clist_use:Nn \l__commalists_symdiff_clist { , } }
  }
}

\endinput