<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ggwork</title>
    <description>The latest articles on DEV Community by ggwork (@ggwork).</description>
    <link>https://dev.to/ggwork</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4070458%2Fc1960c8f-08ea-49bc-b4b7-1c0f82814c7f.jpeg</url>
      <title>DEV Community: ggwork</title>
      <link>https://dev.to/ggwork</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ggwork"/>
    <language>en</language>
    <item>
      <title>How to Build a Client-Side Chinese Converter with OpenCC (No Server Needed)</title>
      <dc:creator>ggwork</dc:creator>
      <pubDate>Sun, 06 Sep 2026 07:06:17 +0000</pubDate>
      <link>https://dev.to/ggwork/how-to-build-a-client-side-chinese-converter-with-opencc-no-server-needed-27b2</link>
      <guid>https://dev.to/ggwork/how-to-build-a-client-side-chinese-converter-with-opencc-no-server-needed-27b2</guid>
      <description>&lt;p&gt;While working on a browser-based utility project recently, I hit a surprisingly tricky problem: I needed to add Chinese Simplified ⇄ Traditional conversion to a tool. The catch? I didn't want to send users' text to a server, and I definitely didn't want to maintain a backend just for text transformation.&lt;/p&gt;

&lt;p&gt;The obvious answer was to find a JavaScript library that could handle this entirely in the browser. But as I started researching, I realized this was more nuanced than I initially thought.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Existing Solutions
&lt;/h2&gt;

&lt;p&gt;Let me be honest: there are plenty of Chinese converter tools out there. The issue is that most of them either:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rely on server-side APIs&lt;/strong&gt; – which means your text leaves the browser. For a privacy-focused tool, that's a non-starter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use naive character mapping&lt;/strong&gt; – where each character is mapped individually. This works for most cases but completely misses &lt;strong&gt;phrase-level conversion&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's what I mean by phrase-level conversion. Consider the word "软件" (software). A naive character-by-character conversion would give you "軟件" in Traditional Chinese. But that's wrong for Taiwan – they say "軟體". Similarly, "鼠标" (mouse) should become "滑鼠" in Taiwan, not "鼠標".&lt;/p&gt;

&lt;p&gt;This is the classic trap of Chinese conversion. It's not just about character mapping; it's about &lt;strong&gt;regional vocabulary differences&lt;/strong&gt;. And that's precisely where OpenCC shines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why OpenCC?
&lt;/h2&gt;

&lt;p&gt;OpenCC (Open Chinese Convert) is the gold standard for Chinese conversion. It's been around for years, maintained by the open-source community, and used by major projects. The key features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Phrase-aware conversion&lt;/strong&gt; – it understands context and converts whole phrases, not just characters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Region-specific output&lt;/strong&gt; – separate conversion rules for Taiwan vs. Hong Kong Traditional&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two-way support&lt;/strong&gt; – both Simplified → Traditional and Traditional → Simplified&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The problem? OpenCC is primarily a Python/C++ library. Using it in the browser requires a JavaScript port. That's where &lt;code&gt;opencc-js&lt;/code&gt; comes in – it's a direct JavaScript implementation that runs entirely client-side.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture Decision
&lt;/h2&gt;

&lt;p&gt;I wanted this tool to work entirely in the browser. No server, no API calls, no text leaving the user's device. This meant:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Load the conversion engine via CDN&lt;/li&gt;
&lt;li&gt;Package the dictionaries locally&lt;/li&gt;
&lt;li&gt;Cache converter instances to avoid re-initialization&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first decision was how to load the library. I went with the UMD build from a CDN:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://fastly.jsdelivr.net/npm/opencc-js@1.4.1/dist/umd/full.js"&lt;/span&gt; &lt;span class="na"&gt;onerror=&lt;/span&gt;&lt;span class="s"&gt;"window.__openccFailed=true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;onerror&lt;/code&gt; handler was crucial – if the CDN fails to load, we need to show a graceful error message instead of a broken tool. This is something I learned from experience: always plan for network failures, even in client-side tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Implementation
&lt;/h2&gt;

&lt;p&gt;The API is refreshingly simple. Once the library loads, creating a converter is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;converter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;OpenCC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Converter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tw&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;converter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;汉字&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// "漢字"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;from&lt;/code&gt; and &lt;code&gt;to&lt;/code&gt; parameters accept region codes: &lt;code&gt;cn&lt;/code&gt; for mainland China, &lt;code&gt;tw&lt;/code&gt; for Taiwan, &lt;code&gt;hk&lt;/code&gt; for Hong Kong. This gives us four conversion directions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simplified → Traditional (Taiwan): &lt;code&gt;{from: 'cn', to: 'tw'}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Simplified → Traditional (Hong Kong): &lt;code&gt;{from: 'cn', to: 'hk'}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Traditional (Taiwan) → Simplified: &lt;code&gt;{from: 'tw', to: 'cn'}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Traditional (Hong Kong) → Simplified: &lt;code&gt;{from: 'hk', to: 'cn'}&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But there's a performance consideration. Creating a converter isn't free – it loads and initializes the dictionary data. Creating a new converter on every conversion would be wasteful. The solution is simple caching:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;converterCache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getConverter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;converterCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;converterCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;OpenCC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Converter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;converterCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, the first conversion in each direction pays the initialization cost, but subsequent conversions are instant.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI-Assisted Development Experience
&lt;/h2&gt;

&lt;p&gt;Here's where things got interesting. I built this tool with heavy assistance from Claude AI, and the experience was... educational.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the AI Got Right
&lt;/h3&gt;

&lt;p&gt;The initial scaffolding was surprisingly good. I described the requirements – a browser-based converter with four conversion directions, copy functionality, and dark mode support – and the AI produced a working skeleton within minutes. The CSS was particularly well-handled, including the &lt;code&gt;prefers-color-scheme&lt;/code&gt; media query for dark mode.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the AI Got Wrong
&lt;/h3&gt;

&lt;p&gt;The first version had a critical flaw: it was trying to use &lt;code&gt;opencc-js&lt;/code&gt; as an ES module, which doesn't work with the UMD build. The error was subtle – the import statement looked correct, but the module resolution failed at runtime.&lt;/p&gt;

&lt;p&gt;The AI also initially tried to create a new converter on every conversion, which would have been a performance disaster. It took a specific prompt to add caching:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Cache the converter instances so they're only created once per direction."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The Back-and-Forth
&lt;/h3&gt;

&lt;p&gt;The real value of AI assistance came from iteration. For example, the AI initially handled the CDN failure case poorly – it just let the page break. I had to explicitly ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What happens if the CDN fails to load? Add error handling."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's when we added the &lt;code&gt;onerror&lt;/code&gt; callback and a status message system. The AI wouldn't have thought about this on its own – it was focused on the happy path.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hard Problem: Regional Differences
&lt;/h2&gt;

&lt;p&gt;The most interesting engineering challenge wasn't technical – it was linguistic. Consider these conversions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"软件" → Taiwan: "軟體" (not "軟件")&lt;/li&gt;
&lt;li&gt;"鼠标" → Taiwan: "滑鼠" (not "鼠標")&lt;/li&gt;
&lt;li&gt;"网络" → Taiwan: "網路" (not "網絡")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't character-level transformations. They require &lt;strong&gt;word-level knowledge&lt;/strong&gt;. OpenCC handles this through its extensive dictionary system, but it means the conversion isn't always predictable. This is actually a feature – it's what makes the output sound natural to native speakers.&lt;/p&gt;

&lt;p&gt;For the UI, this meant I couldn't just label the options generically. Users need to know that "Simplified → Traditional" isn't enough – they need to choose between Taiwan and Hong Kong variants. The UI needed to be explicit about these regional differences.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Considerations
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;full.js&lt;/code&gt; build of opencc-js is about 2MB – it includes all the dictionaries. That's a significant download, but it's a one-time cost. Once loaded, the conversion itself is fast:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First conversion after load: ~50-100ms (dictionary initialization)&lt;/li&gt;
&lt;li&gt;Subsequent conversions: &amp;lt;5ms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The caching strategy was crucial. Without it, every conversion would pay the initialization cost. With it, the tool feels instant after the first use.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Client-side libraries have hidden costs.&lt;/strong&gt; The 2MB dictionary download is worth it for privacy, but it's a real trade-off. If I were building this for a high-traffic site, I'd consider lazy-loading the library only when the user actually needs conversion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Regional variants are a UX problem, not just a technical one.&lt;/strong&gt; Users often don't know whether they need Taiwan or Hong Kong Traditional. The tool needs to make this distinction clear without overwhelming them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. AI assistance works best with specific prompts.&lt;/strong&gt; The AI didn't anticipate edge cases like CDN failures or performance issues. But when I asked for specific improvements, it implemented them correctly. The collaboration worked because I knew what questions to ask.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;During this process, I built a small browser-based tool to make this workflow easier. It handles all four conversion directions, works entirely client-side, and respects user privacy by never sending text to a server.&lt;/p&gt;

&lt;p&gt;The tool is part of a larger collection of browser-based developer utilities I've been building. If you're curious, you can find it at &lt;a href="https://craftvo.app/en/tool/chinese-convert" rel="noopener noreferrer"&gt;Craftvo&lt;/a&gt;. But more importantly, if you're dealing with Chinese text conversion in your own projects, OpenCC is worth a serious look – it's the difference between output that looks machine-translated and output that reads naturally.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; &lt;code&gt;javascript&lt;/code&gt;, &lt;code&gt;chinese&lt;/code&gt;, &lt;code&gt;opensource&lt;/code&gt;, &lt;code&gt;webdev&lt;/code&gt;, &lt;code&gt;i18n&lt;/code&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How I Built a Birthday Number Generator That Actually Makes Sense (Sort Of)</title>
      <dc:creator>ggwork</dc:creator>
      <pubDate>Fri, 04 Sep 2026 05:37:50 +0000</pubDate>
      <link>https://dev.to/ggwork/how-i-built-a-birthday-number-generator-that-actually-makes-sense-sort-of-5cfa</link>
      <guid>https://dev.to/ggwork/how-i-built-a-birthday-number-generator-that-actually-makes-sense-sort-of-5cfa</guid>
      <description>&lt;p&gt;Last week, I found myself staring at my calendar, wondering if there was any meaningful way to derive lottery numbers from a birthday. Not because I believe in lucky numbers — I'm a developer, I believe in deterministic algorithms — but because I kept seeing friends post their "birthday numbers" on social media, and I wanted to understand the logic (or lack thereof) behind it.&lt;/p&gt;

&lt;p&gt;So I did what any reasonable developer would do: I built a tool for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Random Isn't Random Enough
&lt;/h2&gt;

&lt;p&gt;Here's the thing about lottery number generators — most of them just use &lt;code&gt;Math.random()&lt;/code&gt;. That's fine if you want truly random numbers, but it's boring. Nobody feels a connection to a number that came from a JavaScript PRNG seed.&lt;/p&gt;

&lt;p&gt;What people actually want is a &lt;em&gt;story&lt;/em&gt; behind their numbers. "My birthday gives me these numbers" sounds way better than "a random number generator gave me these numbers." It's the difference between having a meaningful ritual and just gambling.&lt;/p&gt;

&lt;p&gt;The challenge: how do you map a date like &lt;code&gt;1990-05-20&lt;/code&gt; to a valid set of lottery numbers in a way that feels intentional, produces valid ranges, and doesn't require a server?&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Algorithm" (I Use That Term Loosely)
&lt;/h2&gt;

&lt;p&gt;Let me walk you through the logic, because it's actually a fun little puzzle.&lt;/p&gt;

&lt;p&gt;For a Double Color Ball (双色球), you need 6 red balls (1-33) and 1 blue ball (1-16). For 3D/P3, you need 3 digits (0-9).&lt;/p&gt;

&lt;p&gt;The naive approach would be something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Don't do this&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;redBall&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;year&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This fails immediately because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;1990 % 33 = 10&lt;/code&gt; — fine, but what about the month and day?&lt;/li&gt;
&lt;li&gt;You need 6 unique numbers, and simple modulo arithmetic will produce duplicates fast&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;map33&lt;/code&gt; needs to handle negative numbers and zero gracefully&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The actual solution came from a classic programming trick I remembered from implementing hash functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;map33&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;33&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;33&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the "true modulo" pattern — it handles negative numbers correctly, which matters more than you'd think. When you're deriving numbers from date components and custom lucky numbers, you can easily end up with negative intermediate values.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI Collaboration: Where It Helped and Where It Didn't
&lt;/h2&gt;

&lt;p&gt;Full disclosure: I built most of this with an AI assistant. Here's what that actually looked like.&lt;/p&gt;

&lt;h3&gt;
  
  
  What AI Got Right
&lt;/h3&gt;

&lt;p&gt;I started with a prompt like: "Build me a tool that takes a birthday and generates lottery numbers. It should map date components to valid ranges and support custom lucky numbers."&lt;/p&gt;

&lt;p&gt;The AI immediately understood the domain. It knew about 双色球's structure (6+1), it knew about 3D's 3-digit format, and it generated the &lt;code&gt;map33&lt;/code&gt; function correctly on the first try. That's genuinely impressive — it's not just pattern-matching; it's understanding the constraints of the problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where It Stumbled
&lt;/h3&gt;

&lt;p&gt;The first version had a critical bug: it didn't handle duplicate numbers. If your birthday somehow produced the same number twice (which happens more often than you'd think), you'd end up with 5 red balls instead of 6. The AI's solution was to just... not handle it. Classic.&lt;/p&gt;

&lt;p&gt;I had to prompt it specifically: "What happens if the date components map to the same number? How do you ensure 6 unique red balls?" That's when it added the deduplication logic with a "fill from the edges" approach — if you can't get enough unique numbers from the date, it pulls from the extremes (1 and 33) to fill the gaps.&lt;/p&gt;

&lt;h3&gt;
  
  
  The CSS Nightmare
&lt;/h3&gt;

&lt;p&gt;Here's where AI really struggled: the styling. The tool needed to work in both light and dark mode, look decent on mobile, and present the numbers in a way that felt like a lottery ticket.&lt;/p&gt;

