Khắc had only ever been run on sentences. This release is what happened when
it was pointed at a real document: ten years of Samuel Pepys's diary, 106,002
lines, 1.3 million words.
It took 178 seconds. Splitting that same text into ten files, byte for
byte, producing the same 3,562 results, took 6.
That was the tell. How text is divided across files should not change the
total, and when it does, the cost is quadratic in something per document.
Profiling put 6,819 of 6,820 samples in one refiner. Three sites shared one
shape: each materialized the entire document before a match in order to
answer a question anchored at its end, so the cost was
matches x document length. Invisible in a sentence, where quadratic and
linear are the same number, and invisible to both the test suite and the
benchmark, because both measure one short string at a time.
Bounded windows fixed it. Same corpus, same machine, same event counts at
every size:
lines bytes before after
5000 306989 0.59 s 1.04 s
20000 1235869 5.10 s 2.12 s
40000 2547182 22.75 s 4.25 s
80000 5165554 87.54 s 8.50 s
106002 6878227 178.16 s 11.11 s
Cost per KB is flat now at about 1,700 us/KB, where it had degraded from
1,983 to 26,524. The one-file versus ten-file gap collapsed from 4.3x to 1.0x.
Output is identical field for field: nothing moved, only the clock.
The command line tool
khac reads files, directories or stdin in any of the fourteen locales and
tells you every date it found and where it is.
$ khac examples/notes/handover.md
2026-08-21 Friday
all day Ship the beta next Friday, then freeze the branch.
examples/notes/handover.md:1:15 [en] "next Friday,"
It deliberately does not try to be more than that. --format jsonl is the
real contract, one object per match, carrying a UTF-8 byte offset into the
file:
$ khac --format jsonl -q examples/notes/kickoff.md | jq -r '"\(.source) \(.byteOffset) \(.byteLength)"'
examples/notes/kickoff.md 25 15
examples/notes/kickoff.md 76 16
$ dd if=examples/notes/kickoff.md bs=1 skip=25 count=15 2>/dev/null
August 10, 2012
Widen that window yourself and you have whatever you needed: read back to the
previous full stop for the sentence, to the line start for the bullet, up the
file for the heading it sits under. Titles, agendas and calendar export are
programs that read this, not flags inside it. A guess that is wrong one time
in ten is worse than an offset that is always right.
Every one of the 8,327 matches in the Pepys corpus slices back to its exact
match text from (byteOffset, byteLength), and the same holds for Vietnamese
two-byte characters, CJK three-byte, emoji surrogate pairs and NFD combining
marks. Bytes rather than the UTF-16 the library reports internally, because
nothing downstream speaks UTF-16.
--reference-mtime is the flag that makes an archive readable: each file
resolves against its own modification date, so a note written in 2019 saying
"next Friday" means the Friday after it was written, not the one after today.
Overlap ranking no longer rewards whichever reading claims more
Two fixes that only make sense together.
Relative results were reporting almost nothing as certain, which was
dishonest: "in 2 days" knows the day it lands on. Fixing that alone made
things worse, because overlap resolution led on certainty, and certainty
counts what a result assigned, not what a writer wrote. A casual day word
resolving off the reference marks year, month and day certain without a single
character stating any of them.
So the more a reading inferred, the more likely it was to win. "next Mon"
went to the month over Monday. "next Weds" lost to the fragment "s next".
The French range "14h à 16h" lost to "de 14h". Every loser had claimed a
full set of components while stating none of them.
Certainty is honest now, and ranking leads on match length then parser order,
which is chrono's own rule. score remains public and meaningful; it is not a
ranking key. Measured against 720 corpus cases: four wrong answers recovered,
one lost.
Known, and not fixed here
Pointed at 17th century prose with no filtering, 88% of hits contain no digit.
"now" is 29% of them and no certainty level removes it. "sat" is 13% and is
usually the past tense of to sit. "pasty" parses as past + y = last year.
These are findings, not regressions, and they are what --require and the
certain field exist for. They are written up rather than quietly filtered,
because a date parser that pretends running prose is easy is lying about the
thing you would actually use it on.
Everything else
examples/ships runnable inputs, including the full Pepys corpus
(public domain, Project Gutenberg).- README leads with the tool and what it is for.
- 421 tests, 0 failures. Oracle 1,927 / 1,988 across 13 locales. Vietnamese
suite of 92, verified by a native speaker.
.package(url: "https://github.com/nhannht/Khac.git", from: "0.4.0")