Notes › Tech Notes › The cut lands on the last dot only: archive.tar.gz yields gz, photo. and .gitignore share one cell, and the rebuilt column drops a dot

The cut lands on the last dot only: archive.tar.gz yields gz, photo. and .gitignore share one cell, and the rebuilt column drops a dot

The splitter cuts at the last dot only, so archive.tar.gz yields gz. Names with no dot and names starting with one share an empty-extension return; trailing-dot names like photo. land in the same count. The rebuilt column swallows a trailing dot, leaving 4 of 11 rows unmatched.

The cut point is hard-wired to the last dot: archive.tar.gz only yields gz

I had a pile of file names to sort by suffix. Is archive.tar.gz a gz file or a tar.gz file? Does photo. have a suffix at all? Which column does .gitignore, starting with a dot, belong to? Paste those rows in and press the split button once: the result does not match the word "extension" you had in mind. The gap is not in the button: the cut point is nailed to the last dot, and a branch right behind it merges two very different inputs into one answer. The whole template is 6546 bytes / 158 lines, and the entire suffix story lives in those few lines of splitSuffix.

The cut point comes from filename.lastIndexOf('.'), so scanning right to left, the first dot you touch is the only cut point there is. Filename Suffix Split therefore slices archive.tar.gz into a name of archive.tar and an ext of gz - a multi-part suffix always loses only its final segment, never the whole stack. Among the 11 samples the rows holding 2 or more dots fill 7 cells; once the split is done, cells whose ext still contains a dot number 0, while cells whose name still contains a dot run to 8. That pair of counts is the story of this page: every interior dot stays on the file-name side instead of following the suffix into the extension column.

archive.tar.gz: only the last segment splits off Sample Cut name ext archive.tar.gz archive.tar gz my.file.txt my.file txt a.b.c.d.zip a.b.c.d zip rows with dots ≥ 2: 7 · dots inside ext: 0 · dots still in name: 8

Figure 1: the cut point sits on the last dot, so interior dots stay on the file-name side.

The dot <= 0 test merges "no dot" and "leading dot" into one return

Immediately after the cut point, L66 carries if (dot <= 0), and that one line catches two different inputs: dot of -1, where README has no dot at all, and dot of 0, where the dot in .gitignore sits in the first position. Both share the same return { name: filename, ext: '' } - the whole string goes into name untouched, ext gets an empty string. So .gitignore gets no extension, README gets none either, and the two cells drop into the same "no suffix" counter.

The family that is easy to read past is the trailing-dot one: photo., .hidden., .., name. Those take the dot > 0 branch - the dot really is somewhere inside the string - but substring(dot + 1) runs from one past that dot to the end, and what falls out is an empty string again. So ext stays empty regardless, and in the tally at L96 these rows join README and .gitignore inside withoutExt; 4 of the 11 rows behave that way. The difference only surfaces in name: the first two kinds keep the entire string there, while the trailing-dot family arrives with its final dot already shaved off.

Cell by cell: an 11-row truth table, and which rows the first column cannot rebuild

Below the 11 samples are spread out one cell at a time. The last column is the rebuilt first column: take name and stitch ext back onto it. Compare that against the trimmed input row by row and 7 rows line up while 4 do not, and the 4 misses are exactly the trailing-dot samples photo. / .hidden. / .. / name. The third column is the split result; the fifth is the string the page shows you afterwards.

Input file nameLast dotnameextRebuilt first columnMatches?
archive.tar.gz11archive.targzarchive.tar.gzyes
my.file.txt7my.filetxtmy.file.txtyes
.gitignore0.gitignore-.gitignoreyes
.bashrc.conf7.bashrcconf.bashrc.confyes
README-1README-READMEyes
photo.5photo-photono
.hidden.7.hidden-.hiddenno
a..b2a.ba..byes
..1.-.no
name.4name-nameno
a.b.c.d.zip7a.b.c.dzipa.b.c.d.zipyes

The rebuilt first column quietly eats one trailing dot