&lt;p&gt;The AI's first CSS attempt was a mess. It used fixed pixel values everywhere, didn't handle the &lt;code&gt;prefers-color-scheme&lt;/code&gt; media query properly, and the ticket layout broke on mobile. I had to iterate on the CSS more than the JavaScript — which, if you've done any web development, you know is the real truth of frontend work.&lt;/p&gt;

&lt;p&gt;The final version uses CSS custom properties for theming:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ffffff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#111827&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#3b82f6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefers-color-scheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;--bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#1a1a2e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#e2e8f0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#60a5fa&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This made the dark mode transition trivial — no JavaScript, no class toggling, just good CSS architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture Decision: Zero Dependencies
&lt;/h2&gt;

&lt;p&gt;I made a deliberate choice early on: this tool would have zero dependencies. No React, no Vue, no build step. Just vanilla HTML, CSS, and JavaScript in a single file.&lt;/p&gt;

&lt;p&gt;Why? Because the tool is so simple that any framework would be overkill. The entire logic is about 50 lines of JavaScript. Adding React to this would be like using a flamethrower to light a candle.&lt;/p&gt;

&lt;p&gt;But here's the trade-off I had to accept: no framework means I'm responsible for everything. State management? That's just a variable. DOM updates? That's &lt;code&gt;innerHTML&lt;/code&gt;. Internationalization? That's a dictionary object and a language detector.&lt;/p&gt;

&lt;p&gt;The i18n was actually a fun challenge. The tool needs to support both Chinese and English, and I didn't want to load a library for that. The solution was a simple dictionary pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;I18N&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;zh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;生日幸运数字选号器&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;gen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;生成号码&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Birthday Number Picker&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;gen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Generate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;detectLang&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lang&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zh&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zh&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Language detection via URL parameter first, then falls back to browser settings. It's not perfect — someone with a Chinese browser but English preference gets the short end — but it covers 95% of cases without any external services.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Why" Behind Every Decision
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why client-side only?
&lt;/h3&gt;

&lt;p&gt;Privacy. If someone enters their birthday, that's personal data. Sending it to a server feels unnecessary and potentially creepy. With everything client-side, the data never leaves the browser. It's also faster — no network requests, instant response.&lt;/p&gt;

&lt;p&gt;The trade-off: I can't track usage patterns or improve the "algorithm" based on user behavior. But for a fun utility tool, that's a good trade.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the specific number mapping?
&lt;/h3&gt;

&lt;p&gt;I could have done something more complex — like hashing the date string and using the hash as a seed. But that feels too opaque. The whole point is that the user can see the connection between their birthday and the numbers. So I kept it transparent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Year's last two digits → maps to a red ball&lt;/li&gt;
&lt;li&gt;Month → maps to a red ball&lt;/li&gt;
&lt;li&gt;Day → maps to a red ball&lt;/li&gt;
&lt;li&gt;Custom lucky numbers → each maps to a red ball&lt;/li&gt;
&lt;li&gt;Blue ball comes from a combination of all components&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This way, when someone asks "where did this number come from?", you can actually explain it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the disclaimer?
&lt;/h3&gt;

&lt;p&gt;Because I have to be responsible. This tool is for entertainment, and it's easy for people to get superstitious about numbers. The disclaimer is front and center:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;本工具按趣味规则将日期转换为号码，与开奖毫无关联，中奖概率不因此而改变。请理性购彩，量力而行。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's not legal advice — I'm not a lawyer — but it's honest. The tool doesn't change your odds. It just makes the numbers feel meaningful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance: Because Even Simple Things Can Be Slow
&lt;/h2&gt;

&lt;p&gt;The whole tool runs in microseconds, but I still thought about performance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No framework runtime&lt;/strong&gt;: The page loads instantly because there's nothing to parse&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No external assets&lt;/strong&gt;: No CDN fonts, no icon libraries, no analytics scripts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Efficient DOM updates&lt;/strong&gt;: Only update the result container when generating new numbers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The entire page is under 4KB of HTML, CSS, and JavaScript. That's smaller than a single image file. It loads faster than you can blink.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. AI is great at constraints, bad at edge cases
&lt;/h3&gt;

&lt;p&gt;The AI understood the problem domain well but kept missing edge cases: duplicates, empty input, invalid dates. I had to explicitly ask about these. It's like pair programming with a brilliant but inexperienced developer — you have to think about what they're not thinking about.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. CSS is still the hardest part
&lt;/h3&gt;

&lt;p&gt;Every time. The JavaScript logic was solved in minutes. The CSS took hours. And the most important part wasn't the layout — it was the theming system that respects user preferences.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Simplicity is a feature
&lt;/h3&gt;

&lt;p&gt;I could have made this a full app with accounts, saved numbers, and sharing features. But the constraint — single file, no dependencies, instant load — forced better decisions. The tool does one thing and does it well.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Entertainment tools need to be honest
&lt;/h3&gt;

&lt;p&gt;There's a fine line between fun and misleading. The tool is explicitly labeled as entertainment, and the disclaimer is prominent. You can have fun with numbers without pretending there's magic involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;The tool works. You pick a date, optionally add lucky numbers, and get a set of numbers that feel connected to your birthday. It's not going to change your odds of winning — nothing will — but it makes the ritual more meaningful.&lt;/p&gt;

&lt;p&gt;If you're curious, you can try it here: &lt;a href="https://craftvo.app/en/tool/lottery-birthday-picker" rel="noopener noreferrer"&gt;Birthday Number Picker&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The entire thing is a single HTML file. No build step, no dependencies, no server. Just a date input, a few math functions, and a CSS custom property for dark mode.&lt;/p&gt;

&lt;p&gt;And honestly? That's exactly what a tool like this should be.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to Build a Chemical Equation Balancer with Linear Algebra in Vanilla JavaScript</title>
      <dc:creator>ggwork</dc:creator>
      <pubDate>Fri, 04 Sep 2026 02:29:34 +0000</pubDate>
      <link>https://dev.to/ggwork/how-to-build-a-chemical-equation-balancer-with-linear-algebra-in-vanilla-javascript-1fb5</link>
      <guid>https://dev.to/ggwork/how-to-build-a-chemical-equation-balancer-with-linear-algebra-in-vanilla-javascript-1fb5</guid>
      <description>&lt;p&gt;While working on a collection of browser-based developer tools, I kept running into the same problem: I needed to balance chemical equations for a chemistry study tool, and every existing solution was either a heavy external API call or a clunky desktop app. I wanted something that worked entirely in the browser — no server, no dependencies, just pure math and JavaScript.&lt;/p&gt;

&lt;p&gt;The result was a small single-file tool that solves this with linear algebra. Here's how I approached it, including the parts where my AI assistant confidently led me astray.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Balancing Equations Is Linear Algebra in Disguise
&lt;/h2&gt;

&lt;p&gt;Balancing a chemical equation like &lt;code&gt;H2 + O2 = H2O&lt;/code&gt; is essentially solving a system of linear equations. Each element gives you one equation, and you're solving for the coefficients that make atoms conserved on both sides.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;H2 + O2 = H2O&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hydrogen: 2a = 2c&lt;/li&gt;
&lt;li&gt;Oxygen: 2b = c&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Where a, b, and c are the coefficients for H2, O2, and H2O respectively.&lt;/p&gt;

&lt;p&gt;The trick is that this is an underdetermined system — you have more variables than equations. The solution is a vector in the null space of the coefficient matrix. Find that vector, scale it to integers, and you're done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parsing Chemical Formulas: The Part That Made Me Question My Life Choices
&lt;/h2&gt;

&lt;p&gt;The math is elegant. The parsing is where things get ugly.&lt;/p&gt;

&lt;p&gt;Chemical formulas have nested parentheses: &lt;code&gt;Al2(SO4)3&lt;/code&gt; means 2 Al, 3 S, and 12 O. I needed a parser that could handle this recursively.&lt;/p&gt;

&lt;p&gt;My initial approach was a simple regex-based parser. That worked for flat formulas like &lt;code&gt;H2O&lt;/code&gt; but completely broke on anything with parentheses.&lt;/p&gt;

&lt;p&gt;The better approach: a recursive descent parser that processes the formula character by character.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parseFormula&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formula&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parseGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;multiplier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;localCounts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
        &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;formula&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formula&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subCounts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseGroup&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="c1"&gt;// Handle subscript after closing paren&lt;/span&gt;
                &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;subMult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\d&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formula&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nx"&gt;subMult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formula&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
                    &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;elem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subCounts&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nx"&gt;localCounts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;elem&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localCounts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;elem&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;subMult&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formula&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;localCounts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;A-Z&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formula&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;elem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;formula&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
                &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;a-z&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formula&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nx"&gt;elem&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;formula&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\d&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formula&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formula&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
                    &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="nx"&gt;localCounts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;elem&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localCounts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;elem&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;localCounts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseGroup&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// Apply top-level multiplier if needed&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight: handle the &lt;code&gt;(SO4)3&lt;/code&gt; pattern by recursively parsing inside parentheses, then multiplying all counts by the subscript. This small function handles the recursive structure elegantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gaussian Elimination: Where the AI Saved My Sanity
&lt;/h2&gt;

&lt;p&gt;The linear algebra part is where most implementations fail. Here's the core idea:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build a matrix where rows are elements and columns are compounds&lt;/li&gt;
&lt;li&gt;Reactants get positive coefficients, products get negative&lt;/li&gt;
&lt;li&gt;Find the null space vector using Gaussian elimination&lt;/li&gt;
&lt;li&gt;Scale to smallest integer coefficients&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The AI assistant was actually quite good at this part. It generated a solid Gaussian elimination implementation on the first try. The issue was everything around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI Collaboration: A Story of Confident Mistakes
&lt;/h2&gt;

&lt;p&gt;Here's where I need to be honest about the AI-assisted development experience. I'm using this process for all my tools now, and it's a mixed bag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the AI did well:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Gaussian elimination algorithm was correct on the first attempt&lt;/li&gt;
&lt;li&gt;The basic structure was solid&lt;/li&gt;
&lt;li&gt;The i18n setup was clean&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What the AI got wrong:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It initially parsed formulas incorrectly for compounds with multiple capital letters like &lt;code&gt;NaCl&lt;/code&gt; — it treated &lt;code&gt;Na&lt;/code&gt; as &lt;code&gt;N&lt;/code&gt; followed by &lt;code&gt;a&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;It didn't handle the case where coefficients could be zero (which means an element appears on only one side)&lt;/li&gt;
&lt;li&gt;The UI was ugly — I had to add proper styling and dark mode support myself&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The parsing bug was particularly frustrating. The AI kept assuming element symbols are single letters followed by optional lowercase. That works for &lt;code&gt;H2O&lt;/code&gt; but breaks for &lt;code&gt;NaCl&lt;/code&gt; or &lt;code&gt;CaCO3&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I had to explicitly show it the pattern for multi-letter element symbols:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Wrong: /[A-Z][a-z]?/ - only handles one lowercase letter&lt;/span&gt;
&lt;span class="c1"&gt;// Right: /[A-Z][a-z]*/ - handles Na, Ca, Mg, etc.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The "Works on My Machine" Moment
&lt;/h2&gt;

&lt;p&gt;The first version worked perfectly for all my test cases. I was ready to ship it.&lt;/p&gt;

&lt;p&gt;Then I tried &lt;code&gt;Fe + O2 = Fe2O3&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The AI's implementation gave me &lt;code&gt;4Fe + 3O2 = 2Fe2O3&lt;/code&gt; — which is correct, but the intermediate steps were wrong. The matrix had a zero row that wasn't being handled properly, and the AI's "solution" was to add a special case.&lt;/p&gt;

&lt;p&gt;The real fix was understanding that the null space computation needs to handle rank deficiency properly. When you have more compounds than elements, there's always a non-zero solution. But when you have fewer, you might get a trivial solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance and Edge Cases
&lt;/h2&gt;

&lt;p&gt;The algorithm runs in O(n³) where n is the number of compounds. For typical chemistry equations (up to 10 compounds), this is instantaneous. No performance concerns.&lt;/p&gt;

&lt;p&gt;But there are edge cases that will break any implementation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;H2O = H2O&lt;/code&gt; (already balanced) — should return coefficients of 1&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;C6H12O6 = C6H12O6&lt;/code&gt; (same compound on both sides) — trivial solution&lt;/li&gt;
&lt;li&gt;Equations with fractional coefficients that can't be scaled to integers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last one is interesting. Some equations legitimately have fractional solutions, but for chemistry, we want integers. The AI initially just returned the fractional solution without trying to scale it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Final Architecture
&lt;/h2&gt;

&lt;p&gt;The tool ended up being a single HTML file with three main components:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Parser&lt;/strong&gt;: Converts chemical formulas to element counts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solver&lt;/strong&gt;: Uses Gaussian elimination to find minimal integer coefficients&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Renderer&lt;/strong&gt;: Displays the balanced equation with proper subscripts and a conservation table&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The conservation table was a nice touch — it shows that hydrogen starts with 4 atoms and ends with 4 atoms, so you can visually verify the balancing worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;On AI-assisted development:&lt;/strong&gt; The AI is a powerful pair programmer, but it's not a replacement for understanding the problem. I had to debug the parser logic myself because the AI kept making the same mistake. It's great for generating boilerplate and standard algorithms, but domain-specific logic still requires human oversight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On the math:&lt;/strong&gt; Linear algebra is everywhere in programming. This project was a reminder that understanding the math behind the problem leads to cleaner solutions than brute-force approaches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On chemical formulas:&lt;/strong&gt; Parsing is always harder than it looks. The recursive descent approach handles all the edge cases but requires careful implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;During this process, I built a small browser-based tool to make this workflow easier. It's a single HTML file that handles parsing, balancing, and verification entirely in the browser. No API calls, no server-side processing, just math and JavaScript.&lt;/p&gt;

