Notes › Tech Notes › Empty Fields Survive the Text Column Split: a,,b Stays, an Index Out of Range Fills Blank and 0 or abc Returns Every Column

Empty Fields Survive the Text Column Split: a,,b Stays, an Index Out of Range Fills Blank and 0 or abc Returns Every Column

Running the text-column script verbatim in a sandbox measured the split and merge tabs: the Tab preset value is a 2-character literal, every empty field survives, an index out of range fills a blank line while 0 or abc falls back to all columns, and the two line filters keep separate books.

The Empty Field Survived; I Had It Wrong

Conclusion first: paste a,,b into the split box and the empty field between the two commas is not swallowed; 2 in Keep column indices gives a line of 0 bytes, while 0 hands all three columns back. Three branches, one chain.

The page under test is the Text Column Split and Merge tool: the template is 10134 bytes and its <script> source is 4123 characters over 94 lines. The page has three tabs; this article measures the first two, and the third is left to the earlier note on the Cartesian product. Readings come from a Node sandbox with a DOM stub, the real browser only read back static properties, and four items stay unmeasured: CRLF normalization, the value each box holds after a tab click, rows and scrolling, and the stutter above 1 MB of input.

The Tab Preset Value Is 2 Characters

The tab options on L26 and L41 both read value="\t": a backslash plus a lowercase t, 2 characters, read back by a real browser as U+005C U+0074 and unequal to String.fromCharCode(9). getSep recognizes only that 2-character string and turns it into U+0009; the other 10 arguments come back unchanged. Data holding the literal \t (a\tb\tc, 7 characters) is not split by the Tab preset at all, and the output is those same 7 bytes.

Empty Fields Are Always Kept

The else branch of doSplit only maps and joins, never filters, so two neighbouring separators always leave an empty string that survives untouched to the output (both separators on the comma preset): a,,b gives 3 segments and 4 bytes, and a line ending a,b, keeps 4 bytes with an empty trailing segment. On the - output preset a,,b produces the 8-byte string a - - b. The segment ledger is the number of separators plus one, and the opening sample has 2 commas per line against 3 segments. This empty comes only from neighbouring separators: select it alone (column index 2) and the line is 0 bytes, yet the stats bar still reports 1 output line.

Keep Column Indices: 1 Based, Given Order, No Deduplication

The chain trims the whole box, splits on commas, runs parseInt(s.trim(), 10) per piece, drops anything NaN or not positive, and reads values with parts[idx - 1], which makes it 1 based; only an empty selection falls back to all columns. The reference line is 张三,25,北京, its output separator set temporarily to the pipe:

Index box filled withMeasured outputHow to read it
1,3张三|北京5 characters
1,5张三|out of range fills blank
5a line of 0 bytesline kept, reports 1
2,,325|北京empty pieces dropped
1 - 3, 1<TAB>2张三cut down to 1
0, abc, a fullwidth 2张三|25|北京falls back to all columns

The out-of-range empty comes from aiming 5 at a line of 3 segments: an empty string is filled in, the line stays, and the stats report 1. The invalid empty comes from 0, abc and a fullwidth 2: the box is voided and every column returns, the same branch as a blank box.

Every corner comes from parseInt: 1.9 reads as 1, 2.99 as 2, 007 as 7. One magnitude higher and floating point takes over: the 21-digit 999999999999999999999 reads back as 1e+21, and idx - 1 no longer changes the value, so the fetch is still undefined and the line is 0 bytes while the stats report 1.

Two Line Filters: Length, and Length After trim

The split tab uses filter(l => l.length > 0): feed it a,b, a space-only line, c,d, a truly empty line and d,e, which is 5 segments, and it reports 4 input lines and 12 bytes over 4 lines, the space-only line now a 0-byte blank. The line count keeps it, the content never shows it, and only switching to columns 1,2 makes it appear, as a single-byte ,. A CRLF blank line is 1 \r with length 1, so it passes (3 against 2). Merge carries trim, so blank lines never enter the count: 1 space-only line and 1 truly empty line on side B still report list B as 3 and leave the output at 27 bytes; drop 1 line on side A and the pairing shifts, leaving the last line - z.

Whitespace-only lines: what each tab counts Split, length > 0: the space line lives, 5 segments as 4 lines A CRLF blank line is one CR 3 lines with it, LF-only 2 Merge after trim: not counted

Figure 1: one invisible line, each tab keeping its own books.

doMerge Takes the Longer Side and Still Hangs the Joiner

