Notes › Resources › Shenglü Qimen and Liweng Duiyun Fed Whole Into the Text Diff Page: 154 Common Lines, 665 Added, 401 Removed, and the Stats Line Gives Only Two Numbers

Shenglü Qimen and Liweng Duiyun Fed Whole Into the Text Diff Page: 154 Common Lines, 665 Added, 401 Removed, and the Stats Line Gives Only Two Numbers

Two full-text TXT files pasted whole into the text diff page: measured 665 added and 401 removed over 154 common lines; the guard fires only when both sides are empty, the tie-break fixes deletes before adds, a moved line costs 1 delete plus 1 add, and the stats line carries just two numbers.

Counting the lines of two TXT files first

The starting point is plain: two TXT files, one with 555 segments and one with 819. What I want is not their content but the books the line diff page keeps when a whole corpus is poured into it. How common lines are counted, which side of the added-and-removed pair comes first, and how many numbers the stats line actually writes. The two files serve only as bytes and lines.

Bytes and segments of the corpus

Both are published site resources: Shenglü Qimen 《声律启蒙》 at 26353 bytes, 9185 code units, 9183 code points, 555 segments, 403 non-empty lines and 152 blank ones; and Liweng Duiyun 《笠翁对韵》 at 28903 bytes, 10211 code units, 10209 code points, 819 segments, 671 non-empty lines and 148 blank ones. Neither carries CRLF and neither carries a BOM. Code units run 2 above code points in each file because each holds 2 supplementary-plane code points. The page under test is the text diff, whose template is 118 lines and 4487 bytes with 3 script functions.

The three entries of a whole-file run

Feeding both full texts straight into lineDiff gives added 665, removed 401, equal 154, a result of 1220 lines, and a dp table of 556 × 820 = 455920 cells. The price of that scale was computed in an older article, so this one only registers the tier. The identities: 555 minus 154 is 401, 819 minus 154 is 665, and 555 plus 819 minus 154 is 1220. Swapping the two sides trades the added and removed counts, while equal and the cell count do not move.

input tierA segmentsB segmentsremovedaddedequalresult lines
whole files as they come5558194016651541220
blank lines stripped first40367139766561068
each file against itself555 and 819same as left00555 and 819same as left

Both blank tiers report the same added of 665, and the whole gap sits in removed: of the 152 blank lines on side A, 148 are absorbed and 4 reach the removals, so removed is 401. The dp table of the non-empty run is 404 × 672 = 271488 cells. Timing is a script-level fact: 5 back-to-back calls returned 113.9, 139.5, 120.5, 122.7 and 117.6 ms with a median of 120.5 ms, and that figure is one function call, not a page response.

The guard fires only when both sides are empty

Inside doDiff() each side is joined and then trimmed, and only when both come back empty does the page raise the toast 「请输入要对比的文本」 and return: the test is on the pair, not on either side alone. Four measured cases:

  • Both sides empty, or 3 half-width spaces against nothing: the guard fires, nothing changes, and the stats and the rendered lines freeze on the previous round.
  • One side empty against 403 non-empty corpus lines: it runs, and the stats read 「新增 403 行,删除 1 行」, added 403 and removed 1.
  • Moving those 403 lines to side A gives the mirror image 「新增 1 行,删除 403 行」, added 1 and removed 403.
  • One side empty against all 555 segments: 「新增 554 行,删除 0 行」, added 554 and removed 0.

The last case: splitting an empty string yields 1 segment, and if the other side has no blank lines that empty segment becomes a removal of 1. Against the 152 blank rows it is absorbed into the LCS, removals fall to 0, and added rises to 554.

Guard hit Empty vs empty, 3 spaces vs empty, returns untouched One side empty, 403 non-empty lines Added 403, Removed 1 403 non-empty, one side empty Added 1, Removed 403 One side empty, all 555 segments Added 554, Removed 0 Leaner blank tier gives 148, common 154 = 148 + 6

Figure 1: the two guard tiers and how blank lines rewrite the removals.

The tie-break fixes deletes before adds

The backtrack condition reads dp[i][j-1] >= dp[i][j]: on a tie it steps into the added branch first and decrements j, the walk runs from the far end of the table backwards, and a reverse() at the end flips the stream, so the output always prints removals before additions:

  • a, b against b, a yields removed a, equal b, added a; after doDiff, side A shows a minus line for a, b, and a placeholder, while side B shows a placeholder, b, and a plus line for a.
  • old against new yields one adjacent removed-and-added pair; a, b against a, c yields equal, removed, added.
  • 1, 2, 3, 4 against 3, 4, 1, 2 yields removed 1 and 2, equal 3 and 4, added 1 and 2: the common block anchors on 3 and 4.