&lt;p&gt;The tool is part of my collection of browser-based utilities at &lt;a href="https://craftvo.app/en/tool/chemical-equation-balancer" rel="noopener noreferrer"&gt;Craftvo&lt;/a&gt;. If you're working with chemistry or just want to see the implementation, it's all vanilla JavaScript — feel free to peek at the source.&lt;/p&gt;

&lt;p&gt;The next time you need to balance an equation, remember: it's just linear algebra wearing a chemistry costume. And like most things in programming, the parser is the part that'll make you question your career choices.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Tags&lt;/strong&gt;: &lt;code&gt;javascript&lt;/code&gt;, &lt;code&gt;chemistry&lt;/code&gt;, &lt;code&gt;algorithms&lt;/code&gt;, &lt;code&gt;linear-algebra&lt;/code&gt;, &lt;code&gt;webdev&lt;/code&gt;&lt;/p&gt;

</description>
      <category>computerscience</category>
      <category>javascript</category>
      <category>showdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building a Lottery Bet Calculator: A Lesson in Combinatorics and AI-Assisted Development</title>
      <dc:creator>ggwork</dc:creator>
      <pubDate>Thu, 03 Sep 2026 07:58:36 +0000</pubDate>
      <link>https://dev.to/ggwork/building-a-lottery-bet-calculator-a-lesson-in-combinatorics-and-ai-assisted-development-5dhn</link>
      <guid>https://dev.to/ggwork/building-a-lottery-bet-calculator-a-lesson-in-combinatorics-and-ai-assisted-development-5dhn</guid>
      <description>&lt;p&gt;As a developer, I've learned that the strangest requests often lead to the most interesting technical challenges. Last month, a friend asked me a simple question: "How many combinations are there if I pick 7 red balls and 2 blue balls in the lottery?"&lt;/p&gt;

&lt;p&gt;Twenty minutes later, I was deep in a rabbit hole of combinatorial formulas, probability theory, and the surprisingly complex world of lottery betting systems. And because apparently I enjoy reinventing wheels, I decided to build a tool to solve this problem properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem That Started It All
&lt;/h2&gt;

&lt;p&gt;The original question was straightforward: calculate how many bets you'd need to place to cover all combinations of a "复式" (compound) lottery bet. In Chinese lottery systems like Double Color Ball (双色球), you can select more numbers than the base requirement, and the system generates all possible combinations.&lt;/p&gt;

&lt;p&gt;The math is simple combinatorics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For a compound bet selecting 7 red balls from 33, you need C(7,6) = 7 combinations&lt;/li&gt;
&lt;li&gt;Multiply by the number of blue balls you've selected&lt;/li&gt;
&lt;li&gt;Each combination costs ¥2&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But here's where it gets interesting: there's a second betting system called "胆拖" (dan-tuo), where you designate certain numbers as "bold" (胆) that must appear in every combination, and others as "drag" (拖) that fill in the remaining slots.&lt;/p&gt;

&lt;p&gt;This isn't just simple C(n,k) — you need to account for the bold numbers taking up slots. The formula becomes C(dragCount, 6 - boldCount) × blueCount.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not Just Use Excel?
&lt;/h2&gt;

&lt;p&gt;My first instinct was to throw together a quick spreadsheet. But then I thought about the users: lottery enthusiasts who might not be comfortable with Excel formulas, and who need quick answers on their phones while standing in line at the lottery shop.&lt;/p&gt;

&lt;p&gt;I wanted something that works anywhere, requires no installation, and gives instant feedback. A browser-based tool was the obvious choice. Pure HTML, CSS, and JavaScript — no build step, no dependencies, no server.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Math That Almost Broke Me
&lt;/h2&gt;

&lt;p&gt;Here's where the "fun" began. The naive approach to calculating combinations is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;combination&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;factorial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;factorial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;factorial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works fine for small numbers, but C(33,6) involves factorials that quickly exceed JavaScript's safe integer limit. I know, I know — "BigInt exists," but I wanted to keep things simple and fast.&lt;/p&gt;

&lt;p&gt;The solution? Use the multiplicative formula with integer division at each step to avoid overflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;combination&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works because each intermediate result is always an integer. The key insight is that you're multiplying and dividing in a specific order that keeps the numbers manageable. It's a classic combinatorial computing trick that I'd read about but never actually needed until now.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI Collaboration: Where It Helped and Where It Failed
&lt;/h2&gt;

&lt;p&gt;This project was my first serious attempt at AI-assisted development, and it was... educational.&lt;/p&gt;

&lt;h3&gt;
  
  
  What AI Got Right
&lt;/h3&gt;

&lt;p&gt;I described the requirements to Claude in a single, detailed prompt: the four calculation modes, the input validation rules, the output format, and the technical constraints. The AI generated a complete, working HTML file with all the JavaScript logic in one go. The core math was correct, the UI was clean, and it even included dark mode support and i18n scaffolding.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where It Stumbled
&lt;/h3&gt;

&lt;p&gt;The first version had a critical bug in the 胆拖 (dan-tuo) mode. The AI initially calculated the combinations as C(totalSelected, 6), not accounting for the fact that bold numbers are guaranteed to appear. The formula needed to be C(dragCount, 6 - boldCount), which is a subtle but crucial difference.&lt;/p&gt;

&lt;p&gt;I caught this by manually testing edge cases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Test: 4 bold numbers, 3 drag numbers&lt;/span&gt;
&lt;span class="c1"&gt;// Should be C(3, 6-4) = C(3, 2) = 3 combinations&lt;/span&gt;
&lt;span class="c1"&gt;// AI's first attempt: C(7, 6) = 7 combinations (wrong!)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI's second attempt was correct, but it took a specific, detailed prompt to get there: "The bold numbers are always included, so the drag numbers only need to fill 6 - boldCount slots."&lt;/p&gt;

&lt;h3&gt;
  
  
  The Validation Logic
&lt;/h3&gt;

&lt;p&gt;Another issue was input validation. The AI generated reasonable checks but missed some edge cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bold count can't be 0 (that's just a regular compound bet)&lt;/li&gt;
&lt;li&gt;Bold + drag must be at least 7&lt;/li&gt;
&lt;li&gt;Drag count must be at least 1&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I had to add these constraints manually. It's not that the AI couldn't handle it — it just didn't think about all the edge cases without being prompted.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture Decisions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pure Frontend, No Build Step
&lt;/h3&gt;

&lt;p&gt;I deliberately avoided any framework or build tool. The entire tool is a single HTML file with embedded CSS and JavaScript. This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero dependencies&lt;/strong&gt;: No npm install, no CDN links, nothing that could break&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instant loading&lt;/strong&gt;: The file is tiny and loads immediately&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Works offline&lt;/strong&gt;: Once loaded, it doesn't need a network connection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easy to maintain&lt;/strong&gt;: One file, one place to look for everything&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade-off is that I can't use modern JavaScript features like modules or JSX, but for a tool this size, it's not a limitation — it's a feature.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Mode Switching Logic
&lt;/h3&gt;

&lt;p&gt;The UI needs to show different input fields depending on the selected mode. I initially considered having separate forms for each mode, but that would mean duplicating a lot of code. Instead, I used a dynamic input system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CONFIGS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ssq-fu&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;redCount&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blueCount&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;双色球复式&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ssq-dt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;boldCount&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dragCount&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blueCount&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;双色球胆拖&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;d3-g3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;numCount&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;3D组三复式&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;d3-g6&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;numCount&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;3D组六复式&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the user selects a mode, the script dynamically renders the appropriate input fields. This keeps the code DRY and makes it trivial to add new modes in the future.&lt;/p&gt;

&lt;h3&gt;
  
  
  The 3D Lottery Modes
&lt;/h3&gt;

&lt;p&gt;The 3D/排列三 lottery has two compound bet modes that trip people up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;组三 (Group 3)&lt;/strong&gt;: Two numbers are the same, one is different. Formula: k × (k-1)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;组六 (Group 6)&lt;/strong&gt;: All three numbers are different. Formula: C(k, 3)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These formulas aren't obvious to most people, so the tool serves a real educational purpose here. I've had users message me saying "I never understood why 组三 gives more combinations than 组六 for the same numbers" — and the tool makes it concrete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Considerations
&lt;/h2&gt;

&lt;p&gt;For this tool, performance is a non-issue. The heaviest calculation is C(33,6) ≈ 1.1 million, which JavaScript handles in microseconds. But I still made deliberate choices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No floating point arithmetic&lt;/strong&gt;: All calculations use integers to avoid precision issues&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No DOM manipulation during calculation&lt;/strong&gt;: The result is built once and inserted, not updated incrementally&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debounced input handling&lt;/strong&gt;: The calculate button is explicit, not automatic on every keystroke, to avoid unnecessary work&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Edge Cases Are the Real Work
&lt;/h3&gt;