The first column is not the input echoed back. It is a rebuilt string assembled on the spot by name + (ext ? '.' + ext : ''), and the input had already been trimmed of leading and trailing whitespace before it reached the function. Send photo. around that loop: name comes out as photo, ext is empty, so the rebuilt string stops at photo and the trailing dot is gone. The whole trailing-dot family behaves alike - three dots ... rebuild to just two, and one extra dot pasted after archive.tar.gz comes back as plain archive.tar.gz - each lap swallowing exactly 1 trailing dot. Meanwhile the name column still keeps every interior dot, so the reason the two columns disagree sits in the rebuild step, not in the split itself.

Every sample here keeps the letter case it arrived in; the case of the suffix itself is handled on the Suffix Case Converter page.

First column = name + (one dot, only if ext exists) + ext Input Rebuilt first column Trailing dot photo. photo dot gone ... .. 1 dot fewer archive.tar.gz. archive.tar.gz 1 dot fewer Of 11 rows: 7 match · 4 lose a dot

Figure 2: every rebuild lap swallows one trailing dot, so 4 of the 11 rows cannot be reproduced.

The three stat lines and the toast keep one ledger

The statistics in the top-right corner run through the test at L96, if (r.ext), which asks whether ext is an empty string - not whether the name contains a dot anywhere. Run the 11 samples as a single batch and you get total 11, with 5 rows carrying a suffix and 6 rows without one. That shares its denominator with the 11 table rows, the 6 dash cells, the 5 green cells and the toast line "Split 11 file names", and 5 + 6 = 11 locks together as well. The 6 rows poured in by the example button give 6 / 4 / 2 instead, with dot counts of 1, 2, 0, 1, 1, 1; the archive.tar.gz row alone contributes 2 dots while ext only ever walks away with gz.

Blank rows enter the denominator but no count at all: mix 5000 empty lines into 20000 lines and the tally settles at 15000, because the 5000 skipped lines never appear in any column. Those three denominators agree, and each is worth checking on its own:

  • Table rows 11, dash cells 6, green cells 5, and the N in the toast also reads 11 - four places, one denominator.
  • The 6 example rows give 6 / 4 / 2, and 6 = 4 + 2 holds there too.
  • 20000 lines mixed with 5000 blank ones report 15000; blank lines enter no column.

The earlier article from this same tool, about splitting a file name into two columns, keeps a different ledger and has nothing to do with the branch behind the cut point here, so its conclusions are not repeated. For that chain, open the older article on two-column file name splits.

Q: photo. clearly carries a dot, so how is it counted as having no suffix?

The dot is the last character, so substring(dot + 1) cuts from one past it to the end of the string and yields an empty string. ext stays empty and the tally files that row under no suffix. The only trace left is that its name has lost the trailing dot, unlike README, where the whole string stays inside name.

Q: Could .tar.gz be kept whole as one extension?

Not on this page. The cut point always lands on the last dot, and across the 11 rows not one ends with a dot inside ext, so for .tar.gz the extension keeps only gz while the earlier part stays behind in name. The tool copy about supporting multi-part extensions does not describe what the code actually cuts.

Q: The first column disagrees with the input - is that broken?

It is a rebuilt string made by stitching name back to ext, not the original text written back. That is why the trailing-dot family shows one dot fewer in the first column: 4 of the 11 rows cannot be reproduced from the trimmed input, while the name column keeps every interior dot.

📎 Related articles

3.7 becomes 3 and 1e3 becomes 1: this fraction calculator truncates at the input, then switches display rules at 14 characters 2026-11-14 The Regex Test Page Always Forces g: gg Errors Out, Uppercase Fails Silently, and 21 Grouped Matches List Only 20 Rows 2026-11-14 Star Run Length Is Hardcoded: the Masking Page Cuts a Fixed 4, 3, 11 or 12 Stars, an 18-Unit String Is Taken by the Phone Tier, an Empty Box Falls Back to * 2026-11-13 The Lock Is One-Way and Percent Is Two Rulers: pct 50 Gives 619x430, and All Four PNG Quality Stops Return 6971 B 2026-11-13 A Month on This Page Is Hard-Coded as 30 Days: the Six Fixed Divisors, 13-Digit Millisecond Input, and slice(0,16) Truncation Measured Under UTC+8 and UTC-5 2026-11-13