切点写死在最后一个点:archive.tar.gz 只分到 gz
手边攒了一堆文件名想按后缀归堆:archive.tar.gz 到底算 gz 还是 tar.gz?photo. 算不算有后缀?开头带点的 .gitignore 又该进哪一列?把这几行贴进去、点一次「拆分后缀」,结果和脑子里那个「扩展名」并不完全对得上。差的不是按钮,是切点被钉死在最后一个点,外加紧跟其后的一条分支把两种输入并到了一起。模板一共 6546 字节 / 158 行,拆后缀这件事全住在 splitSuffix 那十来行里。
切点走的是 filename.lastIndexOf('.'),从右往左数到的第一个点就是唯一切点。文件名后缀拆分于是把 archive.tar.gz 切成 name 为 archive.tar、ext 为 gz——多重后缀永远只切最后那一段。11 个样本里点数 ≥ 2 的有 7 格,切完之后 ext 里含点的 0 格、name 里仍带点的反而有 8 格:中间那些点全被留在了文件名那一侧,而不是跟着进扩展名列。
图 1:切点钉在最后一个点,多重点全被留在文件名侧。
dot <= 0 这一条,把「没有点」和「点打头」并成一个返回
紧跟着切点的 L66 那道 if (dot <= 0) 一次接住两类完全不同的输入:dot 为 -1(README 根本没有点)和 dot 为 0(.gitignore 的点就在第 0 位)。两者共用同一个 return { name: filename, ext: '' }——整串原样进 name,ext 给空串。于是 .gitignore 不被拆出扩展名、README 也一样,两格落进同一个「无后缀」计数。
真正容易被读漏的是尾点那一族:photo.、.hidden.、..、name.。它们走的是 dot > 0 那条分支,点确实在串里某处,只是 substring(dot + 1) 从点后一位切到串尾,切出来是个空串。于是 ext 照样为空,在 L96 的统计里它和 README、.gitignore 一起落进 withoutExt——11 格里这样的有 4 格。差别只落在 name:前两类整串进 name,尾点族的 name 被削掉了最后那个点。
逐格摊开:11 行真值表,看第一列对不回输入的有哪几格
下面把 11 个样本一格一格铺开。最后一列是「重建出来的第一列」,它等于 name 拼回 ext;拿它和修剪后的输入逐格比,7 格对得上、4 格对不上,对不上的正是 photo. / .hidden. / .. / name. 这四个尾点样本。
| 输入文件名 | 最后一个点 | name | ext | 第一列重建串 | 对得上? |
|---|---|---|---|---|---|
| archive.tar.gz | 11 | archive.tar | gz | archive.tar.gz | 是 |
| my.file.txt | 7 | my.file | txt | my.file.txt | 是 |
| .gitignore | 0 | .gitignore | — | .gitignore | 是 |
| .bashrc.conf | 7 | .bashrc | conf | .bashrc.conf | 是 |
| README | -1 | README | — | README | 是 |
| photo. | 5 | photo | — | photo | 否 |
| .hidden. | 7 | .hidden | — | .hidden | 否 |
| a..b | 2 | a. | b | a..b | 是 |
| .. | 1 | . | — | . | 否 |
| name. | 4 | name | — | name | 否 |
| 多.重.后.缀.zip | 7 | 多.重.后.缀 | zip | 多.重.后.缀.zip | 是 |
重建出来的第一列,会悄悄吃掉一个尾点
第一列不是原样回填输入,它是按 name + (ext ? '.' + ext : '') 现拼的重建串,而输入在进函数之前已经被剪过首尾空白。photo. 走完一圈,name 是 photo、ext 空,重建串就停在 photo,那个尾点没了。尾点这一族通吃:三个点 ... 重建剩 ..,archive.tar.gz 后面再补一个点,回来变回 archive.tar.gz——每次恰好吞掉 1 个尾点。第二列的 name 却仍保留中间那些点,所以两列对不齐的根子在重建那一步,不在切分本身。
本页所有样本都按输入原样大小写记着,后缀的大小写归后缀大小写转换那页管。
图 2:重建列每次吞掉一个尾点,11 格里 4 格对不回输入。
统计三行和提示语,是同一本账
右上角的统计走 L96 的 if (r.ext),判据是 ext 空不空,不是「有没有点」。11 个样本整批跑一遍,total 11、有后缀 5、无后缀 6,和表格的 11 行、6 个破折号格、5 个绿格、提示语那句「已拆分 11 个文件名」完全同分母,5 + 6 = 11 也咬得上。点「示例」灌进去的那 6 行则得 6 / 4 / 2,每行点数分别 1、2、0、1、1、1,archive.tar.gz 那一格一个人贡献 2 个点、ext 却只拿走 gz。
空行只进分母、不进任何计数:掺进 20000 行里 5000 个空行,实测统计停在 15000,被跳过的 5000 不出现在任何一列。同分母这三处对齐,可以逐条核:
- 表格行 11、破折号格 6、绿格 5,提示语的 N 也写 11,四处同分母。
- 示例灌的 6 行得 6 / 4 / 2,且 6 = 4 + 2。
- 20000 行掺 5000 空行,统计 15000,空行不进任何一列。
至于同工具更早那篇拆两列的文章,量的是另一套账,和这里切点的分支不是一回事,它的结论本篇不重复;要看那条链路可点开文件名拆两列的旧文。
Q:photo. 明明有个点,怎么被算成无后缀?
点在倒数第一位,substring(dot + 1) 从点后一位切到串尾是空串,ext 为空,统计就把它归进无后缀那一格;只是它的 name 被削掉了尾点,和 README 那种整串进 name 并不一样。
Q:.tar.gz 不能整体当扩展名吗?
在这一页不能。切点恒在最后一个点,11 格里 ext 含点 0 格,.tar.gz 的 ext 只剩 gz,前面那段留在 name 里——工具文案里「支持多扩展名」这句,和代码实际切出来的并不一致。
Q:第一列和输入对不上,是坏了吗?
它是按 name 拼回 ext 现造的重建串,不回填原串。尾点这一族因此在第一列少一个点,11 格里有 4 格对不回修剪后的输入,而第二列的 name 仍保留中间那些点。