&lt;p&gt;The math formulas are simple. The real work was thinking through all the boundary conditions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What happens when bold count equals 5? (You need exactly 1 drag number)&lt;/li&gt;
&lt;li&gt;What happens when bold count equals 0? (It's a compound bet, not dan-tuo)&lt;/li&gt;
&lt;li&gt;What's the maximum reasonable input? (33 red balls, 16 blue balls for Double Color Ball)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of these came from manual testing, not from the AI or from the spec.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. AI Is a Great Junior Developer
&lt;/h3&gt;

&lt;p&gt;Using AI for this project felt like working with a competent junior developer who's fast but needs supervision. It:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Writes clean, well-structured code quickly&lt;/li&gt;
&lt;li&gt;Makes subtle logic errors that are easy to miss&lt;/li&gt;
&lt;li&gt;Needs specific, detailed instructions to handle edge cases&lt;/li&gt;
&lt;li&gt;Excels at boilerplate and repetitive patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key is knowing when to let it run and when to step in. For the core math logic, I reviewed every line. For the CSS and HTML structure, I let it do its thing.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Sometimes the Simple Solution Is the Best
&lt;/h3&gt;

&lt;p&gt;I could have made this a React app with TypeScript, testing, and CI/CD. But the simplest solution — a single HTML file — is actually the most robust. It can't have dependency issues, it can't fail to build, and it works on any device with a browser.&lt;/p&gt;

&lt;p&gt;During this process, I built a small browser-based tool to make this workflow easier. You can find it at &lt;a href="https://craftvo.app/en/tool/lottery-bet-calc" rel="noopener noreferrer"&gt;craftvo.app&lt;/a&gt; if you're curious about the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;Building this tool taught me that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Combinatorics in JavaScript requires careful thought&lt;/strong&gt; — the naive factorial approach fails for real-world inputs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI assistance is most effective when you understand the problem domain&lt;/strong&gt; — I caught the dan-tuo bug because I understood the math, not because the AI told me&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small tools deserve the same engineering rigor&lt;/strong&gt; — just because it's a single file doesn't mean it shouldn't be well-architected&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And yes, my friend got his answer: 7 red balls and 2 blue balls gives you 14 combinations, costing ¥28. The tool is there if he ever needs to calculate something more complex.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Meta Description&lt;/strong&gt;: Building a lottery bet calculator taught me about combinatorial math, JavaScript integer precision, and the realities of AI-assisted development. Here's what I learned about working with AI on real engineering problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tags&lt;/strong&gt;: javascript, webdev, ai, productivity, mathematics&lt;/p&gt;

</description>
      <category>ai</category>
      <category>algorithms</category>
      <category>programming</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>How to Minify and Beautify HTML, JS, and CSS in the Browser Without a Backend</title>
      <dc:creator>ggwork</dc:creator>
      <pubDate>Wed, 02 Sep 2026 01:17:04 +0000</pubDate>
      <link>https://dev.to/ggwork/how-to-minify-and-beautify-html-js-and-css-in-the-browser-without-a-backend-1hpg</link>
      <guid>https://dev.to/ggwork/how-to-minify-and-beautify-html-js-and-css-in-the-browser-without-a-backend-1hpg</guid>
      <description>&lt;p&gt;While working on a project recently, I needed to clean up some minified JavaScript that was basically unreadable. The code worked fine, but trying to debug it was like reading hieroglyphics without the Rosetta Stone. I opened my browser's dev tools, copied the code, and started searching for a quick way to format it.&lt;/p&gt;

&lt;p&gt;The first thing I found was a bunch of online tools. Some were great, but they all had the same problem: I'd paste my code into a random website and hope they weren't logging my snippets. For work projects, that's a non-starter. For personal projects, it's still uncomfortable.&lt;/p&gt;

&lt;p&gt;I wanted a simple solution without relying on external services. Something that runs entirely in the browser, works offline, and doesn't send my code anywhere. Because apparently I enjoy reinventing wheels.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Just Use a Library" Approach
&lt;/h2&gt;

&lt;p&gt;Before building anything, I considered my options. Writing a full HTML/JS/CSS parser from scratch? Absolutely not. That's a rabbit hole of edge cases that would consume weeks.&lt;/p&gt;

&lt;p&gt;The pragmatic choice was &lt;code&gt;js-beautify&lt;/code&gt;, a battle-tested library that handles all three languages. It's the same engine behind many popular online formatters, so I knew it handled edge cases I'd never think about.&lt;/p&gt;

&lt;p&gt;The harder question was minification. &lt;code&gt;js-beautify&lt;/code&gt; handles formatting, but for minification I had options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Terser&lt;/strong&gt; - Excellent for JS, but doesn't handle HTML or CSS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean-css&lt;/strong&gt; - Great for CSS, but nothing else&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;html-minifier-terser&lt;/strong&gt; - Handles HTML but pulls in a lot of dependencies&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each library would need its own integration and error handling. That's three different APIs to learn and maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trade-off That Sounded Wrong but Worked
&lt;/h2&gt;

&lt;p&gt;Here's where I made a decision that initially felt wrong: I used &lt;code&gt;js-beautify&lt;/code&gt; for both beautification AND minification.&lt;/p&gt;

&lt;p&gt;Wait, what? &lt;code&gt;js-beautify&lt;/code&gt; doesn't minify. You're right. But here's the thing — for a browser-based tool, the minification doesn't need to be production-grade. It needs to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Remove comments&lt;/li&gt;
&lt;li&gt;Collapse whitespace&lt;/li&gt;
&lt;li&gt;Remove unnecessary newlines&lt;/li&gt;
&lt;li&gt;Keep the code valid&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most use cases — cleaning up copied snippets, reducing file size for quick sharing, making code more compact — that's enough. If someone needs aggressive minification with dead-code elimination, they should use a proper build tool anyway.&lt;/p&gt;

&lt;p&gt;The trade-off was clear: &lt;code&gt;js-beautify&lt;/code&gt; gives me one consistent API for all three languages, and I can control the output through its options. The minification won't beat Terser's compression ratio, but it's reliable and simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up the Core Logic
&lt;/h2&gt;

&lt;p&gt;The heart of the tool is surprisingly small. Here's the essence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;beautifyCode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lang&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;indent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;indent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;indent_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;indent&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;indent&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;indent_char&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;indent&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;html_beautify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;js_beautify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;css_beautify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;output&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;updateStats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;showStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Failed to format code. Check for syntax errors.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For minification, I configure the same library with different options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;minifyCode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lang&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;indent_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;indent_char&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;preserve_newlines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;max_preserve_newlines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;wrap_line_length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Remove comments before minifying&lt;/span&gt;
    &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jslint_happy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;space_after_anon_function&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;html_beautify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;indent_inner_html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;js_beautify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;css_beautify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;selector_separator_newline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;output&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;updateStats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;showStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Failed to minify code.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trick is realizing that minification and beautification are the same operation with different settings. Set &lt;code&gt;preserve_newlines&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt; and &lt;code&gt;wrap_line_length&lt;/code&gt; to &lt;code&gt;0&lt;/code&gt;, and the beautifier produces compact output. Not perfect minification, but solid enough for a browser tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  The CDN Problem I Didn't Expect
&lt;/h2&gt;

&lt;p&gt;The first version loaded &lt;code&gt;js-beautify&lt;/code&gt; from jsdelivr. It worked perfectly in my testing. Then I deployed it and waited for feedback.&lt;/p&gt;

&lt;p&gt;A few days later, I got a message from a user in China: "The tool doesn't work. The button does nothing."&lt;/p&gt;

&lt;p&gt;I opened the console and saw it immediately: &lt;code&gt;ReferenceError: html_beautify is not defined&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The CDN was blocked. jsdelivr is frequently blocked or slow in mainland China, and my user couldn't load the library at all. The page rendered fine, but every button silently failed.&lt;/p&gt;

&lt;p&gt;This is the kind of bug that's easy to miss if you're only testing from one region. The fix was two-fold:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Switch to a CDN that's more reliable in China (&lt;code&gt;staticfile.org&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Add a fallback that shows a clear error message if the library fails to load
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.staticfile.org/js-beautify/1.15.1/beautifier.min.js"&lt;/span&gt; 
        &lt;span class="na"&gt;onerror=&lt;/span&gt;&lt;span class="s"&gt;"window.__beautifierFailed=true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in the click handlers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkLib&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__beautifierFailed&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;html_beautify&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;showStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Code formatter library failed to load. Check your internet connection.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spoiler: it was never a CSS issue this time. It was a CDN availability issue. Classic "works on my machine" situation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling the Edge Cases
&lt;/h2&gt;

&lt;p&gt;With the core logic working, I started testing edge cases. Here's what broke:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Empty input&lt;/strong&gt;: Clicking "Beautify" with nothing in the textarea should do nothing, not throw an error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Invalid syntax&lt;/strong&gt;: Paste in broken code and &lt;code&gt;js-beautify&lt;/code&gt; sometimes throws, sometimes produces garbage. I needed to catch errors and show a friendly message.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Very large files&lt;/strong&gt;: A 2MB minified file can freeze the browser. I added a size check and a warning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Special characters&lt;/strong&gt;: The library handles these fine, but my statistics calculation needed to use byte counts, not character counts. Chinese characters in comments or strings would throw off the size calculation.&lt;/p&gt;

&lt;p&gt;The statistics display was a nice touch that users appreciated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateStats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;outBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ratio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inBytes&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;outBytes&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;inBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stats&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 
    &lt;span class="s2"&gt;`Original: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;formatSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inBytes&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; → Result: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;formatSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outBytes&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ratio&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%)`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;new Blob([input]).size&lt;/code&gt; gives me the exact byte count, which handles Unicode correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building This With AI Assistance
&lt;/h2&gt;

&lt;p&gt;I used AI assistance throughout this project, and it was a mixed bag. Here's the honest breakdown.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What AI handled well:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The initial structure and CSS styling. I described the layout I wanted, and it generated a clean, responsive design with dark mode support in minutes.&lt;/li&gt;
&lt;li&gt;The i18n setup. I asked for a lightweight translation system, and it produced a clean &lt;code&gt;t()&lt;/code&gt; function with &lt;code&gt;data-i18n&lt;/code&gt; attributes.&lt;/li&gt;
&lt;li&gt;The statistics calculation. It got the &lt;code&gt;Blob&lt;/code&gt; approach right on the first try.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What AI got wrong:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first version of the minify function was a mess. Here's what it generated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// AI's first attempt (wrong)&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;minifyCode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// ... setup ...&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// This breaks strings, regexes, and template literals!&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This naive whitespace removal would destroy JavaScript. It would break template literals, regex patterns, and string concatenation. I had to explain that minification requires understanding the language structure, not just regex replacement.&lt;/p&gt;

&lt;p&gt;The AI also initially ignored the CDN fallback issue entirely. I had to explicitly ask for error handling around the library loading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where I had to step in:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The CDN fallback logic (AI didn't consider regional availability)&lt;/li&gt;
&lt;li&gt;The error handling for invalid input (AI assumed valid input)&lt;/li&gt;
&lt;li&gt;The performance considerations for large files (AI didn't think about it)&lt;/li&gt;
&lt;li&gt;The minification strategy (AI suggested using regex, which was wrong)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Browser-only tools have a real place.&lt;/strong&gt; For quick formatting tasks, sending code to a server is overkill and a privacy concern. A client-side tool is instant, private, and works offline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. "Good enough" is often the right call.&lt;/strong&gt; My minification isn't as aggressive as a dedicated minifier, but it handles 95% of use cases with one library instead of three.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. CDN choice matters more than you think.&lt;/strong&gt; If you're building a global tool, test from multiple regions. A library that works in the US might be completely inaccessible elsewhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. AI is a great pair programmer, not a replacement.&lt;/strong&gt; It handled the boilerplate and styling perfectly. But it didn't understand the domain-specific pitfalls of code manipulation. I had to know enough to catch its mistakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;During this process, I built a small browser-based tool to make this workflow easier. It's a single HTML file that handles HTML, JavaScript, and CSS formatting in both directions — minify and beautify — with customizable indentation, file statistics, and a clean interface. Everything runs locally in your browser, so your code never leaves your machine.&lt;/p&gt;

&lt;p&gt;If you're dealing with unreadable minified code or want to compress your CSS before deployment, it might save you a few minutes. You can find it at &lt;a href="https://craftvo.app/en/tool/code-formatter" rel="noopener noreferrer"&gt;craftvo.app&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The next time you're staring at a wall of minified JavaScript, remember: you don't need to send your code to a random server just to make it readable. A few lines of JavaScript and a good library can do it right in your browser. And if you're building something similar, don't forget to test your CDN from more than one continent.&lt;/p&gt;

</description>
      <category>css</category>
      <category>html</category>
      <category>javascript</category>
      <category>tools</category>
    </item>
    <item>
      <title>Building a Position Averaging Calculator: Getting the Math Right Matters</title>
      <dc:creator>ggwork</dc:creator>
      <pubDate>Tue, 01 Sep 2026 01:37:42 +0000</pubDate>
      <link>https://dev.to/ggwork/building-a-position-averaging-calculator-getting-the-math-right-matters-2694</link>
      <guid>https://dev.to/ggwork/building-a-position-averaging-calculator-getting-the-math-right-matters-2694</guid>
      <description>&lt;p&gt;Last week, I found myself staring at a spreadsheet trying to figure out my average cost after buying the same stock three times at different prices. You know the drill — you buy at 10, it drops, you buy more at 8, then it dips again and you're thinking about averaging down further. The math isn't hard, but it's tedious, and I kept second-guessing whether I was accounting for fees correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Existing Solutions
&lt;/h2&gt;

&lt;p&gt;I searched for online calculators, and honestly, most of them were disappointing. Either they required sign-up, were buried in financial portal websites with more ads than functionality, or they ignored trading fees entirely. A few had the fee calculation, but they made assumptions that didn't match how my broker actually charged me.&lt;/p&gt;

&lt;p&gt;The real issue? &lt;strong&gt;Most calculators ignore the fact that your break-even price isn't just your average cost.&lt;/strong&gt; When you sell, you pay fees too. So if your average cost is 9.0051, you can't actually break even by selling at 9.0051 — you need to sell slightly higher to cover the sell-side fees.&lt;/p&gt;

&lt;p&gt;That's the gap I wanted to fill.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Math
&lt;/h2&gt;

&lt;p&gt;The calculation itself is straightforward weighted averaging, but the fee handling is where things get interesting. Here's the essential logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentRaw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;commRate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stampRate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;transferRate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;minComm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sym&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;totalShares&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;totalCost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;shares&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;buys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;amt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;shares&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fee&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;amt&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;commRate&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;transferRate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;actualFee&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;minComm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;totalCost&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;amt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;actualFee&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;totalShares&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;shares&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;avgCost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;totalCost&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;totalShares&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// Break-even includes sell-side fees&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sellFeePerShare&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;avgCost&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;commRate&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;stampRate&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;transferRate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;breakEven&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;avgCost&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;sellFeePerShare&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;totalShares&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;totalCost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;avgCost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;breakEven&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight here is the break-even calculation. Most tools stop at &lt;code&gt;avgCost&lt;/code&gt;, but that's misleading. If you sell at your average cost, you'll actually lose money because of sell-side fees. The break-even price needs to be slightly higher — it's the price where your sell proceeds (after fees) exactly equal your total buy cost (including buy fees).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Chose Pure Frontend
&lt;/h2&gt;

&lt;p&gt;I could have built this with a backend. I could have added user accounts, saved calculation history, all that stuff. But honestly, this is a privacy-sensitive calculation. Do you really want your stock positions floating around on some server?&lt;/p&gt;

&lt;p&gt;Pure frontend means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No data leaves the browser&lt;/li&gt;
&lt;li&gt;No server costs for me&lt;/li&gt;
&lt;li&gt;Instant load times&lt;/li&gt;
&lt;li&gt;Works offline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade-off? No persistent history, no cross-device sync. For this use case, that's a trade-off I'm comfortable with. If you need to save results, just screenshot it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI-Assisted Development Experience
&lt;/h2&gt;

&lt;p&gt;This is where it gets interesting. I built this with heavy AI assistance, and the experience was... educational.&lt;/p&gt;

&lt;h3&gt;
  
  
  What AI Got Right
&lt;/h3&gt;

&lt;p&gt;I described the requirements conversationally: "I need a calculator that takes multiple buy records with price and shares, calculates weighted average cost including fees, and shows break-even price." The AI generated a solid initial structure — the HTML layout, the dynamic row addition, the basic styling. It even handled the dark mode CSS without me asking.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where AI Struggled
&lt;/h3&gt;

&lt;p&gt;The first version had a subtle bug in the fee calculation. It was applying the minimum commission to every trade individually, which is correct, but it was also applying the minimum to the &lt;em&gt;sell&lt;/em&gt; fee estimation in the break-even calculation. That's wrong — the minimum commission should only apply if the trade actually happens.&lt;/p&gt;

&lt;p&gt;Here's the conversation that followed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Me: The break-even price is too high. If I buy 100 shares at 10 with a 5 yuan minimum commission, the break-even shouldn't include the minimum commission because the sell fee is proportional to the sell amount.

AI: You're right. Let me fix the break-even calculation to use the percentage rate only, not the minimum.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the pattern I've noticed with AI coding — it's great at generating the 80% solution, but the edge cases and financial logic accuracy require human domain knowledge. The AI doesn't "know" that minimum commissions don't apply proportionally; it just patterns from typical code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Another AI Miss: Input Validation
&lt;/h3&gt;

&lt;p&gt;The AI initially assumed all inputs would be valid numbers. It took me pointing out that someone might type "abc" or leave a field empty before it added proper validation. And even then, the first validation attempt was too aggressive — it blocked legitimate edge cases like zero shares (which should be an error, but the error message was confusing).&lt;/p&gt;

&lt;h2&gt;
  
  
  The Refinement Process
&lt;/h2&gt;

&lt;p&gt;After the initial AI-generated version, I went through several iterations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fixed the fee calculation&lt;/strong&gt; — the minimum commission logic needed adjustment for the break-even price&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Added input validation&lt;/strong&gt; — with clear error messages in Chinese (the tool targets Chinese-speaking users)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improved the UX&lt;/strong&gt; — the first version had all inputs on one line, which was cramped on mobile. I restructured to a grid layout that stacks on small screens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Added the optional current price field&lt;/strong&gt; — this lets users see floating profit/loss without needing a separate calculator&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The floating P&amp;amp;L calculation was another spot where I had to correct the AI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// AI's first version — wrong&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;floatPL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentPrice&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;totalShares&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;totalCost&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Correct version — includes sell fees&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sellFee&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentPrice&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;totalShares&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;commRate&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;stampRate&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;transferRate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;floatPL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentPrice&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;totalShares&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;sellFee&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;totalCost&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference matters. If you're showing someone their unrealized profit, it should reflect what they'd actually get if they sold right now, not a naive price-minus-cost calculation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Domain Knowledge Still Matters
&lt;/h3&gt;

&lt;p&gt;AI can write code, but it can't know that in Chinese stock markets, there's a stamp tax on sells (0.05%) that doesn't apply to buys, or that some brokers have a 5 yuan minimum commission. These domain-specific rules are where human input becomes critical.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Test with Real Numbers
&lt;/h3&gt;

&lt;p&gt;The AI generated test cases, but they were too clean. I had to create edge cases myself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What if the user enters a price of 0?&lt;/li&gt;
&lt;li&gt;What if shares are negative?&lt;/li&gt;
&lt;li&gt;What if the commission rate is 0 but the minimum is 5?&lt;/li&gt;
&lt;li&gt;What about very small trades where the minimum commission dominates?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These edge cases exposed bugs that would have shipped otherwise.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. AI Is Great for UI, Less Great for Logic
&lt;/h3&gt;

&lt;p&gt;For this project, the AI's HTML/CSS was immediately usable. The JavaScript logic needed multiple rounds of correction. I've found this pattern holds across projects — AI excels at structure and boilerplate, but business logic requires careful human review.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;After several hours of iteration, I had a working tool that handles the edge cases I care about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple buy records with dynamic add/remove&lt;/li&gt;
&lt;li&gt;Commission, stamp tax, and transfer fees included in cost&lt;/li&gt;
&lt;li&gt;Break-even price that accounts for sell-side fees&lt;/li&gt;
&lt;li&gt;Optional current price for floating P&amp;amp;L&lt;/li&gt;
&lt;li&gt;Dark mode (because I'm a developer, of course I added dark mode)&lt;/li&gt;
&lt;li&gt;Pure frontend — no data leaves the browser&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The calculation runs in milliseconds, the UI is responsive, and the whole thing is a single HTML file. No build step, no dependencies, no server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts on AI-Assisted Development
&lt;/h2&gt;

&lt;p&gt;This project reinforced my belief that AI coding tools are incredibly useful, but they're not a replacement for understanding the problem domain. The AI saved me maybe 60% of the time compared to writing everything from scratch, but the remaining 40% required me to actually understand what the tool needed to do.&lt;/p&gt;

&lt;p&gt;The pattern I've settled into:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Describe the problem clearly to the AI&lt;/strong&gt; — the more specific, the better&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review all generated logic carefully&lt;/strong&gt; — especially anything involving money or dates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create edge case tests&lt;/strong&gt; — the AI won't do this for you&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iterate with targeted prompts&lt;/strong&gt; — "fix the fee calculation" works better than "something's wrong"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're building a similar tool, whether it's a calculator or something else, the same principles apply. AI gets you 80% there quickly, but the last 20% is where the real engineering happens.&lt;/p&gt;

&lt;p&gt;During this process, I built a small browser-based tool to make this workflow easier. It's free to use, handles all the fee calculations correctly (I hope), and respects your privacy since everything runs locally in your browser. If you're curious, you can find it &lt;a href="https://craftvo.app/en/tool/stock-average-cost" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now if you'll excuse me, I need to go check whether my actual average cost matches what the calculator says. Spoiler: it probably won't, because my broker's fee structure is apparently a mystery even to their own support team.&lt;/p&gt;

</description>
      <category>fintech</category>
      <category>productivity</category>
      <category>tools</category>
    </item>
    <item>
      <title>How to Calculate Exact Age in the Browser Without Cheating with Math</title>
      <dc:creator>ggwork</dc:creator>
      <pubDate>Mon, 31 Aug 2026 09:41:34 +0000</pubDate>
      <link>https://dev.to/ggwork/how-to-calculate-exact-age-in-the-browser-without-cheating-with-math-5225</link>
      <guid>https://dev.to/ggwork/how-to-calculate-exact-age-in-the-browser-without-cheating-with-math-5225</guid>
      <description>&lt;p&gt;While working on a utility site recently, I needed a precise age calculator. Sounds trivial, right? Pick two dates, subtract, done. Except it's not. The moment you try to calculate "25 years, 3 months, and 12 days" accurately, you realize that naive date arithmetic is a rabbit hole of edge cases.&lt;/p&gt;

&lt;p&gt;Let me walk you through the engineering decisions that turned a seemingly simple tool into a proper date-handling exercise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Simple Subtraction
&lt;/h2&gt;

&lt;p&gt;Here's the trap I initially fell into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ageInMilliseconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;birthDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ageInYears&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ageInMilliseconds&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;365.25&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works... until it doesn't. The 365.25 days approximation drifts. Leap years aren't uniform. And try explaining to a user born on February 29th why their age is "wrong" on non-leap years.&lt;/p&gt;

&lt;p&gt;The real issue is that calendars aren't arithmetic — they're cultural constructs with rules. Months have different lengths. Leap years follow a specific pattern. You can't just divide milliseconds by a constant and get a meaningful answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Correct" Way: Component-wise Calculation
&lt;/h2&gt;

&lt;p&gt;The key insight is to calculate years, months, and days separately, adjusting for borrows like you learned in elementary school subtraction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calculateAge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;birthDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetDate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;years&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;targetDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFullYear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;birthDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFullYear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;months&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;targetDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMonth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;birthDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMonth&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;days&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;targetDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;birthDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;days&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;months&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;days&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;daysInMonth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFullYear&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;targetDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMonth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;months&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;years&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;months&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;years&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;months&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;days&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the part that matters — the borrow logic. When your birthday hasn't happened yet this month, you borrow days from the previous month. When it's not your birthday month yet, you borrow months from the previous year. Simple, elegant, and handles all the calendar weirdness correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Birthday Problem
&lt;/h2&gt;

&lt;p&gt;The next challenge was calculating days until the next birthday. The naive approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextBirthday&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFullYear&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;birthDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMonth&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;birthDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDate&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This breaks for people born on February 29th. What's their birthday in non-leap years? March 1st? February 28th?&lt;/p&gt;

&lt;p&gt;The pragmatic answer: check if the birthday exists in the current year; if not, use March 1st. It's not perfect, but it's predictable and defensible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getBirthdayInYear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;birthMonth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;birthDay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;birthMonth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;birthDay&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMonth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;birthMonth&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// March 1st&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Real-Time Seconds Problem
&lt;/h2&gt;

&lt;p&gt;Here's where things get interesting. I wanted to show total seconds alive, and it needed to tick in real-time. The naive approach would recalculate everything every second. That's wasteful and causes jank.&lt;/p&gt;

&lt;p&gt;The solution: calculate the static values once, then only update the seconds display:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;totalSeconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;initialSeconds&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;totalSeconds&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;totalSeconds&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;formatNumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;totalSeconds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a micro-optimization, but it's the kind of thinking that prevents your tool from feeling sluggish. The rest of the calculation happens once; only the seconds counter updates.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI Collaboration Experience
&lt;/h2&gt;

&lt;p&gt;Now for the part that made this project actually fun — building it with AI assistance. I've been experimenting with using AI as a pair programmer for utility tools, and this was a perfect test case.&lt;/p&gt;

&lt;h3&gt;
  
  
  The First Attempt
&lt;/h3&gt;

&lt;p&gt;I gave the AI my requirements: "Create a single-file HTML age calculator with precise year/month/day calculation, total days/hours/minutes, and next birthday countdown."&lt;/p&gt;

&lt;p&gt;The AI's first attempt was... surprisingly good. It nailed the structure, the styling, and the basic logic. But it had one critical bug: it used the millisecond division approach for age calculation. The output looked right for most dates, but it was subtly wrong for edge cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Iteration Process
&lt;/h3&gt;

&lt;p&gt;This is where the real collaboration began. Instead of pointing out the bug directly, I asked:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What happens for someone born on February 29th, 2000, calculating age on March 1st, 2024?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The AI paused, recalculated, and immediately recognized the issue. It suggested the component-wise approach. But its first implementation had a bug in the borrow logic — it didn't handle the case where the target date's month has fewer days than the birth month's day.&lt;/p&gt;

&lt;p&gt;I had to guide it through the borrow logic step by step. The AI was great at generating code quickly, but it struggled with the edge case reasoning that comes naturally to experienced developers. It would optimize for the common case and miss the exceptions.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Worked Well
&lt;/h3&gt;

&lt;p&gt;The AI excelled at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generating the complete HTML structure with proper i18n support&lt;/li&gt;
&lt;li&gt;Creating responsive CSS with dark mode support&lt;/li&gt;
&lt;li&gt;Writing the structured data for SEO&lt;/li&gt;
&lt;li&gt;Implementing the real-time seconds counter&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Where I Had to Step In
&lt;/h3&gt;

&lt;p&gt;The AI struggled with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Calendar edge cases (February 29th, month-end boundaries)&lt;/li&gt;
&lt;li&gt;The borrow logic in date arithmetic&lt;/li&gt;
&lt;li&gt;Understanding that not all dates are valid in all years&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The i18n Trap
&lt;/h2&gt;

&lt;p&gt;One thing the AI got right from the start: proper internationalization. It automatically used a key-value system with &lt;code&gt;data-i18n&lt;/code&gt; attributes and a JavaScript dictionary. This was smart because it anticipated the need for multiple languages without over-engineering.&lt;/p&gt;

&lt;p&gt;The language detection logic is simple but effective:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lang&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
  &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zh&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;URL parameter takes priority, then browser language, then default to Chinese. This pattern is simple enough to be maintainable but powerful enough for edge cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Calendar math is deceptively complex.&lt;/strong&gt; What looks like a simple subtraction problem has more edge cases than you'd expect. Always test with boundary dates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. AI is a great pair programmer, but not a replacement for understanding.&lt;/strong&gt; The AI generated code quickly, but I had to understand the domain deeply to guide it correctly. It's like having a brilliant intern who's fast but needs supervision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Real-time features need incremental updates.&lt;/strong&gt; Don't recalculate everything when you only need to update one value. Think about what actually needs to change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Internationalization should be baked in from the start.&lt;/strong&gt; Retrofitting i18n is painful. Designing for it from day one is barely any extra work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;I ended up with a clean, single-file tool that handles all the date edge cases correctly. It's fast, responsive, and works in both Chinese and English. The whole thing is about 300 lines of HTML/CSS/JS — no frameworks, no dependencies.&lt;/p&gt;

&lt;p&gt;During this process, I built a small browser-based tool to make this workflow easier. You can find it at &lt;a href="https://craftvo.app/en/tool/age-calculator" rel="noopener noreferrer"&gt;craftvo.app&lt;/a&gt; if you want to see the final result.&lt;/p&gt;

&lt;p&gt;The real takeaway isn't about age calculators specifically. It's about the mindset: don't trust naive arithmetic when dealing with human constructs like calendars. And when using AI assistance, remember that it's a tool for accelerating your thinking, not replacing it. The best results come from understanding the problem deeply and using AI to execute your understanding faster.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Building an ASCII Table Tool: Why I Ditched the Spreadsheet and Made My Own</title>
      <dc:creator>ggwork</dc:creator>
      <pubDate>Mon, 31 Aug 2026 01:57:27 +0000</pubDate>
      <link>https://dev.to/ggwork/building-an-ascii-table-tool-why-i-ditched-the-spreadsheet-and-made-my-own-426f</link>
      <guid>https://dev.to/ggwork/building-an-ascii-table-tool-why-i-ditched-the-spreadsheet-and-made-my-own-426f</guid>
      <description>&lt;p&gt;I was debugging a network protocol implementation last month when I hit a wall. I needed to quickly verify what byte value corresponded to a specific control character — was it &lt;code&gt;0x1B&lt;/code&gt; for ESC or was that &lt;code&gt;0x1C&lt;/code&gt;? My go-to approach was to open a random ASCII chart website, but every single one I found was either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Buried in ads and popups&lt;/li&gt;
&lt;li&gt;Missing the control characters entirely (you know, the actually useful ones)&lt;/li&gt;
&lt;li&gt;So cluttered with unnecessary features that the table was half the screen&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I did what any reasonable developer would do: I spent an afternoon building my own. Because apparently I enjoy reinventing wheels.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Initial Approach: Data First, UI Later
&lt;/h2&gt;

&lt;p&gt;The first decision was the data structure. I needed all 128 standard ASCII characters (0-127), each with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Decimal, hex, octal, and binary representations&lt;/li&gt;
&lt;li&gt;HTML entity (for the printable ones)&lt;/li&gt;
&lt;li&gt;Standard name (like "Capital A" or "Line Feed")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I started by hardcoding an array of objects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ASCII_CHARS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NUL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;control&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Capital A&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;char&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ... 126 more entries&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was tedious but straightforward. The real challenge came with control characters — you can't just display &lt;code&gt;\x00&lt;/code&gt; and expect users to know what it means. I needed proper names: NUL, SOH, STX, and so on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI Collaboration: Where It Helped and Where It Didn't
&lt;/h2&gt;

&lt;p&gt;This is where AI-assisted development got interesting. I described the requirements to Claude — "I need an ASCII table with all 128 characters, multi-format display, search, and copy functionality" — and it generated a solid foundation on the first try.&lt;/p&gt;

&lt;p&gt;The data generation was where AI really shined. I asked it to generate the complete character metadata, and it produced accurate names, hex codes, and HTML entities. This saved me probably 30 minutes of manual data entry and potential typos.&lt;/p&gt;

&lt;p&gt;But here's where it went wrong: the AI's first version had a subtle bug. It was using &lt;code&gt;String.fromCharCode()&lt;/code&gt; directly to display characters, which works fine for printable characters but breaks for control characters — they render as invisible or garbled. The AI didn't catch this because it never actually rendered the table.&lt;/p&gt;

&lt;p&gt;I had to step in and add explicit handling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;formatChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dec&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;dec&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;127&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;␀&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Use visual placeholder&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromCharCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dec&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the difference between "code that compiles" and "code that actually works." The AI was great at generating the skeleton, but it missed the edge cases that only become obvious when you're looking at a rendered page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance: The Sticky Header Problem
&lt;/h2&gt;

&lt;p&gt;One thing I insisted on was a sticky table header — when you're scrolling through 128 rows, you need to see what column you're in. This turned out to be surprisingly tricky.&lt;/p&gt;

&lt;p&gt;The naive approach is &lt;code&gt;position: sticky; top: 0&lt;/code&gt; on the &lt;code&gt;th&lt;/code&gt; elements. But this breaks when the table is inside a scrollable container with a border. The header would scroll away or get clipped by the container's overflow.&lt;/p&gt;

&lt;p&gt;The fix was to ensure the scroll container is a direct parent with proper overflow settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.table-wrap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;max-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;600px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;th&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sticky&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;z-index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple in hindsight, but it took me a few iterations to get the z-index right — otherwise the sticky header would appear under the table rows when scrolling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search: The Unexpected Complexity
&lt;/h2&gt;

&lt;p&gt;Search seemed straightforward: filter by decimal, hex, or name. But I quickly realized that users think in different formats. Some search for "65", others for "0x41", others for "A", and others for "Capital A".&lt;/p&gt;

&lt;p&gt;The solution was to normalize the input and search across multiple fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;filterRows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;allRows&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;allRows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 
    &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0x&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI initially generated a search that only matched exact decimal values — completely missing the use case where someone types "A" or "0x41". I had to iterate on the prompt: "Search should be fuzzy, matching partial strings across multiple fields."&lt;/p&gt;

&lt;h2&gt;
  
  
  The Copy Feature: Clipboard API Gotchas
&lt;/h2&gt;

&lt;p&gt;Click-to-copy sounds simple, but there's a catch: the Clipboard API only works in secure contexts (HTTPS or localhost). Since this is a static tool, I need to handle the fallback for HTTP environments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;copyHex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clipboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;showStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Copied: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Fallback for older browsers&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;textarea&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;textarea&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;textarea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;textarea&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;textarea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;copy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;textarea&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI didn't consider this edge case at all — it just used &lt;code&gt;navigator.clipboard&lt;/code&gt; without any error handling. This would have silently failed for users on HTTP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mobile Responsiveness: The Table That Wouldn't Fit
&lt;/h2&gt;

&lt;p&gt;Tables are inherently non-responsive, and ASCII tables are no exception. With 7 columns (DEC, HEX, OCT, BIN, CHAR, NAME, HTML), it's impossible to fit everything on a 320px screen.&lt;/p&gt;

&lt;p&gt;My solution was progressive disclosure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;600px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.hide-sm&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This hides the HTML entity column on small screens. Not perfect, but functional. The table becomes scrollable horizontally if needed, but at least the essential columns are visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AI Got Right (and What It Didn't)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;AI nailed:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generating the complete character dataset with zero typos&lt;/li&gt;
&lt;li&gt;Setting up the basic table structure and i18n scaffolding&lt;/li&gt;
&lt;li&gt;Producing a clean, consistent CSS foundation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;AI struggled with:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Edge cases (control characters, clipboard fallbacks)&lt;/li&gt;
&lt;li&gt;Understanding the actual user experience (search that works the way people think)&lt;/li&gt;
&lt;li&gt;Performance considerations (sticky headers, DOM rendering at scale)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern I've noticed: AI is excellent at generating the "happy path" but consistently misses the "sad path" — the error handling, edge cases, and browser quirks that make production code actually production-ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  The i18n Decision
&lt;/h2&gt;

&lt;p&gt;Since I'm building this for a global audience, I needed both Chinese and English support. The lightweight approach was a simple dictionary-based system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;I18N&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;zh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ASCII 表&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;searchPlaceholder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;搜索...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ASCII Table&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;searchPlaceholder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Search...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is intentional — I didn't want to pull in a full i18n library for a single tool. The trade-off is that adding a new language requires duplicating all strings, but for a tool with maybe 20 user-facing strings, it's manageable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AI is a great pair programmer, not a replacement.&lt;/strong&gt; It saved me time on boilerplate and data generation, but I still needed to understand the domain well enough to catch its mistakes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Edge cases are where the real work lives.&lt;/strong&gt; Any developer can render an ASCII table. Making it actually useful — with proper control character handling, intuitive search, and graceful fallbacks — is the difference between a toy and a tool.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sometimes the old ways are fine.&lt;/strong&gt; I briefly considered using a framework like React or Vue, but for a static reference tool, vanilla JS was simpler, faster to load, and had zero dependencies to maintain.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Performance matters even for simple tools.&lt;/strong&gt; The sticky header and efficient filtering make the tool feel instant. Users notice when a "simple" tool is sluggish.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;During this process, I built a small browser-based tool to make this workflow easier. It's a single HTML file with no dependencies — just open it in a browser and it works. The complete ASCII table (0-127) with multi-format display, fuzzy search, category filtering, and click-to-copy functionality.&lt;/p&gt;

&lt;p&gt;If you find yourself constantly opening random ASCII chart websites that are cluttered with ads, or if you just want a clean, fast reference tool, you might find it useful. &lt;a href="https://craftvo.app/en/tool/ascii-table" rel="noopener noreferrer"&gt;Try it here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The irony isn't lost on me — I spent four hours building a tool that saves me maybe 30 seconds each time I need to look up a character code. But those 30 seconds add up, and now I have a tool that works exactly the way I want it to. Sometimes reinventing the wheel is worth it.&lt;/p&gt;

</description>
      <category>coding</category>
      <category>programming</category>
      <category>tools</category>
    </item>
    <item>
      <title>How I Built a Compound Interest Calculator Without Any Backend</title>
      <dc:creator>ggwork</dc:creator>
      <pubDate>Sun, 30 Aug 2026 02:20:57 +0000</pubDate>
      <link>https://dev.to/ggwork/how-i-built-a-compound-interest-calculator-without-any-backend-2j2c</link>
      <guid>https://dev.to/ggwork/how-i-built-a-compound-interest-calculator-without-any-backend-2j2c</guid>
      <description>&lt;p&gt;While working on a side project recently, I needed a way to help users quickly understand how their savings could grow over time. Nothing fancy — just input a principal, an interest rate, and a timeframe, and see the future value. But here's the thing: I didn't want to spin up a server, manage a database, or deal with API rate limits. I wanted something that would work instantly, even on a slow connection, and that wouldn't cost me a cent in hosting.&lt;/p&gt;

&lt;p&gt;So I built a compound interest calculator that runs entirely in the browser. No backend. No dependencies. Just pure JavaScript and a bit of math.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Why Not Just Use an Existing Tool?"
&lt;/h2&gt;

&lt;p&gt;Good question. There are plenty of compound interest calculators out there. But most of them are either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Buried in a finance website with a million other widgets&lt;/li&gt;
&lt;li&gt;Require JavaScript from external CDNs that might fail&lt;/li&gt;
&lt;li&gt;Don't let you customize the compounding frequency&lt;/li&gt;
&lt;li&gt;Or my personal favorite: they call an API to do the math. Seriously. A compound interest calculation is about 10 lines of code, and some sites will make a network request to compute it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted something self-contained. A single HTML file that could be hosted anywhere, even on a static file server, and just work. Plus, I'm an indie developer — I enjoy reinventing wheels when the wheel design is fun.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Math (It's Not That Scary)
&lt;/h2&gt;

&lt;p&gt;The core formula for compound interest with regular contributions isn't as intimidating as it looks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FV = P(1 + i)^N + PMT × ((1 + i)^N − 1) / i
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;P&lt;/strong&gt; is the initial principal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;i&lt;/strong&gt; is the periodic interest rate (annual rate divided by compounding periods per year)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;N&lt;/strong&gt; is the total number of periods (years × periods per year)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PMT&lt;/strong&gt; is the periodic contribution amount&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tricky part? When &lt;code&gt;i&lt;/code&gt; is zero. If you have a 0% interest rate, the formula &lt;code&gt;((1+i)^N − 1) / i&lt;/code&gt; becomes &lt;code&gt;0/0&lt;/code&gt;, which is undefined. Classic division by zero situation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rPct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;years&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;monthlyAdd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sym&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rPct&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;N&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;years&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PMT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;monthlyAdd&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;principalFV&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;N&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contribFV&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; 
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;PMT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;N&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; 
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PMT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;N&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// fallback for 0% interest&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;principalFV&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;contribFV&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invested&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;monthlyAdd&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;years&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;interest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fv&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;invested&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;invested&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;interest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;series&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;generateSeries&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That fallback for &lt;code&gt;i = 0&lt;/code&gt; was something I initially got wrong. The AI I was working with (more on that in a bit) wrote the formula without the edge case, and the first time I tested with 0% interest, I got &lt;code&gt;NaN&lt;/code&gt; displayed. That's not a great user experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Decision: Pure Frontend vs. API
&lt;/h2&gt;

&lt;p&gt;Let me explain why I went with pure frontend:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cost&lt;/strong&gt;: Zero server costs. Host it on any static host (GitHub Pages, Netlify, even a simple CDN).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy&lt;/strong&gt;: Users' financial data never leaves their browser. No tracking, no analytics, no "we store your data for research purposes."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed&lt;/strong&gt;: Instant response. No network latency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability&lt;/strong&gt;: No server to crash, no API to rate-limit.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The trade-off? I can't update the calculation logic without redeploying. But honestly, the math for compound interest hasn't changed in centuries. I think it's safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the Chart Without a Library
&lt;/h2&gt;

&lt;p&gt;The second challenge was the growth chart. Most people would reach for Chart.js or D3. But I wanted to keep this dependency-free. So I wrote a tiny SVG chart generator.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;drawChart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;series&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sym&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;maxVal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;series&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;points&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;series&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;series&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;maxVal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;svg viewBox="0 0 &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;
    &amp;lt;polyline points="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;points&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" fill="none" stroke="#3b82f6" stroke-width="2"/&amp;gt;
  &amp;lt;/svg&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the essence of it. A polyline with points scaled to fit the viewBox. It's not going to win any design awards, but it shows the growth curve clearly, and it's about 15 lines of code.&lt;/p&gt;

&lt;p&gt;The challenge here was handling the edge case where all values are zero. If &lt;code&gt;maxVal&lt;/code&gt; is 0, you get a division by zero in the scaling. I had to add a check: if the max is 0, just draw a flat line at the bottom.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI-Assisted Development Experience
&lt;/h2&gt;

&lt;p&gt;Now, the part I promised I'd talk about: building this with AI assistance. I used ChatGPT for most of the initial implementation, and honestly, it was a mixed bag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I asked for:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Write a compound interest calculator in vanilla JavaScript. It should take principal, annual rate, years, compounding frequency, and monthly contribution. Return the future value and a series of values for each year to plot on a chart."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;What I got:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A pretty solid implementation of the formula, with the series generation, and a basic chart. The AI handled the core math correctly on the first try — which was impressive. But here's where it went wrong:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The 0% interest edge case&lt;/strong&gt; — as I mentioned, it returned &lt;code&gt;NaN&lt;/code&gt; without the fallback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The series generation&lt;/strong&gt; — it initially generated points for every compounding period, not every year. For daily compounding over 30 years, that's 10,950 points in the SVG. The chart was... let's say "visually noisy."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input validation&lt;/strong&gt; — it didn't handle negative numbers or non-numeric input gracefully.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;How I iterated:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I went back with specific feedback:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The 0% case returns NaN. Also, I only want yearly points in the series for the chart, not every compounding period."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The AI fixed both issues. But it introduced a new one: it started generating the series using a different formula than the main calculation, so the chart didn't match the final value. Classic inconsistency bug.&lt;/p&gt;

&lt;p&gt;At that point, I stepped in and rewrote the series generation to use the same iterative formula as the main calculation. This is where I think the human still has an edge — understanding that two separate code paths computing the "same" thing will inevitably drift apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AI Got Right vs. Where I Had to Step In
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;AI handled well:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The core math formula&lt;/li&gt;
&lt;li&gt;The HTML/CSS structure for the form&lt;/li&gt;
&lt;li&gt;The SVG chart generation (once I specified the requirements clearly)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Where AI struggled:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Edge cases (zero interest, zero principal, zero contribution)&lt;/li&gt;
&lt;li&gt;Consistency between different parts of the calculation&lt;/li&gt;
&lt;li&gt;Understanding the UX implications of its choices (like generating 10,000 data points)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My honest take: for a tool like this, AI-assisted development saved me maybe 60-70% of the time. It's excellent for getting a working skeleton quickly. But the last 30% — the edge cases, the polish, the consistency — that's where I had to bring my own experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Always test the edge cases.&lt;/strong&gt; The AI didn't, and I nearly shipped a calculator that showed &lt;code&gt;NaN&lt;/code&gt; for a very valid input (0% interest).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keep it dependency-free when you can.&lt;/strong&gt; For a single-purpose tool, a full charting library is overkill. A polyline SVG does the job.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The "periodic contribution" assumption matters.&lt;/strong&gt; I chose to model contributions at the end of each period (which is standard for most investment scenarios). But this is a subtle detail that affects results, and it's worth documenting for users.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AI is a great pair programmer, not a replacement.&lt;/strong&gt; It wrote the first draft in minutes. But I had to review every line with a skeptical eye.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Final Result
&lt;/h2&gt;

&lt;p&gt;During this process, I built a small browser-based tool to make this workflow easier. It's a single HTML file that calculates compound interest with optional monthly contributions and displays a growth chart. You can try it out if you want to see the approach in action.&lt;/p&gt;

&lt;p&gt;The whole thing is about 200 lines of HTML/CSS/JavaScript. No build step, no dependencies, no server. Just open the file and it works.&lt;/p&gt;

&lt;p&gt;If you're building similar utility tools, I'd encourage you to think about whether you really need a backend. For calculation-heavy tools, the browser is surprisingly capable. And with AI to help you scaffold the initial implementation, you can go from idea to working tool in a single afternoon.&lt;/p&gt;

&lt;p&gt;Just remember to test the edge cases. The AI probably didn't.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>javascript</category>
      <category>sideprojects</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building a Date Difference Calculator: The Hidden Complexity of Time</title>
      <dc:creator>ggwork</dc:creator>
      <pubDate>Fri, 28 Aug 2026 06:25:52 +0000</pubDate>
      <link>https://dev.to/ggwork/building-a-date-difference-calculator-the-hidden-complexity-of-time-329d</link>
      <guid>https://dev.to/ggwork/building-a-date-difference-calculator-the-hidden-complexity-of-time-329d</guid>
      <description>&lt;p&gt;I was working on a project timeline feature when I hit an unexpected wall. The requirement seemed simple: "Show how many days are between these two dates." Easy, right? &lt;code&gt;(end - start) / 86400000&lt;/code&gt;. Done.&lt;/p&gt;

&lt;p&gt;Except it wasn't done. Because "days between" means completely different things depending on whether you're planning a sprint, calculating payroll, or tracking a contract period. And that's before we even get into time zones.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with "Just Subtract the Dates"
&lt;/h2&gt;

&lt;p&gt;Here's the thing that trips up most developers (myself included): a date difference isn't just one number. It's several different numbers, each answering a different question:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Total days&lt;/strong&gt;: The raw duration in 24-hour blocks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calendar breakdown&lt;/strong&gt;: How many full years, months, and days? (Spoiler: it's not just &lt;code&gt;totalDays / 365&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Working days&lt;/strong&gt;: How many weekdays? (Because weekends don't exist when you're on a deadline)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I needed all three, and I needed them to be accurate. Existing libraries like &lt;code&gt;date-fns&lt;/code&gt; or &lt;code&gt;dayjs&lt;/code&gt; handle some of this, but they're heavy for what I needed. Plus, I'm a bit of a minimalist when it comes to dependencies — adding a 50KB library to calculate what should be a few simple functions felt like using a chainsaw to cut a piece of string.&lt;/p&gt;

&lt;p&gt;So I decided to build it myself. Because apparently I enjoy reinventing wheels.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Time Zone Trap
&lt;/h2&gt;

&lt;p&gt;The first thing that bit me: time zones. JavaScript's &lt;code&gt;Date&lt;/code&gt; object is notoriously tricky with this. If you parse &lt;code&gt;"2026-08-01"&lt;/code&gt; with &lt;code&gt;new Date("2026-08-01")&lt;/code&gt;, you get UTC midnight — but if you're in a different timezone, that's actually the previous evening for you.&lt;/p&gt;

&lt;p&gt;The fix was to use &lt;code&gt;datetime-local&lt;/code&gt; inputs and parse everything in local time. No UTC conversions, no &lt;code&gt;getTimezoneOffset()&lt;/code&gt; gymnastics. Here's the core parsing function that saved my sanity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parseLocal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The &lt;code&gt;datetime-local&lt;/code&gt; input format (&lt;code&gt;YYYY-MM-DDTHH:mm&lt;/code&gt;) is designed to be parsed as local time, so &lt;code&gt;new Date()&lt;/code&gt; handles it correctly. The key insight: &lt;strong&gt;don't try to be clever with timezones when you don't need to be&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Calendar Breakdown Problem
&lt;/h2&gt;

&lt;p&gt;Now for the interesting part. I needed to break down a date range into years, months, and days. This sounds straightforward until you realize that months have varying lengths, and "1 month from January 31" is genuinely ambiguous.&lt;/p&gt;

&lt;p&gt;I went with a calendar-push approach: increment the start date by years until you can't go further without exceeding the end date, then months, then days. Here's the logic that made it work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calendarBreakdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;temp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;addMonths&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;temp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;addMonths&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;addMonths&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mo&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;mo&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;temp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;addMonths&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;temp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;86400000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;addMonths&lt;/code&gt; function handles the edge cases — like February 29 — by clamping to the end of the month. It's not perfect (what is, with dates?), but it gives results that match human intuition about calendar spans.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working Days: The Weekend Problem
&lt;/h2&gt;

&lt;p&gt;The workday calculation sounded easy: iterate through dates, skip Saturdays and Sundays. But here's a subtlety that caught me: should the workday count include the start and end dates? Most people think "between" means excluding endpoints, but in business contexts, you usually want inclusive counting.&lt;/p&gt;

&lt;p&gt;I went with inclusive counting, and the implementation is refreshingly simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;countWorkdays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDay&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;day&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;day&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;getDay()&lt;/code&gt; method returns 0 for Sunday and 6 for Saturday, so checking for those two values handles the entire weekend logic. No arrays, no constants, no lookup tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AI Actually Helped
&lt;/h2&gt;

&lt;p&gt;Now, the part you're probably curious about: how much of this did I build with AI assistance?&lt;/p&gt;

&lt;p&gt;I'll be honest — I used Claude for a significant portion of this. The interesting part was the iterative process. My first prompt was something like: "Write a date difference calculator that shows days, hours, and workdays."&lt;/p&gt;

&lt;p&gt;What I got back was... technically correct but practically useless. It used UTC everywhere, which would have been fine if I was building for a server, but this was a browser tool. The AI didn't think about timezones because I didn't ask it to.&lt;/p&gt;

&lt;p&gt;The back-and-forth went something like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Me&lt;/strong&gt;: "The dates are off by one day when I test with local times."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI&lt;/strong&gt;: "Ah, you need to use local time parsing. Here's the fix." (And it was right.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Me&lt;/strong&gt;: "Now the calendar breakdown doesn't match what I expect for ranges crossing month boundaries."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI&lt;/strong&gt;: "You need to use a calendar-push approach instead of just dividing total days."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The AI was genuinely helpful for catching edge cases I hadn't considered and for refactoring the logic. But it also made mistakes — the first version of the workday calculator excluded the start date, which I had to catch and fix.&lt;/p&gt;

&lt;p&gt;My honest take: AI is excellent at generating correct code for well-defined problems, but it's terrible at understanding what "well-defined" means for your specific use case. You still need to be the domain expert.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tool That Emerged
&lt;/h2&gt;

&lt;p&gt;During this process, I built a small browser-based tool to make this workflow easier. It's a date difference calculator that handles all three modes — total days, calendar breakdown, and workday counting — with a clean interface that doesn't require any server-side processing.&lt;/p&gt;

&lt;p&gt;The UI is deliberately minimal: two date inputs, a checkbox for workday mode, and a results panel. The entire thing runs in the browser with zero dependencies, which means it's instant and works offline.&lt;/p&gt;

&lt;p&gt;One design decision I'm particularly happy about: defaulting to showing "today → 30 days from now" as an example. It gives users immediate feedback and demonstrates the tool's capabilities without requiring them to figure out the interface first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Time zones are the root of all evil&lt;/strong&gt;. If you don't need to handle them, don't. Local time parsing with &lt;code&gt;datetime-local&lt;/code&gt; inputs sidesteps an entire class of bugs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;"Days between" is ambiguous&lt;/strong&gt;. Always clarify whether you need total days, calendar days, or working days. They're all different numbers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AI is a pair programmer, not a replacement&lt;/strong&gt;. It caught edge cases I missed, but it also missed cases I caught. We complemented each other.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dependencies aren't always worth it&lt;/strong&gt;. For something this focused, a few well-written functions beat a library every time.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The tool is live at &lt;a href="https://craftvo.app/en/tool/date-difference" rel="noopener noreferrer"&gt;craftvo.app&lt;/a&gt; if you want to see the final result. It's free, it's fast, and it handles the date math so you don't have to.&lt;/p&gt;

&lt;p&gt;Now if you'll excuse me, I need to go explain to my PM why "2 days" and "48 hours" are apparently different numbers.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>programming</category>
      <category>software</category>
    </item>
    <item>
      <title>Building a Braille Translator in the Browser: When Unicode Bit Manipulation Meets AI Pair Programming</title>
      <dc:creator>ggwork</dc:creator>
      <pubDate>Fri, 28 Aug 2026 02:20:50 +0000</pubDate>
      <link>https://dev.to/ggwork/building-a-braille-translator-in-the-browser-when-unicode-bit-manipulation-meets-ai-pair-7g</link>
      <guid>https://dev.to/ggwork/building-a-braille-translator-in-the-browser-when-unicode-bit-manipulation-meets-ai-pair-7g</guid>
      <description>&lt;p&gt;While working on a collection of browser-based utility tools, I stumbled upon an interesting challenge: building a Braille translator. Not because I have any personal connection to Braille — I'm just a developer who loves Unicode puzzles. But the problem turned out to be more fascinating than I expected, with a hidden layer of complexity that made me rethink how I approach character encoding.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem Nobody Asked Me to Solve
&lt;/h2&gt;

&lt;p&gt;I was building a suite of small, focused web tools. You know the type — URL encoders, JSON formatters, maybe a base64 decoder. Useful, boring, done a thousand times. But Braille? That's different.&lt;/p&gt;

&lt;p&gt;Here's the thing: most "Braille translators" online are either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Flash relics from 2005 that barely work on modern browsers&lt;/li&gt;
&lt;li&gt;Server-side tools that require uploading your text somewhere&lt;/li&gt;
&lt;li&gt;Overly complex accessibility suites when all you need is "text → Braille → text"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted something that runs entirely in the browser, works offline, and doesn't send sensitive text to a server. Because apparently, I enjoy reinventing wheels.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Moment I Realized Braille is Just Binary
&lt;/h2&gt;

&lt;p&gt;Here's what blew my mind: Braille isn't some mystical code. It's literally binary.&lt;/p&gt;

&lt;p&gt;A Braille cell has 6 dots, arranged in 2 columns and 3 rows. Each dot is either raised or flat. That's 2^6 = 64 possible combinations. And in Unicode, those 64 combinations map directly to code points U+2800 through U+283F.&lt;/p&gt;

&lt;p&gt;The mapping is beautifully simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Dot 1 is bit 0, dot 2 is bit 1, dot 3 is bit 2, etc.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;brailleCodePoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0x2800&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;dotPatternNumber&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So if you know the dot pattern for a letter, you just add the corresponding number to 0x2800. The letter 'a' is dot 1, so it's 0x2800 + 1 = 0x2801, which is ⠁. The letter 'b' is dots 1 and 2, so it's 0x2800 + 3 = 0x2803, which is ⠃.&lt;/p&gt;

&lt;p&gt;Wait, did I just say "the letter 'a' is dot 1"? Because that's where the fun begins.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Alphabet Is Not What You Think
&lt;/h2&gt;

&lt;p&gt;Here's where my initial assumptions fell apart. I thought I'd just create a simple dictionary mapping letters to their Unicode Braille characters. Then I discovered the actual Braille alphabet pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a = dot 1&lt;/li&gt;
&lt;li&gt;b = dots 1,2&lt;/li&gt;
&lt;li&gt;c = dots 1,4&lt;/li&gt;
&lt;li&gt;d = dots 1,4,5&lt;/li&gt;
&lt;li&gt;e = dots 1,5&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first 10 letters (a-j) follow a pattern, then k-t add dot 3 to the first 10, and u-z follow another pattern. It's almost systematic, but not quite. There's a method to the madness, but you can't derive it from a simple formula — you need the actual mapping.&lt;/p&gt;

&lt;p&gt;I initially tried to write a clever algorithmic solution. That lasted about 15 minutes before I gave up and hardcoded the mapping. Sometimes the simple solution is the right one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Number Problem: A Trap for the Unwary
&lt;/h2&gt;

&lt;p&gt;Just when I thought I had it figured out, I hit the numbers issue.&lt;/p&gt;

&lt;p&gt;In Braille, the letters a-j double as numbers 1-0. But you can't just type "123" and get Braille numbers — you need a special number sign (⠼) before them. So "123" becomes "⠼⠁⠃⠉", not just "⠁⠃⠉".&lt;/p&gt;

&lt;p&gt;This creates an interesting ambiguity: if you see "⠁⠃⠉" without the number sign, is it "abc" or "123"? The answer is context-dependent, which makes bidirectional translation genuinely tricky.&lt;/p&gt;

&lt;p&gt;My solution: when translating text to Braille, detect digits and prepend the number sign once for a run of consecutive digits. When translating Braille to text, if the number sign appears, treat the next characters as digits until you hit a space or non-letter character.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;textToBraille&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;inNumber&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;char&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;char&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;9&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;inNumber&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;BRAILLE_NUMBER_SIGN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;inNumber&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;BRAILLE_MAP&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;inNumber&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;BRAILLE_MAP&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was one of those moments where I realized: "I'm not just building a translator, I'm building a state machine."&lt;/p&gt;

&lt;h2&gt;
  
  
  The Dot Pattern Visualization
&lt;/h2&gt;

&lt;p&gt;Now for the part that made this tool actually useful: showing the dot patterns. Because if you're learning Braille, just seeing "⠓" doesn't help — you need to know that's dots 1, 2, and 5.&lt;/p&gt;

&lt;p&gt;The visualization is straightforward once you have the dot pattern number:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getDots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;codePoint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;codePoint&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mh"&gt;0x2800&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dots&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;dots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;dots&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bit manipulation. Again. It's like binary is following me around.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI-Assisted Development: The Good, The Bad, The Ugly
&lt;/h2&gt;

&lt;p&gt;Now for the part I'm most honest about: I used AI to build much of this. And it was a mixed experience.&lt;/p&gt;

&lt;p&gt;The first prompt I gave was something like: "Create a Braille translator with text to Braille and Braille to text conversion, with a reference chart."&lt;/p&gt;

&lt;p&gt;The AI nailed the basic structure in one shot. It created the HTML layout, the CSS styling, the i18n system — all the scaffolding. I was impressed.&lt;/p&gt;

&lt;p&gt;But then came the bugs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug #1: The Case Sensitivity Trap
&lt;/h3&gt;

&lt;p&gt;The AI initially made the translation case-sensitive. "HELLO" would produce different results than "hello". In Braille, there's no such thing as lowercase — it's all one case. The AI hadn't thought about normalizing input to lowercase before mapping.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug #2: The Number Sign Placement
&lt;/h3&gt;

&lt;p&gt;The AI's first attempt at numbers was a disaster. It would put a number sign before every single digit, so "123" became "⠼⠁⠼⠃⠼⠉" instead of "⠼⠁⠃⠉". That's like writing "one hundred twenty three" as "one-one-hundred-twenty-three". Technically understandable, but completely wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug #3: The Reverse Translation Mapping
&lt;/h3&gt;

&lt;p&gt;The biggest issue was reverse translation. The AI had a static map for text-to-Braille but didn't properly handle the reverse lookup. When I asked it to translate Braille back to text, it would get stuck on ambiguous characters. Is "⠁" an "a" or a "1"? Without context, you can't tell.&lt;/p&gt;

&lt;p&gt;The AI's solution was to always treat it as a letter unless the number sign appeared. That's actually correct, but the AI couldn't articulate why it made that choice — it just happened to be right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I Had to Step In
&lt;/h2&gt;

&lt;p&gt;The AI was great at generating boilerplate and basic logic, but it kept making the same class of mistakes: not understanding the domain. It didn't know that Braille is case-insensitive, that numbers need special handling, or that the dot pattern visualization would be the most useful feature for learners.&lt;/p&gt;

&lt;p&gt;I had to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Explain the number sign rule explicitly, multiple times&lt;/li&gt;
&lt;li&gt;Provide the actual Braille mapping table (the AI kept hallucinating wrong patterns)&lt;/li&gt;
&lt;li&gt;Design the dot visualization myself — the AI's first attempt was a mess of nested divs that looked like a CSS nightmare&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The lesson? AI is great for generating code, but it's terrible at understanding domain-specific rules it wasn't explicitly trained on. Braille is a niche topic, and the AI's training data apparently had conflicting information about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The i18n Challenge
&lt;/h2&gt;

&lt;p&gt;Building bilingual support (Chinese/English) added another layer of complexity. The tool needed to work for both Chinese and English speakers, which meant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All UI strings needed translation&lt;/li&gt;
&lt;li&gt;The language switcher needed to persist (I used localStorage)&lt;/li&gt;
&lt;li&gt;The Braille reference chart needed to be language-agnostic (it is — Braille is Braille)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The i18n system itself was straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;i18n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;zh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;盲文翻译器&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;textInput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;文本输入&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;brailleOutput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;盲文输出&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Braille Translator&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;textInput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Text Input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;brailleOutput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Braille Output&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tricky part was making the AI understand that the default language should be Chinese, not English. It kept defaulting to English and I had to keep reminding it. Classic "works on my machine" situation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Considerations
&lt;/h2&gt;

&lt;p&gt;For a tool like this, performance is almost a non-issue. The entire translation is O(n) — you're just iterating over characters and doing a lookup. Even a 10,000-character document would translate in milliseconds.&lt;/p&gt;

&lt;p&gt;But there was one performance consideration: the reference chart. Rendering 26 letters + 10 digits + punctuation as individual DOM elements adds up. I used a simple CSS grid with &lt;code&gt;auto-fill&lt;/code&gt; columns, which handles responsiveness gracefully without JavaScript. No virtual scrolling needed for 60 items, but it's worth thinking about if you ever scale this to include contractions (the full Braille system has hundreds of contractions).&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Why" Behind the Architecture
&lt;/h2&gt;

&lt;p&gt;I chose vanilla JavaScript over React or Vue for a few reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Zero dependencies&lt;/strong&gt; — this tool needs to work forever, even if npm packages disappear&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single file&lt;/strong&gt; — easy to embed, easy to share, easy to deploy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No build step&lt;/strong&gt; — I can just open the HTML file and it works&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The trade-off? I had to write more boilerplate for things like state management and event handling. But for a tool this small, it's the right call. A React app with a build step would be overkill for what's essentially a &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt; with a mapping function.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Bit manipulation is everywhere&lt;/strong&gt; — even in something as human as Braille&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domain knowledge beats clever code&lt;/strong&gt; — you can't algorithmically derive the Braille alphabet; you need the actual mapping&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI is a junior developer&lt;/strong&gt; — great at scaffolding, bad at domain-specific edge cases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple tools are underrated&lt;/strong&gt; — sometimes the best solution is a single HTML file that just works&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Final Product
&lt;/h2&gt;

&lt;p&gt;The result is a browser-based Braille translator that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Converts text to Braille and back&lt;/li&gt;
&lt;li&gt;Shows dot patterns for each character&lt;/li&gt;
&lt;li&gt;Handles numbers correctly (with the number sign)&lt;/li&gt;
&lt;li&gt;Works in both Chinese and English&lt;/li&gt;
&lt;li&gt;Has a complete reference chart&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;During this process, I built a small browser-based tool to make this workflow easier. You can find it &lt;a href="https://craftvo.app/en/tool/braille-translator" rel="noopener noreferrer"&gt;here&lt;/a&gt; if you're curious.&lt;/p&gt;

&lt;p&gt;The best part? I learned something unexpected about Braille — it's not a mystery code, it's just binary with a different face. And that's the kind of discovery that makes building these little tools worthwhile.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;P.S. If you're wondering why I chose this project: I wanted to build something that would make me think about Unicode in a new way. Mission accomplished. The fact that it might actually help someone learn Braille is just a bonus.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>javascript</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>From "What's the MIME for That?" to a 200+ Entry Reference Tool</title>
      <dc:creator>ggwork</dc:creator>
      <pubDate>Wed, 26 Aug 2026 06:08:25 +0000</pubDate>
      <link>https://dev.to/ggwork/from-whats-the-mime-for-that-to-a-200-entry-reference-tool-1a5j</link>
      <guid>https://dev.to/ggwork/from-whats-the-mime-for-that-to-a-200-entry-reference-tool-1a5j</guid>
      <description>&lt;p&gt;You know that moment when you're building an upload feature and you need to validate file types, and you find yourself Googling "what's the MIME type for .webp" for the fifth time that week? That was me last month. I was working on a file upload handler and kept hitting the same wall: I'd remember common ones like &lt;code&gt;image/jpeg&lt;/code&gt;, but then I'd need something obscure like &lt;code&gt;.avif&lt;/code&gt; or &lt;code&gt;.m4a&lt;/code&gt; and I'd be back to searching.&lt;/p&gt;

&lt;p&gt;The worst part? The answers were scattered across MDN docs, Stack Overflow threads from 2012, and random GitHub gists with questionable accuracy. I wanted a single source of truth I could reference quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Just Build It" Temptation
&lt;/h2&gt;

&lt;p&gt;I considered just hardcoding the few types I needed. But I knew that was a band-aid. I'd be back in the same spot next week with a different project. So I did what any reasonable developer would do: I decided to build a comprehensive reference tool.&lt;/p&gt;

&lt;p&gt;Because apparently I enjoy reinventing wheels.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Data Problem
&lt;/h2&gt;

&lt;p&gt;The first challenge was obvious: I needed a solid MIME type database. Not just the common ones — I wanted coverage across documents, images, video, audio, archives, code, fonts, and "other" stuff that doesn't fit neatly anywhere.&lt;/p&gt;

&lt;p&gt;Here's what I learned: there's no official "MIME type registry" that's both comprehensive and easy to consume. The IANA registry is the authority, but it's a massive HTML table that's painful to scrape. I ended up cross-referencing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The IANA registry (for official types)&lt;/li&gt;
&lt;li&gt;MDN's MIME type documentation (for practical usage)&lt;/li&gt;
&lt;li&gt;My own experience (for the "what people actually use" factor)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The data structure ended up being refreshingly simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.webp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/webp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;img&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WebP 图片&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Four fields. No relational database, no API calls — just a plain array in JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Search Logic: Simpler Than You'd Think
&lt;/h2&gt;

&lt;p&gt;The search was the fun part. I wanted bidirectional lookup — type an extension and get the MIME, or type a MIME and get extensions. My first approach was embarrassingly over-engineered. I was thinking about fuzzy matching, Levenshtein distances, the whole nine yards.&lt;/p&gt;

&lt;p&gt;Then I realized: developers searching for MIME types aren't typos. They know exactly what they're looking for. They just need quick verification. So I kept it simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Strip leading dots from the input&lt;/li&gt;
&lt;li&gt;Case-insensitive substring matching&lt;/li&gt;
&lt;li&gt;Check against extension, MIME type, and description&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The debounce was the only "fancy" part — 200ms to prevent the render function from firing on every keystroke. The whole search logic fits in about 15 lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI Collaboration Experiment
&lt;/h2&gt;

&lt;p&gt;Here's where things got interesting. I decided to build this with AI assistance — not as a gimmick, but because I genuinely wanted to see how well it could handle a well-specified, self-contained project. This felt like the perfect test case: clear requirements, no external dependencies, pure frontend.&lt;/p&gt;

&lt;p&gt;I gave the AI a detailed spec: the exact data structure, the interaction patterns, the UI layout, even the SEO tags. The first attempt was... surprisingly good. It nailed the structure and the search logic on the first try.&lt;/p&gt;

&lt;p&gt;But it completely missed the dark mode support. Classic "works on my machine" situation — the AI assumed everyone uses light mode.&lt;/p&gt;

&lt;p&gt;The iteration process was where the real value came through. I'd say "add prefers-color-scheme media queries" and it would generate the CSS. Then I'd notice the category badges were unreadable in dark mode, and it would fix those specific color values. It was like pair programming with a very fast, slightly forgetful junior developer.&lt;/p&gt;

&lt;p&gt;The part where I had to step in? The data. The AI's initial MIME database was thin — maybe 60 entries. I had to explicitly list the file types I wanted covered: "add these: cpp, java, py, go, rs, woff, woff2, ttf, otf..." That's the kind of domain knowledge that AI still can't fully replicate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The i18n Surprise
&lt;/h2&gt;

&lt;p&gt;I initially thought internationalization would be the hardest part. I was planning a full i18n library integration. But for a tool like this, a lightweight approach made more sense:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;i18n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;zh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type 查询&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;searchPlaceholder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;输入扩展名或MIME...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type Lookup&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;searchPlaceholder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Enter extension or MIME...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just a simple dictionary with a language switcher. The descriptions in the database stay in their original language — which actually makes sense because MIME types themselves are language-agnostic. The tool is for developers, and developers know what &lt;code&gt;image/jpeg&lt;/code&gt; means regardless of their native language.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Copy Interaction
&lt;/h2&gt;

&lt;p&gt;The click-to-copy feature was a requirement I initially underestimated. I thought &lt;code&gt;navigator.clipboard.writeText()&lt;/code&gt; was a solved problem. Turns out, it has a subtle gotcha: it only works in secure contexts (HTTPS or localhost). For a tool that people might open as a local HTML file, I needed a fallback.&lt;/p&gt;

&lt;p&gt;The fallback is the old-school approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;textarea&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;textarea&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;textarea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;textarea&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;textarea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;copy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;textarea&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's deprecated, but it works everywhere. The lesson here: don't assume modern APIs are universally available, even for a "modern" tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance: The "It's Just an Array" Moment
&lt;/h2&gt;

&lt;p&gt;I spent way too long thinking about performance optimization. Should I use a Map? A binary search tree? Index the data by extension?&lt;/p&gt;

&lt;p&gt;Then I realized: the entire dataset is 215 entries. The search runs in milliseconds even with a naive filter. The only performance consideration that actually mattered was the debounce on the search input, and even that was more about avoiding excessive DOM updates than actual CPU usage.&lt;/p&gt;

&lt;p&gt;The table rendering? I'm just regenerating the HTML string and setting &lt;code&gt;innerHTML&lt;/code&gt;. It's not React, but it's also not 10,000 rows. Sometimes the simplest solution is the right one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stats That Matter
&lt;/h2&gt;

&lt;p&gt;One feature I'm glad I added: the result counter. It shows "X results out of Y total entries." It sounds trivial, but when you're searching for something and get zero results, seeing "0 results out of 215 entries" is much more reassuring than just "No results found." It confirms the tool is working — you just searched for something that doesn't exist in the database.&lt;/p&gt;

&lt;p&gt;Speaking of no results: I made sure to include a friendly empty state. Because nothing kills developer trust faster than a blank screen with no explanation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;If I were to rebuild this, I'd probably add a "random MIME type" button. Not because it's useful, but because it's fun. Sometimes you want to test how your code handles unexpected input.&lt;/p&gt;

&lt;p&gt;I'd also consider adding copy feedback per-row instead of a global status message. Right now, clicking any row copies the MIME and shows a status bar message. It works, but a per-row "copied!" tooltip would be more immediate feedback.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Verdict on AI-Assisted Development
&lt;/h2&gt;

&lt;p&gt;This project was a genuine test of AI-assisted development, and my honest take is: it's great for boilerplate and structure, but it still needs a human for judgment calls.&lt;/p&gt;

&lt;p&gt;The AI handled the CSS layout, the search logic, the i18n structure, and even the SEO meta tags without complaint. It made mistakes — the dark mode oversight being the biggest one — but they were easy to catch and fix.&lt;/p&gt;

&lt;p&gt;What the AI couldn't do was understand the subtle requirements that come from real-world usage. It didn't know that &lt;code&gt;.docx&lt;/code&gt; is more common than &lt;code&gt;.doc&lt;/code&gt; in 2024. It didn't know that &lt;code&gt;application/x-www-form-urlencoded&lt;/code&gt; deserves a spot in the database even though it's not a file extension. Those insights come from experience, not from training data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;I built this tool because I was tired of searching for MIME types. The process taught me that even "boring" developer tools have interesting engineering decisions lurking beneath the surface. The data structure matters more than the algorithm. The UX details (like the result counter and empty states) matter more than the feature list.&lt;/p&gt;

&lt;p&gt;And if you're wondering: yes, I still Google MIME types sometimes. But now I have a better starting point.&lt;/p&gt;




&lt;p&gt;During this process, I built a small browser-based tool to make this workflow easier. If you're dealing with the same problem, you can find it at &lt;a href="https://craftvo.app/en/tool/content-type-lookup" rel="noopener noreferrer"&gt;Craftvo's Content-Type Lookup&lt;/a&gt;. It's just a plain HTML file with vanilla JS — no build tools, no frameworks, no server. Open it, search, copy, move on with your life.&lt;/p&gt;




&lt;p&gt;Tags: &lt;code&gt;mime-types&lt;/code&gt;, &lt;code&gt;web-development&lt;/code&gt;, &lt;code&gt;javascript&lt;/code&gt;, &lt;code&gt;developer-tools&lt;/code&gt;, &lt;code&gt;ai-assisted-development&lt;/code&gt;&lt;/p&gt;

</description>
      <category>softwaredevelopment</category>
      <category>tools</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