Give A 5 lines, B 3 lines, joiner left on its default - : the result is 5 lines and 27 bytes, the first 3 lines being a - x at 5 bytes, the last two d - at 4. The missing side takes an empty string, no line disappears, and the joiner hangs at the tail; a longer B hangs it at the head, as in - d. Only an empty joiner gives a clean d, and then 5 lines of 12 bytes; both sides empty gives 0 lines and the stats report 0/0/0. Pairing 50 lines with 37 lines outputs 442 bytes, 150 above the 292 of the empty-joiner preset, exactly 50 lines times a 3-byte joiner, and the 13 lines with a missing side are assembled the same way.

Idempotence Breaks Only at the Blank Line

With both separators on comma, clean data run three times is byte for byte identical and settles at 33 bytes; 张三, 25 ,北京, which carries spaces inside a field, drops from 18 bytes to 16 on the first pass and stays at 16. The break is the space-only line: sample P (53 segments and 580 bytes, holding 1 space-only line) run four times gives 580, 578, 577, 577. That line is written as an empty line on the first pass and filtered away only on the second, so the count is one lower forever after, 52 lines becoming 51.

The space preset is reversible for empty fields (a,,b becomes two adjacent spaces, the 8 bytes do not move) but not for spaces inside a field: New York,25 splits back into 2 segments becoming 3, with the 26 bytes unchanged. The - preset is a one-way door: 张三,25 outputs 7 characters and 11 bytes, and the real browser reads back the 5 input separator presets with no such value among them, so the same page cannot cut it back.

Batch Sample M and the Ragged Sample

M is 50 mixed lines plus 1 space-only line plus 1 truly empty line plus one line of row33,a,,b: 53 segments and 775 bytes. All five presets output 52 lines and the space-only line is still in the output. The comma preset gives 703 bytes and 663 characters; the - preset gives 909 bytes with a widest line of 26; keeping only 3 gives 136 bytes in which 22 lines are 0-byte blanks. A fullwidth comma is not a preset, so r3,v3,w3,x3 takes a single cut.

The width ledger is not M's: the ragged sample, three lines whose column counts differ, is 29 characters and 49 bytes wide on all four single-character output presets, and 41 characters and 61 bytes on the - preset, the gap of 12 bytes being 6 separators multiplied by 2. The uneven part is the column count, not the content length.

No Quote Handling and No Failure Path

A quoted field, a,"b,c",d, is 9 characters, and the comma preset cuts it into 4 segments, splitting the quoted part in half. That is why the shipped line about extracting chosen columns from CSV data is marked a conflict in the copy comparison. The page has no error path: maxlength, alert(, try { and onerror are each hit 0 times, and invalid input only gets a silent fallback, since an input that trims to empty clears the output box and the stats bar.

Do Not Mix Up the Three Byte Ledgers

The three sample lines the page opens with are 26 characters, and the default double-comma preset outputs 50 bytes equal to the input byte for byte; the 49 and 61 bytes belong to the two presets of the ragged sample; the 703 and 909 bytes belong to the 52 lines of batch sample M. Capacity has nothing to do with display: rows=6 is only how many lines are visible, and the 5000-line sample of 91669 bytes takes 3.52 to 3.93 ms per pass while one line of 5000 segments finishes under 1 ms, both pure sandbox computation without DOM writes or paint. Prefixing or numbering lines is another tool's job, covered in the earlier note on prefixes and line numbers.

Q: The empty field inside a,,b is still there, so did the split not finish?

It finished: 2 commas give 3 segments, the middle one was already empty, nothing filters empty segments, and joining back still gives 4 bytes. Only letting the indices skip the second column makes it disappear.

Q: Why do all three columns come back when the index box holds 0?

0 is dropped by the rule that keeps nothing which is not a positive number, abc likewise, so the box holds nothing and it walks the all-columns branch like a blank one.

Q: Pasted into a spreadsheet every line has one extra empty column, which step caused that?

Two different inputs: a separator typed once too many at the line end, where a,b, carries an empty trailing segment, and the missing side of a merge, where d - still hangs the joiner at the tail.

📎 Related articles

120 word slots, 15 whole sentences and 1 hard-coded opening: auditing the lorem-ipsum word list and measuring repeated fragments 2026-11-05 SVG preview sizes: the box equals the container, and r=1 measures 2 pixels once viewBox is removed 2026-11-05 13 Digits In, 11 Out: the Regex Boundaries of the Seven Built-in Types in text-extract 2026-11-05 How a 1 GB File Is Built From 1024 Chunks: Chunk Rhythm, Cancel Boundaries and Three Fill Bytes in a File Generator 2026-11-04 Three Edges of Filename Extraction: The Trailing Slash Backstops One Step, the Query String Stays Whole, Blank Lines Vanish Silently 2026-11-04