In a separate context where only >= becomes >, 4 of the same sample groups flip their order, the block swap re-anchors on 1 and 2, and one group stays byte-for-byte the same, a control record only.

Moving a line: take 10 lines and reinsert line 2 at position 9. Against itself the run is 0 added and 0 removed; after the move it is added 1, removed 1, equal 9, a result of 11 lines. There is no move detection in the code.

The stats line carries only two numbers

The page builds one template string, 新增 ${added} 行,删除 ${removed} 行, and extracting its digits yields exactly 2, so the whole-file run prints added 665 and removed 401 and nothing else. Neither the common-line count nor the LCS length at the end of the dp table enters the string. The copy above the input boxes, 「统计概览:显示新增行数、删除行数、修改行数」, promises a modified count the code cannot produce: the branch that counts modifications runs only when a line type is none of equal, removed and added, while the three push sites write exactly those three literals, and across all 1220 result lines the type set stays at three. Changing a few characters inside a line still moves the whole line onto both sides, and there is no word-level layer here, as an older article logged.

How result lines reach the DOM, and what survives clearing

Every result line is built with createElement and filled through textContent: 1220 divs per side, 2440 in total, all inserted in one pass. The container is max-height 400px with overflow auto, and the control run measured a scrollHeight of 29328px against a client height of 400px. The page ships no escaping helper and no copy button, just 2 matches of <button in the template, so a line containing angle brackets, an &, or quotes is written through textContent and reads back exactly as typed. The counterpart placeholder is a single U+00A0. Clicking 「清空」 only flips the container back to hidden, and 2 lines survive on each side.

CRLF has two layers, reviewed in one line: at the string layer, a line carrying \r against its bare twin is one removal plus one addition; through a textarea the carriage return is normalized into a line feed, and the pasted text runs with 0 additions and 0 removals. Trailing LF is its own entry: splitting leaves 1 extra empty segment on each side, and the file measured against its trailing-LF-trimmed copy gives equal 554, removed 1, added 0.

Whole-file run: 555 x 819 segments equal 154 removed 401 added 665 Result 1220 lines, 2440 divs dp 455920 = 556 x 820, median 120.5 ms

Figure 2: the three entries of the whole-file run.

Three readings of the common lines, and where they sit

The common lines admit three readings. As a multiset including blanks there are 154: the leaner file contributes 148 blank rows and only 6 non-empty lines survive the intersection. Counting distinct strings gives 6 kinds, or 5 once the empty string is dropped, and the separator made of 28 code units appears twice on each side. Counting identical lines at identical positions leaves only 25. Those 6 shared strings sit at the tails of both files, inside the explanatory block appended to each download: lines 544 to 553 in the first file and lines 808 to 817 in the second, and the strings themselves stay out of this article. The LCS of the whole run is exactly 154, and the LCS of the non-empty run is exactly 6. That these match the multiset intersections follows only from the two sides keeping the same order: it is a coincidence measured once, not an identity.

Frequently asked questions

Q: if I leave one side completely blank, does the page stop me with an error?

No. The guard fires only when both sides together are blank. One empty side against 403 non-empty lines runs anyway, reporting added 403 and removed 1.

Q: why do both the removals and the additions grow when I moved a single line?

Because the result set has no move type. lineDiff only emits equal, added and removed, so a moved line is booked as one addition and one removal.

Q: 154 common lines are counted, so why does the stats bar not show them?

The stats string holds only two numbers, added 665 and removed 401. The common-line count and the LCS length never enter it.

Resource notes

The two resource links exist only to name where the corpus came from. Both books are used here as bytes and lines and nothing else: this article quotes no sentence from them, offers no reading of their content, and treats the 6 shared tail strings purely as line-count bookkeeping. The page has no file-input channel, so whole files can only arrive by pasting.

📎 Related articles

Measuring a Pinyin Table With Liweng Duiyun: 7160 Hits, 21 Off-Table Chars, 232 Polyphones 2026-11-07 Does the Thousand Character Classic Hold 1000 Distinct Characters: Word Count Measures 996 in Range, 1000 Real Han, 995 Distinct 2026-11-05 Pasting All 120 Chapters of Dream of the Red Chamber Into a Character Frequency Counter: Why Rank 1 Is the Comma at 59,260 Hits 2026-11-04 SOS is 9 tones, the full alphabet is 104: how Morse timing is calculated 2026-10-21 What does a weight of 30 mean on a spinner? Sector angles and draws with or without replacement 2026-10-21