<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[ToolkitLoop]]></title><description><![CDATA[Fast, private online tools for everyday tasks]]></description><link>https://toolkitloop.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6ac37ea690b64893480a7349/db84cb5d-b47f-4d25-b7b5-404301826018.png</url><title>ToolkitLoop</title><link>https://toolkitloop.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 14 Oct 2026 03:27:29 GMT</lastBuildDate><atom:link href="https://toolkitloop.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Why Character Counters Lie: Graphemes, SMS & X Rules]]></title><description><![CDATA[Most character counters disagree with each other — and with the platform you are posting to. The reason is not a bug: "character" means four different things depending on who is counting. Here is the ]]></description><link>https://toolkitloop.hashnode.dev/why-character-counters-lie-graphemes-sms-x-rules</link><guid isPermaLink="true">https://toolkitloop.hashnode.dev/why-character-counters-lie-graphemes-sms-x-rules</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[webdev]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[programming]]></category><dc:creator><![CDATA[bowen Tse]]></dc:creator><pubDate>Sun, 11 Oct 2026 04:34:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ac37ea690b64893480a7349/23da85f5-f0ee-4fc7-a30a-5e45a5a92751.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Most character counters disagree with each other — and with the platform you are posting to. The reason is not a bug: "character" means four different things depending on who is counting. Here is the map.</p>
<h2>Four units, one word</h2>
<ol>
<li><strong>Code units</strong> — what JavaScript's <code>.length</code> counts (UTF-16). <code>"👨‍👩‍👧‍👦".length === 11</code>.</li>
<li><strong>Code points</strong> — Unicode scalar values. The family emoji is 7.</li>
<li><strong>Grapheme clusters</strong> — what a human sees. The family emoji is 1. A flag is 1. <code>é</code> built from <code>e</code> + combining acute is 1.</li>
<li><strong>Weighted units</strong> — what X counts. URLs are 23, some ranges weigh 2.</li>
</ol>
<p>A counter is only "right" relative to the unit your destination uses. Mismatch the unit and you get the classic failures: a form that rejects text that fits, or accepts text the platform then truncates.</p>
<h2>Graphemes: the only unit readers care about</h2>
<p>ZWJ sequences, regional indicators, combining marks, emoji modifiers — all of these assemble multiple code points into one visible glyph. The browser-native way to count them:</p>
<pre><code class="language-js">const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
const graphemes = [...segmenter.segment(text)].length;
</code></pre>
<p>Any serious counter should default to graphemes. Code-unit counts are an implementation detail leaking into UX.</p>
<h2>X: the weighted regime</h2>
<p>X (Twitter) allows 280 weighted characters per standard post. The <a href="https://github.com/twitter/twitter-text">twitter-text</a> configuration assigns weights per range: most characters weigh 1, URLs are a flat <strong>23</strong> regardless of actual length, and emoji/wide ranges weigh 2. A 193-character draft with a 47-character link and one emoji lands around <strong>169 weighted</strong> — the link collapses, the emoji doubles. If your validation uses a plain count, it is validating against a limit that does not exist.</p>
<h2>SMS: the segment regime</h2>
<p>SMS billing counts <strong>segments</strong>, sized by encoding: GSM-7 gives 160 characters per single message (153 when concatenated); UCS-2 — triggered by a single emoji or CJK character — drops that to 70 (67 concatenated). The GSM-7 extension table (<code>€ ^ { } \ [ ] ~ |</code>) costs two units per character. One emoji does not just add a character; it re-encodes the entire message and more than halves its capacity.</p>
<h2>The tool</h2>
<p>Disclosure: I built <a href="https://toolkitloop.com/character-counter">toolkitloop.com/character-counter</a>, one of 30 free browser tools I maintain. It counts graphemes by default, has an X mode applying the weighted URL rule, reports with/without spaces, and runs entirely client-side — your draft never leaves the browser. A companion <a href="https://toolkitloop.com/social-media-character-limits">social media character limits guide</a> keeps the per-field numbers in one place.</p>
<p>The broader lesson: whenever you build around a "character limit," find out which of the four units the destination counts — then count that. Everything else is a guess wearing a number's clothes.</p>
]]></content:encoded></item><item><title><![CDATA[Fair Wheels Use crypto.getRandomValues(), Not Math.random()]]></title><description><![CDATA[Every "spin the wheel" widget you've seen has two halves, and only one of them matters. The animation — easing curves, tick sounds, confetti — is theater. The selection is math. If the math is wrong, ]]></description><link>https://toolkitloop.hashnode.dev/fair-wheels-use-crypto-getrandomvalues-not-math-random</link><guid isPermaLink="true">https://toolkitloop.hashnode.dev/fair-wheels-use-crypto-getrandomvalues-not-math-random</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[webdev]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[Open Source]]></category><dc:creator><![CDATA[bowen Tse]]></dc:creator><pubDate>Sat, 10 Oct 2026 04:10:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ac37ea690b64893480a7349/0e8356db-2514-47ad-aa18-829a4812b63b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Every "spin the wheel" widget you've seen has two halves, and only one of them matters. The animation — easing curves, tick sounds, confetti — is theater. The selection is math. If the math is wrong, the theater is a lie wearing confetti.</p>
<p>Most wheel implementations pick like this:</p>
<pre><code class="language-js">segments[Math.floor(Math.random() * segments.length)]
</code></pre>
<p>For a yes/no decision wheel this is <em>fine</em>. <code>Math.random()</code> is deterministic and theoretically predictable, but nobody is attacking your lunch decision. The problem starts when the wheel picks giveaway winners, assigns on-call rotations, or does anything where someone can lose something. Then "almost uniform" stops being good enough, and modulo bias — the slight unevenness of <code>Math.floor(Math.random() * n)</code> for certain <code>n</code> — becomes a real, if small, unfairness.</p>
<h2>Rejection sampling in 15 lines</h2>
<p>The Web Crypto API exposes OS-level entropy via <code>crypto.getRandomValues()</code>. Turning random 32-bit integers into a uniform index in <code>[0, n)</code> without modulo bias is a textbook rejection sample:</p>
<pre><code class="language-js">function fairIndex(n) {
  const max = 0xffffffff;
  const limit = max - (max % n); // reject the biased tail
  const buf = new Uint32Array(1);
  let x;
  do {
    crypto.getRandomValues(buf);
    x = buf[0];
  } while (x &gt;= limit);
  return x % n;
}
</code></pre>
<p>Values at or above <code>limit</code> would map unevenly onto the segments, so we throw them away and draw again. The expected retry count is ~1.0 — the loop is practically free, and the distribution is exactly uniform.</p>
<h2>Architecture: selection before animation</h2>
<p>Clean wheel code separates the two halves completely:</p>
<ol>
<li><strong>Select</strong> the winning index with <code>fairIndex()</code> — pure function, unit-testable.</li>
<li><strong>Derive</strong> the final rotation angle from the index (a few full turns for drama, then land mid-slice).</li>
<li><strong>Animate</strong> to that angle with CSS transitions.</li>
</ol>
<p>Because selection is decoupled, you can verify fairness offline: run 100,000 draws, histogram the results, and confirm uniformity before any pixel moves. If you're building any picker — wheels, lotteries, shuffles — this test is cheap and catches real bugs (off-by-one slices, rounding in angle math, you name it).</p>
<h2>Seedless, serverless, stateless</h2>
<p>The whole pipeline runs client-side. There's no seed to protect, no server to trust, and — relevant if your wheel handles entrant names — nothing uploaded anywhere. For a small giveaway tool, "we never see your list" is the simplest privacy policy there is.</p>
<h2>Disclosure</h2>
<p>I built a free wheel implementing exactly this at <a href="https://toolkitloop.com/yes-no-wheel">toolkitloop.com/yes-no-wheel</a> — yes/no mode plus custom segments, no signup. It's one of 30 client-side tools I maintain at ToolkitLoop. The patterns above are general; adapt them freely.</p>
<p>If you've ever audited randomness in a picker, shuffler, or A/B splitter — what did you find? The subtle bugs are always more interesting than the obvious ones.</p>
]]></content:encoded></item></channel></rss>