Simplify Path (LC 71): given an absolute Unix-style path, return its
canonical form — one leading slash, no trailing slash, no . or ..
components, runs of slashes collapsed. The piece splits the path on / and
consumes the components left to right, and the thing to watch is that
every component does exactly one of three things to a stack of
directory names: a name is pushed, "" and . do nothing, and .. pops
the top. That is the whole algorithm. The rule everybody writes a special
branch for — .. when there is nothing to pop — is the same rule with
nothing to do: the root’s parent is the root, so the stack stays empty and
the walk moves on.
The path runs across the top as character tiles, the component being
consumed lit white, and under each component the glyph for what it did:
▲ pushed, ▼ popped, ○ popped nothing, · nothing. Empty components
get a glyph too, sitting in the gap between two slashes, so // is visibly a
component that did nothing rather than a case that was handled. On the right
the stack rises from the root line as a column of plates; a pop draws the
plate that just left as a dashed ghost above the top, and a pop with nothing
to pop draws a dashed outline where a plate would have been.
The answer is never edited. Under the legend is the path the stack
implies right now — / plus the plates joined by / — and it changes only
because the stack did. When the input runs out that line becomes the answer,
and there was no second pass to fix up slashes or strip a trailing one: the
canonical form is a property of the stack, not of the string.
Three rules, switchable, because the second and third are what gets written:
- stack;
..at the root pops nothing is the algorithm. - pop throws on empty is the same walk without the
if stack:guard — what an unguardedpop()does in Python (IndexError), Java (NoSuchElementException) and C++ (undefined behaviour). Theup from the rootandmore .. than depthpresets halt on it with a red ✕ and no answer; JavaScript and Ruby returnundefined/nilfrom an empty pop and so get this one right by accident. - any run of dots is a dot-command is the trap:
.is “here”, two or more dots are “up”. It is right on every input that has no such name — and...is a legal directory name. Thea name made of dotspreset (LeetCode’s own example 4) comes out/b/dinstead of/.../b/d, with the canonical path printed beside the wrong one.
Type any absolute path into the box (letters, digits, ., _, -, /),
or click any component to jump the walk to it.
Reuse
src/canonical.js is a framework-free ES module:
components(path)— the split, keeping empty components and the character span of each ({ text, start, end }), so a renderer can light the input.classify(text, stackDepth, rule)— what one component does:push | skip | pop | pop-empty | error.simplify(path)— the plain answer, under the correct rule.trace(path, { rule })— one frame per component plus a finaldoneframe:{ t, i, comp, action, stack, popped, answer, note }, with the stack after the component acted and the name it popped if it popped one. Also reportstruth(the correct answer),answer,correct,halted, per-actioncountsandmaxDepth. Same trace-per-tick shape aslowest-common,pruneandrebase.simulate(path)— all three rules over one input.
No rendering or timers in the module; the canvas demo is reference code.
Gotchas
dotsandthroware deliberately wrong. They exist so the failure can be watched on the inputs that trigger it; do not reuse them as answers.- The module accepts the problem’s full 3000 characters; the demo caps the box at 400 so the tile row stays readable.
- The demo bundles its own copy of
canonical.js(self-contained by contract); re-copy after editingsrc/. node scripts/screenshot-demo.mjs [out] [--preset=] [--rule=] [--at=] [--w= --h=]regeneratesthumb.pngthroughsite/scripts/lib/chromium.mjs.