Created
August 20, 2025 02:24
-
-
Save rmorey/bf74ca69ec6555a52d10a515b6b434c0 to your computer and use it in GitHub Desktop.
5T's Pressed Pipe Counter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React, { useMemo, useState } from "react"; | |
| /** | |
| * Pressed Pipe Counter | |
| * -------------------- | |
| * A minimal, production-ready React app that counts "pressed" pipes in a string. | |
| * A pipe '|' is pressed iff its immediate left neighbor is ')' and its immediate right | |
| * neighbor is '('. Equivalently, count occurrences of the substring ")|(". | |
| * | |
| * - Live validation for allowed characters: only '(', ')' and '|'. | |
| * - Large, monospace input with instant results. | |
| * - Visualizer highlights pressed pipes. | |
| * - Sample cases and a random generator for quick testing. | |
| * - Pure React + Tailwind (no external component libs) for maximal portability. | |
| */ | |
| // Utility: validate characters and return indices of invalid ones | |
| function findInvalidPositions(s: string): number[] { | |
| const bad: number[] = []; | |
| for (let i = 0; i < s.length; i++) { | |
| const ch = s[i]; | |
| if (ch !== '(' && ch !== ')' && ch !== '|') bad.push(i); | |
| } | |
| return bad; | |
| } | |
| // Utility: count pressed pipes via regex (counts ")|(" | |
| function countPressedRegex(s: string): number { | |
| const m = s.match(/\)\|\(/g); | |
| return m ? m.length : 0; | |
| } | |
| // Utility: also return the exact indices of '|' characters that are pressed | |
| function pressedPipeIndices(s: string): number[] { | |
| const out: number[] = []; | |
| for (let i = 1; i < s.length - 1; i++) { | |
| if (s[i] === '|' && s[i - 1] === ')' && s[i + 1] === '(') out.push(i); | |
| } | |
| return out; | |
| } | |
| // Generate a random valid test string | |
| function randomString(len: number): string { | |
| const alphabet = ['(', ')', '|']; | |
| let res = ""; | |
| for (let i = 0; i < len; i++) { | |
| res += alphabet[Math.floor(Math.random() * alphabet.length)]; | |
| } | |
| return res; | |
| } | |
| // Some curated sample strings | |
| const SAMPLES: { label: string; s: string }[] = [ | |
| { label: "Single pressed", s: ")|(" }, | |
| { label: "None pressed", s: "()|()" }, | |
| { label: "Two pressed", s: ")|()|(" }, | |
| { label: "No presses (double pipe)", s: "((||))" }, | |
| { label: "Mixed", s: "))(|)(()|(" }, | |
| ]; | |
| export default function PressedPipesApp() { | |
| const [input, setInput] = useState<string>(")|()|((|())|(()|()|((" ); | |
| const [randLen, setRandLen] = useState<number>(24); | |
| const invalidPositions = useMemo(() => findInvalidPositions(input), [input]); | |
| const pressedIndices = useMemo(() => pressedPipeIndices(input), [input]); | |
| const count = useMemo(() => countPressedRegex(input), [input]); | |
| const hasInvalid = invalidPositions.length > 0; | |
| const copyCount = async () => { | |
| try { | |
| await navigator.clipboard.writeText(String(count)); | |
| alert("Copied result to clipboard."); | |
| } catch (e) { | |
| console.error(e); | |
| alert("Couldn't copy. You can copy manually: " + count); | |
| } | |
| }; | |
| const copyPressedIndices = async () => { | |
| try { | |
| await navigator.clipboard.writeText(JSON.stringify(pressedIndices)); | |
| alert("Copied pressed indices to clipboard."); | |
| } catch (e) { | |
| console.error(e); | |
| alert("Couldn't copy. You can copy manually: " + JSON.stringify(pressedIndices)); | |
| } | |
| }; | |
| return ( | |
| <div className="min-h-screen bg-zinc-50 text-zinc-900"> | |
| <div className="max-w-4xl mx-auto px-4 py-10"> | |
| <header className="mb-8"> | |
| <h1 className="text-3xl sm:text-4xl font-bold tracking-tight">Pressed Pipe Counter</h1> | |
| <p className="mt-2 text-zinc-600"> | |
| Enter a string using only <code className="px-1 bg-zinc-100 rounded">(</code>, | |
| <code className="px-1 bg-zinc-100 rounded">)</code>, and <code className="px-1 bg-zinc-100 rounded">|</code>. | |
| A pipe <code className="px-1 bg-zinc-100 rounded">|</code> is <em>pressed</em> iff its left neighbor is | |
| <code className="px-1 bg-zinc-100 rounded">)</code> and right neighbor is <code className="px-1 bg-zinc-100 rounded">(</code>. | |
| </p> | |
| </header> | |
| {/* Controls */} | |
| <div className="grid gap-4 sm:grid-cols-[1fr_auto] items-start"> | |
| <textarea | |
| value={input} | |
| onChange={(e) => setInput(e.target.value)} | |
| placeholder={"Type here… (only '(', ')', '|')"} | |
| className={ | |
| "w-full h-40 sm:h-48 p-4 rounded-2xl border outline-none font-mono text-base leading-6 " + | |
| (hasInvalid ? "border-red-400 focus:ring-2 ring-red-300" : "border-zinc-300 focus:ring-2 ring-blue-300") | |
| } | |
| /> | |
| <div className="flex sm:flex-col gap-3 sm:gap-2"> | |
| <button | |
| onClick={() => setInput("")} | |
| className="px-4 py-2 rounded-xl border border-zinc-300 bg-white hover:bg-zinc-50 shadow-sm" | |
| > | |
| Clear | |
| </button> | |
| <button | |
| onClick={() => setInput(randomString(randLen))} | |
| className="px-4 py-2 rounded-xl border border-zinc-300 bg-white hover:bg-zinc-50 shadow-sm" | |
| title="Generate a random valid string" | |
| > | |
| Random | |
| </button> | |
| <button | |
| onClick={copyCount} | |
| className="px-4 py-2 rounded-xl border border-zinc-300 bg-white hover:bg-zinc-50 shadow-sm" | |
| > | |
| Copy Count | |
| </button> | |
| </div> | |
| </div> | |
| {/* Random length slider */} | |
| <div className="mt-3 flex items-center gap-3"> | |
| <label htmlFor="len" className="text-sm text-zinc-600">Random length:</label> | |
| <input | |
| id="len" | |
| type="range" | |
| min={3} | |
| max={200} | |
| value={randLen} | |
| onChange={(e) => setRandLen(parseInt(e.target.value, 10))} | |
| className="w-56" | |
| /> | |
| <span className="text-sm tabular-nums">{randLen}</span> | |
| </div> | |
| {/* Result cards */} | |
| <section className="mt-6 grid gap-4 sm:grid-cols-3"> | |
| <div className="p-4 rounded-2xl bg-white border border-zinc-200 shadow-sm"> | |
| <div className="text-sm text-zinc-500">Pressed pipes</div> | |
| <div className="mt-1 text-3xl font-semibold tabular-nums">{count}</div> | |
| <div className="mt-3 text-xs text-zinc-500">O(n) time · O(1) space</div> | |
| </div> | |
| <div className={"p-4 rounded-2xl border shadow-sm " + (hasInvalid ? "bg-red-50 border-red-200" : "bg-white border-zinc-200") }> | |
| <div className={"text-sm " + (hasInvalid ? "text-red-600" : "text-zinc-500")}>Validation</div> | |
| {hasInvalid ? ( | |
| <div className="mt-1 text-sm text-red-700"> | |
| {invalidPositions.length} invalid {invalidPositions.length === 1 ? 'character' : 'characters'} at indices {" "} | |
| <code className="bg-white px-1 rounded">[{invalidPositions.join(', ')}]</code> | |
| </div> | |
| ) : ( | |
| <div className="mt-1 text-sm text-emerald-700">All characters valid ✓</div> | |
| )} | |
| </div> | |
| <div className="p-4 rounded-2xl bg-white border border-zinc-200 shadow-sm"> | |
| <div className="text-sm text-zinc-500">Pressed indices</div> | |
| <div className="mt-1 text-sm"> | |
| <code className="bg-zinc-50 px-2 py-1 rounded">[{pressedIndices.join(", ")}]</code> | |
| </div> | |
| <div className="mt-2"> | |
| <button onClick={copyPressedIndices} className="text-sm underline text-blue-600 hover:text-blue-700"> | |
| Copy indices | |
| </button> | |
| </div> | |
| </div> | |
| </section> | |
| {/* Visualizer */} | |
| <section className="mt-8"> | |
| <h2 className="text-lg font-semibold mb-2">Visualizer</h2> | |
| <div className="rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm overflow-x-auto"> | |
| <div className="font-mono text-base leading-6 whitespace-pre overflow-x-auto"> | |
| {input.length === 0 ? ( | |
| <span className="text-zinc-400">(your string will render here)</span> | |
| ) : ( | |
| input.split("").map((ch, i) => { | |
| const isPressed = i > 0 && i < input.length - 1 && ch === '|' && input[i - 1] === ')' && input[i + 1] === '('; | |
| const invalid = ch !== '(' && ch !== ')' && ch !== '|'; | |
| return ( | |
| <span | |
| key={i} | |
| className={ | |
| (invalid ? "bg-red-200 text-red-900" : isPressed ? "bg-amber-200 text-amber-900" : "") + | |
| " rounded px-0.5" | |
| } | |
| title={invalid ? `Invalid character at ${i}` : isPressed ? `Pressed '|' at ${i}` : undefined} | |
| > | |
| {ch} | |
| </span> | |
| ); | |
| }) | |
| )} | |
| </div> | |
| <div className="mt-2 text-xs text-zinc-500"> | |
| <span className="inline-block rounded px-1 bg-amber-200 text-amber-900 mr-2">'|'</span> pressed | |
| <span className="inline-block rounded px-1 bg-red-200 text-red-900 ml-4">char</span> invalid | |
| </div> | |
| </div> | |
| </section> | |
| {/* Samples */} | |
| <section className="mt-10"> | |
| <h2 className="text-lg font-semibold mb-2">Try examples</h2> | |
| <div className="flex flex-wrap gap-2"> | |
| {SAMPLES.map(({ label, s }) => ( | |
| <button | |
| key={label} | |
| onClick={() => setInput(s)} | |
| className="px-3 py-1.5 rounded-xl border border-zinc-300 bg-white hover:bg-zinc-50 shadow-sm text-sm" | |
| title={s} | |
| > | |
| {label} | |
| </button> | |
| ))} | |
| </div> | |
| </section> | |
| {/* Tests */} | |
| <section className="mt-10"> | |
| <h2 className="text-lg font-semibold mb-2">Built-in sanity tests</h2> | |
| <div className="rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm"> | |
| <Tests /> | |
| </div> | |
| </section> | |
| <footer className="mt-12 text-xs text-zinc-500"> | |
| Made with React & Tailwind. Algorithm: count occurrences of ")|(". Complexity O(n). | |
| </footer> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function Tests() { | |
| const cases: { s: string; expected: number }[] = [ | |
| { s: ")|(", expected: 1 }, | |
| { s: "||", expected: 0 }, | |
| { s: "()|()", expected: 0 }, | |
| { s: ")|()|(", expected: 2 }, | |
| { s: "((||))", expected: 0 }, | |
| { s: ")|(())|(()", expected: 1 }, | |
| ]; | |
| return ( | |
| <div className="grid gap-2"> | |
| {cases.map((c, idx) => { | |
| const got = countPressedRegex(c.s); | |
| const ok = got === c.expected; | |
| return ( | |
| <div key={idx} className="flex items-center justify-between px-3 py-2 rounded-xl border text-sm" | |
| style={{ borderColor: ok ? "#d1fae5" : "#fecaca", background: ok ? "#ecfdf5" : "#fef2f2" }} | |
| > | |
| <code className="font-mono mr-2">"{c.s}"</code> | |
| <div className="flex items-center gap-3"> | |
| <span className="text-zinc-600">expected <b>{c.expected}</b></span> | |
| <span className={ok ? "text-emerald-700" : "text-red-700"}>{ok ? "pass" : `fail (got ${got})`}</span> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| ); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment