|
| 1 | +<!DOCTYPE html> |
| 2 | +<html lang="en"> |
| 3 | +<head> |
| 4 | + <meta charset="UTF-8" /> |
| 5 | + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| 6 | + <title>tsb — String Operations</title> |
| 7 | + <style> |
| 8 | + * { box-sizing: border-box; margin: 0; padding: 0; } |
| 9 | + body { font-family: system-ui, sans-serif; background: #0d1117; color: #e6edf3; line-height: 1.6; } |
| 10 | + header { background: #161b22; border-bottom: 1px solid #30363d; padding: 1rem 2rem; display: flex; align-items: center; gap: 1rem; } |
| 11 | + header h1 { font-size: 1.4rem; font-weight: 700; } |
| 12 | + header .badge { background: #388bfd22; color: #58a6ff; border: 1px solid #388bfd55; border-radius: 2rem; padding: 0.2rem 0.8rem; font-size: 0.75rem; } |
| 13 | + main { max-width: 900px; margin: 2rem auto; padding: 0 2rem; } |
| 14 | + h2 { font-size: 1.1rem; color: #58a6ff; margin: 2rem 0 0.5rem; } |
| 15 | + p { color: #8b949e; margin-bottom: 0.75rem; font-size: 0.95rem; } |
| 16 | + .section { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 1.25rem; margin-bottom: 1.5rem; } |
| 17 | + .section h3 { font-size: 1rem; margin-bottom: 0.5rem; } |
| 18 | + .row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-top: 0.75rem; } |
| 19 | + label { display: block; font-size: 0.8rem; color: #8b949e; margin-bottom: 0.25rem; } |
| 20 | + input, select, textarea { width: 100%; background: #0d1117; border: 1px solid #30363d; border-radius: 6px; padding: 0.5rem 0.75rem; color: #e6edf3; font-size: 0.9rem; font-family: 'JetBrains Mono', 'Fira Code', monospace; } |
| 21 | + textarea { resize: vertical; min-height: 80px; } |
| 22 | + button { background: #238636; border: none; border-radius: 6px; color: #fff; cursor: pointer; font-size: 0.9rem; padding: 0.5rem 1.25rem; margin-top: 0.75rem; } |
| 23 | + button:hover { background: #2ea043; } |
| 24 | + pre { background: #0d1117; border: 1px solid #21262d; border-radius: 6px; padding: 0.75rem 1rem; overflow-x: auto; font-size: 0.85rem; white-space: pre-wrap; margin-top: 0.5rem; color: #7ee787; font-family: 'JetBrains Mono', 'Fira Code', monospace; } |
| 25 | + .error { color: #f85149; } |
| 26 | + .info { font-size: 0.82rem; color: #8b949e; margin-top: 0.4rem; } |
| 27 | + table { border-collapse: collapse; width: 100%; font-size: 0.85rem; } |
| 28 | + th, td { border: 1px solid #30363d; padding: 0.35rem 0.75rem; text-align: left; } |
| 29 | + th { background: #21262d; color: #8b949e; } |
| 30 | + </style> |
| 31 | +</head> |
| 32 | +<body> |
| 33 | +<header> |
| 34 | + <h1>tsb</h1> |
| 35 | + <span class="badge">string_ops</span> |
| 36 | + <span style="color:#8b949e; font-size:0.9rem;">Standalone string operations for Series and arrays</span> |
| 37 | +</header> |
| 38 | +<main> |
| 39 | + <p> |
| 40 | + <strong>string_ops</strong> provides module-level string functions that complement the |
| 41 | + <code>Series.str</code> accessor. All functions accept a <code>Series</code>, a |
| 42 | + <code>string[]</code>, or a scalar <code>string</code>. |
| 43 | + </p> |
| 44 | + |
| 45 | + <!-- strNormalize --> |
| 46 | + <div class="section"> |
| 47 | + <h3>strNormalize — Unicode normalisation</h3> |
| 48 | + <p>Normalise every element to NFC, NFD, NFKC, or NFKD. Useful when mixing text |
| 49 | + from different sources (e.g. macOS NFD vs Windows NFC).</p> |
| 50 | + <div class="row"> |
| 51 | + <div> |
| 52 | + <label>Input strings (one per line)</label> |
| 53 | + <textarea id="norm-input">é |
| 54 | +café |
| 55 | +file</textarea> |
| 56 | + </div> |
| 57 | + <div> |
| 58 | + <label>Normalization form</label> |
| 59 | + <select id="norm-form"> |
| 60 | + <option value="NFC">NFC (compose)</option> |
| 61 | + <option value="NFD">NFD (decompose)</option> |
| 62 | + <option value="NFKC">NFKC (compat compose)</option> |
| 63 | + <option value="NFKD">NFKD (compat decompose)</option> |
| 64 | + </select> |
| 65 | + </div> |
| 66 | + </div> |
| 67 | + <button onclick="runNormalize()">Run</button> |
| 68 | + <pre id="norm-out"></pre> |
| 69 | + </div> |
| 70 | + |
| 71 | + <!-- strGetDummies --> |
| 72 | + <div class="section"> |
| 73 | + <h3>strGetDummies — one-hot encode by delimiter</h3> |
| 74 | + <p>Split each string by a delimiter and produce a binary indicator DataFrame — |
| 75 | + one column per unique token. Equivalent to <code>pandas.Series.str.get_dummies()</code>.</p> |
| 76 | + <div class="row"> |
| 77 | + <div> |
| 78 | + <label>Input strings (one per line)</label> |
| 79 | + <textarea id="dum-input">a|b |
| 80 | +b|c |
| 81 | +a |
| 82 | +a|b|c</textarea> |
| 83 | + </div> |
| 84 | + <div> |
| 85 | + <label>Separator</label> |
| 86 | + <input id="dum-sep" value="|" /> |
| 87 | + <label style="margin-top:0.75rem">Column prefix (optional)</label> |
| 88 | + <input id="dum-prefix" value="" placeholder="e.g. tag" /> |
| 89 | + </div> |
| 90 | + </div> |
| 91 | + <button onclick="runGetDummies()">Run</button> |
| 92 | + <pre id="dum-out"></pre> |
| 93 | + </div> |
| 94 | + |
| 95 | + <!-- strExtractAll --> |
| 96 | + <div class="section"> |
| 97 | + <h3>strExtractAll — extract all regex matches</h3> |
| 98 | + <p>Find every non-overlapping regex match in each element. Returns a JSON-encoded |
| 99 | + array of match arrays per element — parse with <code>JSON.parse</code>.</p> |
| 100 | + <div class="row"> |
| 101 | + <div> |
| 102 | + <label>Input strings (one per line)</label> |
| 103 | + <textarea id="ext-input">abc 123 def 456 |
| 104 | +foo bar |
| 105 | +hello 99 world 42</textarea> |
| 106 | + </div> |
| 107 | + <div> |
| 108 | + <label>Pattern (regex, no slashes)</label> |
| 109 | + <input id="ext-pat" value="\d+" /> |
| 110 | + <label style="margin-top:0.75rem">Flags</label> |
| 111 | + <input id="ext-flags" value="" placeholder="e.g. i" /> |
| 112 | + </div> |
| 113 | + </div> |
| 114 | + <button onclick="runExtractAll()">Run</button> |
| 115 | + <pre id="ext-out"></pre> |
| 116 | + </div> |
| 117 | + |
| 118 | + <!-- strRemovePrefix / strRemoveSuffix --> |
| 119 | + <div class="section"> |
| 120 | + <h3>strRemovePrefix / strRemoveSuffix</h3> |
| 121 | + <p>Strip a leading or trailing string from elements only when it is present.</p> |
| 122 | + <div class="row"> |
| 123 | + <div> |
| 124 | + <label>Input strings (one per line)</label> |
| 125 | + <textarea id="affix-input">pre_alpha |
| 126 | +pre_beta |
| 127 | +gamma</textarea> |
| 128 | + </div> |
| 129 | + <div> |
| 130 | + <label>Prefix to remove</label> |
| 131 | + <input id="affix-prefix" value="pre_" /> |
| 132 | + <label style="margin-top:0.75rem">Suffix to remove</label> |
| 133 | + <input id="affix-suffix" value="" placeholder="_end" /> |
| 134 | + </div> |
| 135 | + </div> |
| 136 | + <button onclick="runAffix()">Run</button> |
| 137 | + <pre id="affix-out"></pre> |
| 138 | + </div> |
| 139 | + |
| 140 | + <!-- strTranslate --> |
| 141 | + <div class="section"> |
| 142 | + <h3>strTranslate — character-level substitution</h3> |
| 143 | + <p>Replace or delete individual characters using a lookup table. |
| 144 | + Format: one mapping per line as <code>from=to</code> or <code>from=</code> |
| 145 | + to delete.</p> |
| 146 | + <div class="row"> |
| 147 | + <div> |
| 148 | + <label>Input strings (one per line)</label> |
| 149 | + <textarea id="tr-input">hello world |
| 150 | +aeiou</textarea> |
| 151 | + </div> |
| 152 | + <div> |
| 153 | + <label>Translation table (from=to, one per line)</label> |
| 154 | + <textarea id="tr-table">a=A |
| 155 | +e=E |
| 156 | +o=0</textarea> |
| 157 | + </div> |
| 158 | + </div> |
| 159 | + <button onclick="runTranslate()">Run</button> |
| 160 | + <pre id="tr-out"></pre> |
| 161 | + </div> |
| 162 | + |
| 163 | + <!-- strCharWidth / strByteLength --> |
| 164 | + <div class="section"> |
| 165 | + <h3>strCharWidth & strByteLength — display & byte widths</h3> |
| 166 | + <p> |
| 167 | + <strong>strCharWidth</strong> counts columns for terminal display (CJK chars count as 2).<br> |
| 168 | + <strong>strByteLength</strong> counts UTF-8 bytes (useful for byte-limited APIs). |
| 169 | + </p> |
| 170 | + <label>Input strings (one per line)</label> |
| 171 | + <textarea id="width-input">hello |
| 172 | +こんにちは |
| 173 | +café |
| 174 | +😀🎉</textarea> |
| 175 | + <button onclick="runWidths()">Run</button> |
| 176 | + <pre id="width-out"></pre> |
| 177 | + </div> |
| 178 | +</main> |
| 179 | + |
| 180 | +<script type="module"> |
| 181 | +import { |
| 182 | + strNormalize, strGetDummies, strExtractAll, |
| 183 | + strRemovePrefix, strRemoveSuffix, |
| 184 | + strTranslate, strCharWidth, strByteLength, |
| 185 | +} from "/src/stats/string_ops.ts"; |
| 186 | +import { Series } from "/src/index.ts"; |
| 187 | + |
| 188 | +function makeSeriesFromTextarea(id) { |
| 189 | + const lines = document.getElementById(id).value.split("\n").filter(l => l.length > 0); |
| 190 | + return new Series({ data: lines }); |
| 191 | +} |
| 192 | + |
| 193 | +window.runNormalize = function() { |
| 194 | + try { |
| 195 | + const ser = makeSeriesFromTextarea("norm-input"); |
| 196 | + const form = document.getElementById("norm-form").value; |
| 197 | + const out = strNormalize(ser, form); |
| 198 | + const rows = [...out.toArray()].map((v, i) => `${i}: ${JSON.stringify(v)}`).join("\n"); |
| 199 | + document.getElementById("norm-out").textContent = rows; |
| 200 | + } catch (e) { document.getElementById("norm-out").textContent = "Error: " + e.message; } |
| 201 | +}; |
| 202 | + |
| 203 | +window.runGetDummies = function() { |
| 204 | + try { |
| 205 | + const ser = makeSeriesFromTextarea("dum-input"); |
| 206 | + const sep = document.getElementById("dum-sep").value || "|"; |
| 207 | + const prefix = document.getElementById("dum-prefix").value; |
| 208 | + const opts = prefix ? { sep, prefix, prefixSep: "_" } : { sep }; |
| 209 | + const df = strGetDummies(ser, opts); |
| 210 | + const cols = df.columns.values; |
| 211 | + // Build ASCII table |
| 212 | + let lines = cols.join("\t"); |
| 213 | + for (let r = 0; r < df.shape[0]; r++) { |
| 214 | + lines += "\n" + cols.map(c => df.col(c).values[r]).join("\t"); |
| 215 | + } |
| 216 | + document.getElementById("dum-out").textContent = |
| 217 | + `Shape: ${df.shape[0]}×${df.shape[1]}\n\n` + lines; |
| 218 | + } catch (e) { document.getElementById("dum-out").textContent = "Error: " + e.message; } |
| 219 | +}; |
| 220 | + |
| 221 | +window.runExtractAll = function() { |
| 222 | + try { |
| 223 | + const ser = makeSeriesFromTextarea("ext-input"); |
| 224 | + const pat = document.getElementById("ext-pat").value; |
| 225 | + const flags = document.getElementById("ext-flags").value; |
| 226 | + const out = strExtractAll(ser, pat, { flags }); |
| 227 | + const rows = [...out.toArray()].map((v, i) => { |
| 228 | + const m = JSON.parse(v); |
| 229 | + return `${i}: ${m.length} match(es) — ${JSON.stringify(m)}`; |
| 230 | + }).join("\n"); |
| 231 | + document.getElementById("ext-out").textContent = rows; |
| 232 | + } catch (e) { document.getElementById("ext-out").textContent = "Error: " + e.message; } |
| 233 | +}; |
| 234 | + |
| 235 | +window.runAffix = function() { |
| 236 | + try { |
| 237 | + const ser = makeSeriesFromTextarea("affix-input"); |
| 238 | + const prefix = document.getElementById("affix-prefix").value; |
| 239 | + const suffix = document.getElementById("affix-suffix").value; |
| 240 | + let out = ser; |
| 241 | + if (prefix) out = strRemovePrefix(out, prefix); |
| 242 | + if (suffix) out = strRemoveSuffix(out, suffix); |
| 243 | + const rows = [...out.toArray()].map((v, i) => `${i}: ${v}`).join("\n"); |
| 244 | + document.getElementById("affix-out").textContent = rows; |
| 245 | + } catch (e) { document.getElementById("affix-out").textContent = "Error: " + e.message; } |
| 246 | +}; |
| 247 | + |
| 248 | +window.runTranslate = function() { |
| 249 | + try { |
| 250 | + const ser = makeSeriesFromTextarea("tr-input"); |
| 251 | + const tableLines = document.getElementById("tr-table").value.split("\n").filter(l => l.trim()); |
| 252 | + const table = new Map(); |
| 253 | + for (const line of tableLines) { |
| 254 | + const eq = line.indexOf("="); |
| 255 | + if (eq === -1) continue; |
| 256 | + const from = line.slice(0, eq); |
| 257 | + const to = line.slice(eq + 1); |
| 258 | + table.set(from, to === "" ? null : to); |
| 259 | + } |
| 260 | + const out = strTranslate(ser, table); |
| 261 | + const rows = [...out.toArray()].map((v, i) => `${i}: ${v}`).join("\n"); |
| 262 | + document.getElementById("tr-out").textContent = rows; |
| 263 | + } catch (e) { document.getElementById("tr-out").textContent = "Error: " + e.message; } |
| 264 | +}; |
| 265 | + |
| 266 | +window.runWidths = function() { |
| 267 | + try { |
| 268 | + const ser = makeSeriesFromTextarea("width-input"); |
| 269 | + const charW = strCharWidth(ser); |
| 270 | + const byteL = strByteLength(ser); |
| 271 | + const strs = [...ser.toArray()]; |
| 272 | + let lines = "str\t\t\tchars\tdisplay-cols\tbytes"; |
| 273 | + for (let i = 0; i < strs.length; i++) { |
| 274 | + const str = strs[i]; |
| 275 | + lines += `\n${JSON.stringify(str)}\t\t${str.length}\t${charW.values[i]}\t\t${byteL.values[i]}`; |
| 276 | + } |
| 277 | + document.getElementById("width-out").textContent = lines; |
| 278 | + } catch (e) { document.getElementById("width-out").textContent = "Error: " + e.message; } |
| 279 | +}; |
| 280 | +</script> |
| 281 | +</body> |
| 282 | +</html> |
0 commit comments