Notes › Tech Notes › Eight elements compute 40320 permutations but list only 5000 rows: two unrelated caps on the permutation page, duplicates counted by 3!, and the process break at n above 20

Eight elements compute 40320 permutations but list only 5000 rows: two unrelated caps on the permutation page, duplicates counted by 3!, and the process break at n above 20

The page computes 40320 permutations yet writes only 5000 rows: two caps that each check something different, duplicate elements counted by n!, expansion and reduction shown only while n stays at 20 or below, and a dead formatter with zero call sites, all measured at the string layer.

40320 computed, 5000 rows on the same screen

Type 8 single letters into the element box and the field labelled "permutation total" reads 40,320, while the list, scrolled to the bottom, holds only 5000 rows and closes with the line "40320 permutations in total, showing only the first 5000". Copying permutation.html (measured at 233 lines / 9158 bytes) word for word into a plain Node probe showed why: the page carries two unrelated rulers, one for the arithmetic and one for how many lines reach the screen, while the machine between them, which generates the permutations, has no notion that two elements can look alike.

Two gates, each reading its own quantity

The if (n > 8) at L177 tests the number of elements left after the split: four inputs of nine or more items all produced the same toast, "element count exceeds 8, too many permutations, please reduce the elements", and that return sits before heap, so the list length comes back as 0.

The maxShow = Math.min(perms.length, 5000) at L215 tests displayed rows instead, and perms stays full in memory: n=7 really generates 5040, writes 5000 and hides 40; n=8 really generates 40320, writes the same 5000 and hides 35320. That cap of 5000 falls between 720 and 5040, so of the nine stops only two ever reach the tail line. The page under test here is the permutation calculator.

element count, generated, rows written n = 6 720 720 no tail line n = 7 5040 5000 hidden 40 light 5040 made, dark 5000 shown n = 8 40320 5000 hidden 35320, 12.4008% n = 5: 120 to 120 rows

Figure 1: for the same n stops, how many permutations are generated and how many rows are shown.

heap only ever swaps indices

heap swaps two indices throughout according to whether k is odd or even and never compares element values, so duplicate elements are still counted by n!: A, A, B yields 6 rows and 3 once deduplicated; A, A, A yields 6 identical rows and 1; 甲 / 甲 / 乙 likewise yields 6 rows and 3. Both places in the interface, the total field and the "n! =" statistics line, print n! and never the deduplicated count. Across a batch of 40 inputs, 11 of them had a deduplicated count smaller than the number of rows written out. All of this is behaviour of this page alone, not evidence about permutation tools as a class.

generated, deduplicated, shown in first 5000 A, A, B 6 rows 3 left all visible A, A, B, C, D, E, F, G 40320 rows 20160 left 2520 in first 5000 A, A, A, A, B, B, B, B 40320 rows 70 kinds 35 in first 5000

Figure 2: the number the interface prints, next to the two ledgers actually kept.

Expansion and reduction appear only while n stays at 20 or below

The if (n <= 20) at L136 decides how many lines the process panel shows: n=5,r=3 gives 4 lines; n=20,r=7 still gives 4, the reduction line carrying 7 factors and the result 390,700,800; from n=21,r=1 only 3 lines remain, since the expansion and reduction blocks collapse into one line labelled "result", and n=22,r=22 behaves the same. The reduction line always carries as many factors as r, and when r=0 that cell reads "reduce: = 1".

The generate side has no such branch: calc sets no ceiling on n, so n=100000 with r=2 still answers 9,999,900,000, only it takes 9553.75 ms. The factorial itself lives on the factorial calculation page.

The separator regex edits n before you do

The character class /[,、\s,\n]+/ at L170 treats both comma styles, the enumeration comma, newlines and the whole whitespace class as separators. So New York, 北京 parses into 3 elements and the interface reports 3!=6, though the person typing had 2 items in mind; the other direction is just as quiet, since A;B splits into one element: the fullwidth semicolon is not in that class, and neither are the halfwidth semicolon, the vertical bar or the hyphen.

Two silent paths stay apart: input that is empty after trimming leaves through L164, while input made only of separators gives n as 0 and leaves through L173. Both hide the panel, raise no toast, and leave the total field unwritten. L173 is not dead code, since a lone comma reaches it.

fmt is a dead function with zero call sites

A word-boundary regex hits fmt only at its definition on L102, with 0 call sites, while fmtBig is hit 6 times, so every displayed value travels the BigInt route. The Number ruler lies unused, yet two boundaries hide behind it: the notation boundary sits at 22!, written as 1.1240007277776077e+21 while still exact, and the precision boundary sits at 23!, where the BigInt reading is 25852016738884976640000 and reading it back as a double gives 25852016738884978212864, two different numbers.

onclick is assembled out of strings

L218 packs the joined element string into a single-quoted argument and escapes the single quote alone. Two string-layer measurements: with a single quote inside an element the attribute string comes out as copyItem(this, 'A\', B'), and parsing that JS returns A', B, identical to the visible text of that row once the row number prefix is stripped; with a double quote the onclick value closes at the first double quote, so the attribute regex only recovers the fragment copyItem(this, 'A; with a less-than sign that character enters innerHTML unchanged, and what a browser makes of it was not measured.

Two sets of claims also stay apart. The structure after innerHTML injection, how the toast is presented and its 2500 ms, the copy feedback and when the button text returns at 1500 ms, what type=number writes into value for 5.7 and 1e3, and how long a 2019032 byte list string takes to render: no browser was opened for this batch, so all of these are marked not measured. On the string side the claim does hold: at n=8 the list contains 5000 onclick fragments, and the 40320th permutation has no copy entry, because its node was never created.

Switching back always reruns

L82 reruns whichever side you switch back to. Returning to calc leaves 132 in the result field, recomputed from n=12 and r=2, not the 60 that was there before switching away. When r exceeds n the function returns before writing textContent, so the node keeps the old string while display is already none; the same residue appears on the generate side, where after 9 items replace the input the 2019012 characters written earlier are not cleared by one character.

15 copy checks

From line 885 of config/tools.php, the entry and its two template hints were judged one by one: 7 agree, 2 conflict, 6 agree in part.

CopyMeasuredVerdict
Every permutation can be copiedat n=8 the list holds only 5000 clickable entries, and the other 35320 have no entry at allconflict
Take r elements out of n distinct onesdistinctness is never checked, so A, A, B is counted as 3!=6conflict
Supports at most 8 elements (8! = 40320 permutations)8 and 40320 both hold, while 5000 is written into no piece of copypartly agree

5000 is hit 3 times in the template, at L215, L220 and L221, all inside code, and 5040 is hit 0 times. The only place the cap surfaces is the tail line, which carries no thousands separator and so uses a different notation from the 40,320 on the same screen. The earlier article permutations and combinations already treats the distinction, so it is not retold here.

Q: so is 40320 simply wrong?

It is not wrong. 40,320 is both factorialBig(8) and the perms.length that 8 elements really generate; the misunderstanding comes from reading the total field as a row count. The screen parks at 5000 rows while the remaining 35320 entries sit in memory with no exit.

Q: with duplicate elements, how do I keep just one row?

This page offers no deduplication switch, and both places print n!. The string-layer reconciliation is n! divided by the product of the factorials of each value's occurrence count: A,A,B,C,D,E,F,G folds down from 40320 to 20160, and that stop reveals only 2520 kinds inside the first 5000 rows.

Q: the result changed after I switched tabs, was nothing saved?

That is a rerun rather than a loss: switching back always computes again from the current input, while invalid input only hides the panel instead of overwriting the string already there, which is why the state ends up hidden yet still present.

📎 Related articles

Assembly Order and Case Folding for Ad Links: Six UTM States, Two Hard Caps and Five Panel Fields on Their Own Scales 2026-11-12 The 13-Cell Algorithm Table and the 7 Verify Gates: the JWT Page Verifies Through Fewer Branches Than It Decodes, Signed and Checked for Real 2026-11-12 Drag to 4 and drag to 8, both preview 43890 blocks: two block grids for preview and export, three stops that crash and the 6px misclick threshold 2026-11-10 Putting a Bitmap Into SVG Only Swaps the Box: the 232-Byte Container Constant, a base64 Embed Checkbox That Never Fires, and the 142064 Characters That Do Not Move Across Four Zoom Levels 2026-11-10 271 Characters Quietly Missing After the Conversion: The Hole Protocol, the 64-Round Refill Cap and the 20-Entry Warning Limit of the Markdown to HTML Page 2026-11-10