% 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} \def\ctfiledate{2026/07/23}% \def\ctfileversion{0.20e}% \ProvidesExplPackage{commalists-tools-l3}{\ctfiledate}{\ctfileversion}{Basic operations for numeral comma separated lists} %------History % 0.20e Bugfix with cycl + separator improvements % 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 (entrée ET sortie) % #2 = list \seq_set_split:Nne \l__commalists_tmpa_seq { #1 } { #2 } \seq_use:Nn \l__commalists_tmpa_seq { #1 } } \NewDocumentCommand\ctshowlistwithsep{ O { , } m m } { % #1 = sep (entrée) % #2 = list % #3 = sep (sortie) \seq_set_split:Nne \l__commalists_tmpa_seq { #1 } { #2 } \seq_use:Nn \l__commalists_tmpa_seq { #3 } } %------Sort list, reverse list \NewDocumentCommand\ctsortasclist{ s O { , } m O { \mysortedlist } } { % #1 = star (just printing) % #2 = sep (défaut virgule) % #3 = list % #4 = output macro (non-starred, default \mysortedlist) \group_begin: \seq_set_split:Nne \l__commalists_tmpa_seq { #2 } { #3 } \seq_sort:Nn \l__commalists_tmpa_seq { \fp_compare:nNnTF { ##1 } > { ##2 } { \sort_return_swapped: } { \sort_return_same: } } \IfBooleanTF { #1 } { \seq_use:Nn \l__commalists_tmpa_seq { #2 } } { \tl_gset:Ne #4 { \seq_use:Nn \l__commalists_tmpa_seq { #2 } } } \group_end: } \NewDocumentCommand\ctsortdeslist{ s O { , } m O { \mysortedlist } } { % #1 = star (just printing) % #2 = sep (défaut virgule) % #3 = list % #4 = output macro (non-starred, default \mysortedlist) \group_begin: \seq_set_split:Nne \l__commalists_tmpa_seq { #2 } { #3 } \seq_sort:Nn \l__commalists_tmpa_seq { \fp_compare:nNnTF { ##1 } < { ##2 } { \sort_return_swapped: } { \sort_return_same: } } \IfBooleanTF { #1 } { \seq_use:Nn \l__commalists_tmpa_seq { #2 } } { \tl_gset:Ne #4 { \seq_use:Nn \l__commalists_tmpa_seq { #2 } } } \group_end: } \NewDocumentCommand\ctreverselist{ s O { , } m O { \reverselist } } { % #1 = star (just printing) % #2 = sep (défaut virgule) % #3 = list % #4 = output macro (non-starred, default \reverselist) \group_begin: \seq_set_split:Nne \l__commalists_tmpa_seq { #2 } { #3 } \seq_reverse:N \l__commalists_tmpa_seq \IfBooleanTF { #1 } { \seq_use:Nn \l__commalists_tmpa_seq { #2 } } { \tl_gset:Ne #4 { \seq_use:Nn \l__commalists_tmpa_seq { #2 } } } \group_end: } %------Test in list \NewDocumentCommand\ctboolvalinlist{ O { , } m m O { \resisinlist } } { % #1 = sep (défaut virgule) % #2 = element to test % #3 = list % #4 = output macro (non-starred, default \resisinlist) \group_begin: \seq_set_split:Nne \l__commalists_tmpa_seq { #1 } { #3 } \seq_if_in:NnTF \l__commalists_tmpa_seq { #2 } { \tl_gset:Nn #4 { 1 } } { \tl_gset:Nn #4 { 0 } } \group_end: } \NewDocumentCommand\cttestifvalinlist{ O { , } m m } { % #1 = sep (défaut virgule) % #2 = element to test % #3 = list \seq_set_split:Nne \l__commalists_tmpa_seq { #1 } { #3 } \seq_if_in:NnTF \l__commalists_tmpa_seq { #2 } { \use_i:nn } { \use_ii:nn } } %------Add, remove \NewDocumentCommand\ctaddvalinlist{ s O { , } m m } { % #1 = star (just printing) % #2 = sep (défaut virgule) % #3 = value to add % #4 = list \IfBooleanTF { #1 } { #4 #2 #3 } { \tl_set:Ne #4 { \tl_use:N #4 #2 #3 } } } \NewDocumentCommand\ctremovevalinlist{ s O { , } m m O { \mytmplist } } { % #1 = star (just printing) % #2 = sep (défaut virgule) % #3 = value to remove % #4 = list % #5 = output macro (non-starred, default \mytmplist) \group_begin: \seq_set_split:Nne \l__commalists_tmpa_seq { #2 } { #4 } \seq_remove_all:Nn \l__commalists_tmpa_seq { #3 } \IfBooleanTF { #1 } { \seq_use:Nn \l__commalists_tmpa_seq { #2 } } { \tl_gset:Ne #5 { \seq_use:Nn \l__commalists_tmpa_seq { #2 } } } \group_end: } %------Count \NewDocumentCommand\ctcountvalinlist{ s O { , } m m O { \rescount } } { % #1 = star (just printing) % #2 = sep (défaut virgule) % #3 = value to count % #4 = list % #5 = output macro (non-starred, default \rescount) \group_begin: \int_zero:N \l__commalists_tmpa_int \seq_set_split:Nne \l__commalists_tmpa_seq { #2 } { #4 } \seq_map_inline:Nn \l__commalists_tmpa_seq { \str_if_eq:nnT { ##1 } { #3 } { \int_incr:N \l__commalists_tmpa_int } } \IfBooleanTF{ #1 } { \int_use:N \l__commalists_tmpa_int } { \tl_gset:Ne #5 { \int_use:N \l__commalists_tmpa_int } } \group_end: } %------Calculus \NewDocumentCommand\ctminoflist{ s m O { \resmin } } { % #1 = star (just printing) % #2 = list (séparateur virgule obligatoire pour fp_eval min) % #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 (séparateur virgule obligatoire pour fp_eval max) % #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 O { , } m O { \ressum } } { % #1 = star (just printing) % #2 = sep (défaut virgule) % #3 = list % #4 = output macro (non-starred, default \ressum) \group_begin: \seq_set_split:Nne \l__commalists_tmpa_seq { #2 } { #3 } \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 #4 { \fp_use:N \l__commalists_tmpa_fp } } \group_end: } \NewDocumentCommand\ctprodoflist{ s O { , } m O { \resprod } } { % #1 = star (just printing) % #2 = sep (défaut virgule) % #3 = list % #4 = output macro (non-starred, default \resprod) \group_begin: \seq_set_split:Nne \l__commalists_tmpa_seq { #2 } { #3 } \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 #4 { \fp_use:N \l__commalists_tmpa_fp } } \group_end: } \NewDocumentCommand\ctmeanoflist{ s O { , } m O { \resmean } } { % #1 = star (just printing) % #2 = sep (défaut virgule) % #3 = list % #4 = output macro (non-starred, default \resmean) \group_begin: \seq_set_split:Nne \l__commalists_tmpa_seq { #2 } { #3 } \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 #4 { \fp_use:N \l__commalists_tmpa_fp } } \group_end: } \NewDocumentCommand\ctlenoflist{ s O { , } m O { \resmylen } } { % #1 = star (just printing) % #2 = sep (défaut virgule) % #3 = list % #4 = output macro (non-starred, default \myreslen) \group_begin: \seq_set_split:Nne \l__commalists_tmpa_seq { #2 } { #3 } \IfBooleanTF { #1 } { \seq_count:N \l__commalists_tmpa_seq } { \tl_gset:Ne #4 { \seq_count:N \l__commalists_tmpa_seq } } \group_end: } %------Get, index \NewDocumentCommand\ctgetvaluefromlist{ s O { , } m m O { \resmyelt } } { % #1 = star (just printing) % #2 = sep (défaut virgule) % #3 = list % #4 = index % #5 = output macro (non-starred, default \resmyelt) \group_begin: \seq_set_split:Nne \l__commalists_tmpa_seq { #2 } { #3 } \IfBooleanTF { #1 } { \seq_item:Nn \l__commalists_tmpa_seq { #4 } } { \tl_gset:Ne #5 { \seq_item:Nn \l__commalists_tmpa_seq { #4 } } } \group_end: } % message d'erreur \msg_new:nnn { commalist-tools } { index-zero } { ctgetvaluefromcycllist:~index~0~not~available } \NewDocumentCommand\ctgetvaluefromcycllist{ s O { , } m m O { \resmyelt } } { % #1 = star (just printing) % #2 = sep (défaut virgule) % #3 = list % #4 = index % #5 = output macro (default \resmyelt) \group_begin: \seq_set_split:Nne \l__commalists_tmpa_seq { #2 } { #3 } \int_compare:nNnTF { #4 } = { 0 } { \msg_warning:nn { commalist-tools } { index-zero } \tl_set_eq:Nn #5 \c_empty_tl } { \int_set:Nn \l__commalists_tmpa_int { \int_mod:nn { #4 } { \seq_count:N \l__commalists_tmpa_seq } } % si mod = 0 (index multiple de la longueur) → dernier élément \int_compare:nNnT { \l__commalists_tmpa_int } = { 0 } { \int_set:Nn \l__commalists_tmpa_int { \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 #5 { \exp_args:NNV \seq_item:Nn \l__commalists_tmpa_seq \l__commalists_tmpa_int } } } \group_end: } \NewDocumentCommand\ctgetindexfromlist{ s O { , } m m O { \resmyindex } } { % #1 = star (just printing) % #2 = sep (défaut virgule) % #3 = element % #4 = list % #5 = output macro (non-starred, default \resmyindex) \group_begin: \int_zero:N \l__commalists_tmpa_int \seq_set_split:Nne \l__commalists_tmpa_seq { #2 } { #4 } \seq_if_in:NnT \l__commalists_tmpa_seq { #3 } { \seq_map_inline:Nn \l__commalists_tmpa_seq { \int_incr:N \l__commalists_tmpa_int \str_if_eq:nnT { ##1 } { #3 } { \seq_map_break: } } } \IfBooleanTF { #1 } { \int_use:N \l__commalists_tmpa_int } { \tl_gset:Ne #5 { \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 O { , } m m m O { \mysublist } } { % #1 = star (display only) % #2 = sep (défaut virgule) % #3 = list % #4 = begin index (* = start from first) % #5 = end index (* = go to last) % #6 = output macro (non-starred only, default \mysublist) \group_begin: \seq_set_split:Nne \l__commalists_tmpa_seq { #2 } { #3 } % --- resolve begin index \int_set:Nn \l__commalists_begin_int { \str_if_eq:nnTF { * } { #4 } { 1 } { #4 } } % --- resolve end index \int_set:Nn \l__commalists_end_int { \str_if_eq:nnTF { * } { #5 } { \seq_count:N \l__commalists_tmpa_seq } { #5 } } % --- 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 { #2 } } { \tl_gset:Ne #6 { \seq_use:Nn \l__commalists_sub_seq { #2 } } } \group_end: } %------Slice-list \seq_new:N \l__commalists_slice_seq \tl_new:N \l__commalists_slice_formula_tl \NewDocumentCommand\ctslicelist{ s O { , } m m O { \myslicelist } } { % #1 = star (display only) % #2 = sep (défaut virgule) % #3 = list % #4 = formula in x (e.g. 2*x, 3*x+1, x^2) % #5 = output macro (non-starred only, default \myslicelist) \group_begin: \seq_set_split:Nne \l__commalists_tmpa_seq { #2 } { #3 } \seq_map_indexed_inline:Nn \l__commalists_tmpa_seq { \tl_set:Nn \l__commalists_slice_formula_tl { #4 } \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 { #2 } } { \tl_gset:Ne #5 { \seq_use:Nn \l__commalists_slice_seq { #2 } } } \group_end: } %------Transform-list \seq_new:N \l__commalists_transform_seq \tl_new:N \l__commalists_transform_formula_tl \NewDocumentCommand\cttransformlist{ s O { , } d < > m m O { \mytransformlist } } { % #1 = star (display only) % #2 = sep (défaut virgule) % #3 = round (optionnel, entre < >, nombre de décimales) % #4 = list % #5 = formula in x (e.g. 2*x+1, x^2, sqrt(x)) % #6 = output macro (non-starred only, default \mytransformlist) \seq_clear:N \l__commalists_transform_seq \group_begin: % --- sécurité : [] explicite ne redonne pas le défaut O{,} en xparse \tl_if_empty:nTF { #2 } { \seq_set_split:Nne \l__commalists_tmpa_seq { , } { #4 } } { \seq_set_split:Nne \l__commalists_tmpa_seq { #2 } { #4 } } % --- 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 { #5 } \tl_replace_all:Nnn \l__commalists_transform_formula_tl { x } { ##1 } % --- evaluate and collect result \IfNoValueTF{ #3 } { \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, #3 ) } } } } % --- output \tl_if_empty:nTF { #2 } { \IfBooleanTF { #1 } { \seq_use:Nn \l__commalists_transform_seq { , } } { \tl_gset:Ne #6 { \seq_use:Nn \l__commalists_transform_seq { , } } } } { \IfBooleanTF { #1 } { \seq_use:Nn \l__commalists_transform_seq { #2 } } { \tl_gset:Ne #6 { \seq_use:Nn \l__commalists_transform_seq { #2 } } } } \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 { , } d < > m O { \myuniqlist } } { % #1 = star (display only) % #2 = sep (défaut virgule) % #3 = options (asc, des), entre < > % #4 = list % #5 = output macro (non-starred only, default \myuniqlist) \group_begin: % parse keys \IfNoValueF { #3 } { \keys_set:nn { commalists / uniq } { #3 } } \seq_set_split:Nne \l__commalists_tmpa_seq { #2 } { #4 } \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 { #2 } } { \tl_gset:Ne #5 { \seq_use:Nn \l__commalists_tmpa_seq { #2 } } } \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 { , } d < > m m O { \myintersectlist } } { % #1 = star (just printing) % #2 = sep (défaut virgule) % #3 = options (asc, des), entre < > % #4 = list 1 % #5 = list 2 % #6 = output macro (non-starred, default \myintersectlist) \group_begin: % parse keys \IfNoValueF { #3 } { \keys_set:nn { commalists / intersect } { #3 } } \seq_set_split:Nne \l__commalists_tmpa_seq { #2 } { #4 } \seq_set_split:Nne \l__commalists_tmpb_seq { #2 } { #5 } % --- 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 { #2 } } { \tl_gset:Ne #6 { \seq_use:Nn \l__commalists_intersect_seq { #2 } } } \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 { , } d < > m m O { \mymergelist } } { % #1 = star (just printing) % #2 = sep (défaut virgule) % #3 = options (asc, des), entre < > % #4 = list 1 % #5 = list 2 % #6 = output macro (non-starred, default \mymergelist) \group_begin: % keys reading \IfNoValueF { #3 } { \keys_set:nn { commalists / merge } { #3 } } \seq_set_split:Nne \l__commalists_tmpa_seq { #2 } { #4 } \seq_set_split:Nne \l__commalists_tmpb_seq { #2 } { #5 } \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 { #2 } } { \tl_gset:Ne #6 { \seq_use:Nn \l__commalists_merge_seq { #2 } } } \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 { , } d < > m m O { \mydifflist } } { % #1 = star (display only) % #2 = sep (défaut virgule) % #3 = options (asc, des), entre < > % #4 = list A % #5 = list B % #6 = output macro (non-starred, default \mydifflist) \group_begin: % parse keys \IfNoValueF { #3 } { \keys_set:nn { commalists / diff } { #3 } } % --- iterate A, keep elements absent from B \seq_set_split:Nne \l__commalists_tmpa_seq { #2 } { #4 } \seq_set_split:Nne \l__commalists_tmpb_seq { #2 } { #5 } \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 { #2 } } { \tl_gset:Ne #6 { \seq_use:Nn \l__commalists_diff_seq { #2 } } } \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 { , } d < > m m O { \mysymdifflist } } { % #1 = star (display only) % #2 = sep (défaut virgule) % #3 = options (asc, des), entre < > % #4 = list A % #5 = list B % #6 = output macro (non-starred, default \mysymdifflist) \group_begin: % parse keys \IfNoValueF { #3 } { \keys_set:nn { commalists / symdiff } { #3 } } \seq_set_split:Nne \l__commalists_tmpa_seq { #2 } { #4 } \seq_set_split:Nne \l__commalists_tmpb_seq { #2 } { #5 } % --- 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 { #2 } } { \tl_gset:Ne #6 { \seq_use:Nn \l__commalists_symdiff_seq { #2 } } } \group_end: } %------Shuffle list \NewDocumentCommand\ctshufflelist{ s O { , } m O { \myshuffledlist } } { % #1 = star (just printing) % #2 = sep (défaut virgule) % #3 = list (content) % #4 = output macro (non-starred, default \myshuffledlist) \group_begin: \seq_set_split:Nne \l__commalists_tmpa_seq { #2 } { #3 } \seq_shuffle:N \l__commalists_tmpa_seq % --- output \IfBooleanTF{ #1 } { \seq_use:Nn \l__commalists_tmpa_seq { #2 } } { \tl_gset:Ne #4 { \seq_use:Nn \l__commalists_tmpa_seq { #2 } } } \group_end: } \endinput