% numtriangle.sty -- reusable number triangles (Pascal's triangle and friends) % % Commands % \NumberTriangle[keys] triangle from a recurrence or a closed formula % \PascalTriangle[keys] shortcut for Pascal's triangle % \ListTriangle[keys]{data} triangle from explicit entries: rows separated % by ';', entries by ',' (entries are typeset % in math mode, so \frac{1}{2} etc. work) % \NewTriangle{\Name}{keys} define your own reusable triangle command % \numtriset{keys} change defaults for the rest of the group % % Inside rule/left/right/formula/highlight expressions (l3fp syntax): % \n row index \k position in the row % \L entry above-left, T(n-1,k-1) \R entry above-right, T(n-1,k) % \P previous entry in the same row, T(n,k-1) (also usable in `right') % \v the entry's own value (highlight only) % % Key `fractions' prints non-integer values as exact fractions \frac{p}{q} % (recovered by continued fractions), e.g. for the Leibniz harmonic triangle. % % Each entry becomes a TikZ node named -- (default T-4-2), % so you can draw on top of a triangle with the `after' key. \NeedsTeXFormat{LaTeX2e} \ProvidesExplPackage{numtriangle}{2026/10/14}{1.0} {Pascal-style number triangles} \RequirePackage{tikz} \tikzset{ numtri/cell/.style = {inner~sep=1pt, minimum~size=1.5em}, numtri/highlight/.style = {fill=blue!18, rounded~corners=2pt}, } % ---------------------------------------------------------------- variables \int_new:N \l__numtri_rows_int \int_new:N \l__numtri_start_int \int_new:N \l__numtri_mod_int \dim_new:N \l__numtri_xsep_dim \dim_new:N \l__numtri_ysep_dim \bool_new:N \l__numtri_centered_bool \bool_new:N \l__numtri_fractions_bool \fp_new:N \l__numtri_x_fp \fp_new:N \l__numtri_target_fp \fp_new:N \l__numtri_a_fp \fp_new:N \l__numtri_h_fp \fp_new:N \l__numtri_hh_fp \fp_new:N \l__numtri_k_fp \fp_new:N \l__numtri_kk_fp \fp_new:N \l__numtri_tmp_fp \int_new:N \l__numtri_cf_int \tl_new:N \l__numtri_shown_tl \tl_new:N \l__numtri_left_tl \tl_new:N \l__numtri_right_tl \tl_new:N \l__numtri_rule_tl \tl_new:N \l__numtri_formula_tl \tl_new:N \l__numtri_highlight_tl \tl_new:N \l__numtri_cell_tl \tl_new:N \l__numtri_hlstyle_tl \tl_new:N \l__numtri_format_tl \tl_new:N \l__numtri_name_tl \tl_new:N \l__numtri_after_tl \tl_new:N \l__numtri_picture_tl \tl_new:N \l__numtri_nodes_tl \tl_new:N \l__numtri_value_tl \seq_new:N \l__numtri_prev_seq \seq_new:N \l__numtri_curr_seq \seq_new:N \l__numtri_rows_seq \seq_new:N \l__numtri_row_seq \int_new:N \l__numtri_i_int \int_new:N \l__numtri_j_int % --------------------------------------------------------------------- keys \keys_define:nn { numtri } { rows .int_set:N = \l__numtri_rows_int, rows .initial:n = 7, start .int_set:N = \l__numtri_start_int, start .initial:n = 0, left .tl_set:N = \l__numtri_left_tl, left .initial:n = 1, right .tl_set:N = \l__numtri_right_tl, right .initial:n = 1, edges .meta:n = { left = #1, right = #1 }, rule .tl_set:N = \l__numtri_rule_tl, rule .initial:n = \L + \R, formula .tl_set:N = \l__numtri_formula_tl, xsep .dim_set:N = \l__numtri_xsep_dim, xsep .initial:n = 2.2em, ysep .dim_set:N = \l__numtri_ysep_dim, ysep .initial:n = 1.7em, shape .choice:, shape / centered .code:n = \bool_set_true:N \l__numtri_centered_bool, shape / left .code:n = \bool_set_false:N \l__numtri_centered_bool, shape .initial:n = centered, highlight .tl_set:N = \l__numtri_highlight_tl, mod .int_set:N = \l__numtri_mod_int, mod .initial:n = 0, cell~style .tl_set:N = \l__numtri_cell_tl, highlight~style .tl_set:N = \l__numtri_hlstyle_tl, format .tl_set:N = \l__numtri_format_tl, fractions .bool_set:N = \l__numtri_fractions_bool, fractions .default:n = true, name .tl_set:N = \l__numtri_name_tl, name .initial:n = T, picture .tl_set:N = \l__numtri_picture_tl, after .tl_set:N = \l__numtri_after_tl, } \NewDocumentCommand \numtriset { m } { \keys_set:nn { numtri } {#1} } % --------------------------------------------------- one node (row i, pos j) % #1 = value as typeset, needs \n \k \v already defined \cs_new_protected:Npn \__numtri_add_node:n #1 { \bool_set_false:N \l_tmpa_bool \int_compare:nNnT { \l__numtri_mod_int } > { 0 } { \fp_compare:nNnT { \v - \int_use:N \l__numtri_mod_int * floor( \v / \int_use:N \l__numtri_mod_int ) } > { 0 } { \bool_set_true:N \l_tmpa_bool } } \tl_if_empty:NF \l__numtri_highlight_tl { \fp_compare:nT { \l__numtri_highlight_tl } { \bool_set_true:N \l_tmpa_bool } } \tl_put_right:Nx \l__numtri_nodes_tl { \exp_not:N \node [ numtri/cell, \exp_not:V \l__numtri_cell_tl \bool_if:NT \l_tmpa_bool { , numtri/highlight, \exp_not:V \l__numtri_hlstyle_tl } ] ( \l__numtri_name_tl - \n - \k ) at ( \bool_if:NTF \l__numtri_centered_bool { \dim_eval:n { \l__numtri_xsep_dim * \l__numtri_j_int - \l__numtri_xsep_dim * \l__numtri_i_int / 2 } } { \dim_eval:n { \l__numtri_xsep_dim * \l__numtri_j_int } } , \dim_eval:n { - \l__numtri_ysep_dim * \l__numtri_i_int } ) { $ \exp_not:V \l__numtri_format_tl { \exp_not:n {#1} } $ } ; } } % ---------------------------------------- decimal -> \frac{p}{q} (cont. fractions) \cs_new_protected:Npn \__numtri_to_fraction:n #1 { \fp_set:Nn \l__numtri_target_fp { abs(#1) } \fp_set_eq:NN \l__numtri_x_fp \l__numtri_target_fp \fp_set:Nn \l__numtri_h_fp { 1 } \fp_set:Nn \l__numtri_hh_fp { 0 } \fp_set:Nn \l__numtri_k_fp { 0 } \fp_set:Nn \l__numtri_kk_fp { 1 } \int_zero:N \l__numtri_cf_int \__numtri_cf_step: \tl_set:Nx \l__numtri_shown_tl { \fp_compare:nNnT { #1 } < { 0 } { - } \fp_compare:nNnTF { \l__numtri_k_fp } = { 1 } { \fp_use:N \l__numtri_h_fp } { \exp_not:N \frac { \fp_use:N \l__numtri_h_fp } { \fp_use:N \l__numtri_k_fp } } } } \cs_new_protected:Npn \__numtri_cf_step: { \int_incr:N \l__numtri_cf_int \fp_set:Nn \l__numtri_a_fp { floor(\l__numtri_x_fp) } \fp_set:Nn \l__numtri_tmp_fp { \l__numtri_a_fp * \l__numtri_h_fp + \l__numtri_hh_fp } \fp_set_eq:NN \l__numtri_hh_fp \l__numtri_h_fp \fp_set_eq:NN \l__numtri_h_fp \l__numtri_tmp_fp \fp_set:Nn \l__numtri_tmp_fp { \l__numtri_a_fp * \l__numtri_k_fp + \l__numtri_kk_fp } \fp_set_eq:NN \l__numtri_kk_fp \l__numtri_k_fp \fp_set_eq:NN \l__numtri_k_fp \l__numtri_tmp_fp \bool_lazy_any:nF { { \int_compare_p:nNn { \l__numtri_cf_int } > { 40 } } { \fp_compare_p:nNn { \l__numtri_x_fp - \l__numtri_a_fp } = { 0 } } { \fp_compare_p:nNn { abs( \l__numtri_h_fp / \l__numtri_k_fp - \l__numtri_target_fp ) } < { 1e-10 * \l__numtri_target_fp } } } { \fp_set:Nn \l__numtri_x_fp { 1 / ( \l__numtri_x_fp - \l__numtri_a_fp ) } \__numtri_cf_step: } } \cs_new_protected:Npn \__numtri_set_nk: { \cs_set:Npx \n { \int_eval:n { \l__numtri_start_int + \l__numtri_i_int } } \cs_set:Npx \k { \int_eval:n { \l__numtri_start_int + \l__numtri_j_int } } } \cs_new_protected:Npn \__numtri_draw: { \begin{tikzpicture} [ baseline = (current~bounding~box.center) ] \exp_args:NV \tikzset { \l__numtri_picture_tl } \l__numtri_nodes_tl \l__numtri_after_tl \end{tikzpicture} } % ------------------------------------------------- computed (recurrence/formula) \cs_new_protected:Npn \__numtri_computed: { \tl_clear:N \l__numtri_nodes_tl \seq_clear:N \l__numtri_prev_seq \int_step_variable:nnNn { 0 } { \l__numtri_rows_int - 1 } \l__numtri_tmp_i { \int_set:Nn \l__numtri_i_int { \l__numtri_tmp_i } \seq_clear:N \l__numtri_curr_seq \int_step_variable:nnNn { 0 } { \l__numtri_i_int } \l__numtri_tmp_j { \int_set:Nn \l__numtri_j_int { \l__numtri_tmp_j } \__numtri_set_nk: \int_compare:nNnT { \l__numtri_j_int } > { 0 } { \cs_set:Npx \P { \seq_item:Nn \l__numtri_curr_seq { \l__numtri_j_int } } } \tl_if_empty:NTF \l__numtri_formula_tl { \int_compare:nNnTF { \l__numtri_j_int } = { 0 } { \tl_set:Nx \l__numtri_value_tl { \fp_eval:n { \l__numtri_left_tl } } } { \int_compare:nNnTF { \l__numtri_j_int } = { \l__numtri_i_int } { \tl_set:Nx \l__numtri_value_tl { \fp_eval:n { \l__numtri_right_tl } } } { \cs_set:Npx \L { \seq_item:Nn \l__numtri_prev_seq { \l__numtri_j_int } } \cs_set:Npx \R { \seq_item:Nn \l__numtri_prev_seq { \l__numtri_j_int + 1 } } \tl_set:Nx \l__numtri_value_tl { \fp_eval:n { \l__numtri_rule_tl } } } } } { \tl_set:Nx \l__numtri_value_tl { \fp_eval:n { \l__numtri_formula_tl } } } \seq_put_right:NV \l__numtri_curr_seq \l__numtri_value_tl \cs_set:Npx \v { \l__numtri_value_tl } \bool_if:NTF \l__numtri_fractions_bool { \exp_args:NV \__numtri_to_fraction:n \l__numtri_value_tl \exp_args:NV \__numtri_add_node:n \l__numtri_shown_tl } { \exp_args:NV \__numtri_add_node:n \l__numtri_value_tl } } \seq_set_eq:NN \l__numtri_prev_seq \l__numtri_curr_seq } \__numtri_draw: } \NewDocumentCommand \NumberTriangle { O{} } { \group_begin: \keys_set:nn { numtri } {#1} \__numtri_computed: \group_end: } \NewDocumentCommand \PascalTriangle { O{} } { \NumberTriangle [ edges = 1, rule = \L + \R, formula = , #1 ] } \NewDocumentCommand \NewTriangle { m m } { \NewDocumentCommand #1 { O{} } { \NumberTriangle [ #2, ##1 ] } } % ------------------------------------------------------- explicit entries \NewDocumentCommand \ListTriangle { O{} m } { \group_begin: \keys_set:nn { numtri } {#1} \tl_clear:N \l__numtri_nodes_tl \seq_set_split:Nnn \l__numtri_rows_seq { ; } {#2} \seq_remove_all:Nn \l__numtri_rows_seq { } \int_zero:N \l__numtri_i_int \seq_map_inline:Nn \l__numtri_rows_seq { \seq_set_split:Nnn \l__numtri_row_seq { , } {##1} \int_zero:N \l__numtri_j_int \seq_map_inline:Nn \l__numtri_row_seq { \__numtri_set_nk: \cs_set:Npn \v {####1} \__numtri_add_node:n {####1} \int_incr:N \l__numtri_j_int } \int_incr:N \l__numtri_i_int } \__numtri_draw: \group_end: } \endinput