<?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: Vera Yang</title>
    <description>The latest articles on DEV Community by Vera Yang (@vera_yang_tw).</description>
    <link>https://dev.to/vera_yang_tw</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%2F4050846%2Fc00ec23d-dfa1-4afd-8328-7d212480df15.png</url>
      <title>DEV Community: Vera Yang</title>
      <link>https://dev.to/vera_yang_tw</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vera_yang_tw"/>
    <language>en</language>
    <item>
      <title>Keep moving forward!</title>
      <dc:creator>Vera Yang</dc:creator>
      <pubDate>Fri, 07 Aug 2026 08:17:27 +0000</pubDate>
      <link>https://dev.to/vera_yang_tw/-14dl</link>
      <guid>https://dev.to/vera_yang_tw/-14dl</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/begoodtool/how-i-built-a-highly-profitable-web-platform-but-wrote-zero-backend-code-to-keep-my-server-costs-1n5l" class="crayons-story__hidden-navigation-link"&gt;How I built a highly profitable web platform, but wrote zero backend code to keep my server costs at $0.&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;
          &lt;a class="crayons-logo crayons-logo--l" href="/begoodtool"&gt;
            &lt;img alt="BeGoodTool.com logo" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F14295%2Fd519278f-433a-4dab-9489-447ef51fc6d4.jpg" class="crayons-logo__image" width="300" height="301"&gt;
          &lt;/a&gt;

          &lt;a href="/yuntao_lin" class="crayons-avatar  crayons-avatar--s absolute -right-2 -bottom-2 border-solid border-2 border-base-inverted  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4066889%2F706cc7d3-d124-4ffc-b344-2cd4e55e930b.jpg" alt="yuntao_lin profile" class="crayons-avatar__image" width="400" height="400"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/yuntao_lin" class="crayons-story__secondary fw-medium m:hidden"&gt;
              YunTao Lin
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                YunTao Lin
                
              
              &lt;div id="story-author-preview-content-4338408" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/yuntao_lin" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4066889%2F706cc7d3-d124-4ffc-b344-2cd4e55e930b.jpg" class="crayons-avatar__image" alt="" width="400" height="400"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;YunTao Lin&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;span&gt;
              &lt;span class="crayons-story__tertiary fw-normal"&gt; for &lt;/span&gt;&lt;a href="/begoodtool" class="crayons-story__secondary fw-medium"&gt;BeGoodTool.com&lt;/a&gt;
            &lt;/span&gt;
          &lt;/div&gt;
          &lt;a href="https://dev.to/begoodtool/how-i-built-a-highly-profitable-web-platform-but-wrote-zero-backend-code-to-keep-my-server-costs-1n5l" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Aug 7&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/begoodtool/how-i-built-a-highly-profitable-web-platform-but-wrote-zero-backend-code-to-keep-my-server-costs-1n5l" id="article-link-4338408"&gt;
          How I built a highly profitable web platform, but wrote zero backend code to keep my server costs at $0.
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/frontend"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;frontend&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/architecture"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;architecture&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/indiehackers"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;indiehackers&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/begoodtool/how-i-built-a-highly-profitable-web-platform-but-wrote-zero-backend-code-to-keep-my-server-costs-1n5l" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt;&amp;nbsp;reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/begoodtool/how-i-built-a-highly-profitable-web-platform-but-wrote-zero-backend-code-to-keep-my-server-costs-1n5l#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            10 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Dead-pixel test sites are just colored divs — the actual engineering problem is the Fullscreen API, not the colors</title>
      <dc:creator>Vera Yang</dc:creator>
      <pubDate>Fri, 07 Aug 2026 07:00:03 +0000</pubDate>
      <link>https://dev.to/begoodtool/dead-pixel-test-sites-are-just-colored-divs-the-actual-engineering-problem-is-the-fullscreen-api-2a22</link>
      <guid>https://dev.to/begoodtool/dead-pixel-test-sites-are-just-colored-divs-the-actual-engineering-problem-is-the-fullscreen-api-2a22</guid>
      <description>&lt;p&gt;I bought a used monitor a while back and did what everyone tells you to do first: pull up an online "dead pixel test" and cycle through some solid colors before deciding whether to keep it. It worked, but afterward I was curious why every single one of these tools looks identical — same handful of colors, same full-bleed look, no ads-covered corners, nothing fancy. So I built my own version to see what's actually involved, and it turns out almost none of the interesting work is in the colors. It's in getting a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; to actually, reliably, cover the entire physical screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The colors are chosen, not decorative
&lt;/h2&gt;

&lt;p&gt;The color grid isn't nine random swatches — each one is picked to isolate a specific kind of pixel failure:&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;testModes&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;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;red&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#FF0000&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;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;green&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#00FF00&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;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#0000FF&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;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;black&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#000000&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;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;white&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#FFFFFF&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;darkText&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;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gray&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#808080&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;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yellow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#FFFF00&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;darkText&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;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cyan&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#00FFFF&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;darkText&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;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;magenta&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#FF00FF&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;Pure red, green, and blue each light up exactly one of a pixel's three sub-pixels — that's how you catch a sub-pixel that's stuck &lt;em&gt;off&lt;/em&gt; (it stays dark against a color that should light it) or stuck permanently &lt;em&gt;on&lt;/em&gt; in one channel (it shows up as a stray colored dot against black). Black and white check the opposite extremes: a "dead" pixel is usually defined as one that never lights at all, so a pure black fill is actually the easiest way to spot one — any point of light against total black is a defect, not a judgment call. Gray is there because it's R, G, and B mixed in equal parts; if the panel has any color tint or backlight unevenness, gray is where a human eye notices it fastest, since a slight tint is invisible against a color but obvious against neutral gray. Yellow, cyan, and magenta are two channels at once — useful for catching a sub-pixel that's stuck in a way pure R/G/B alone wouldn't reveal.&lt;/p&gt;

&lt;p&gt;Then there are two patterns instead of flat colors:&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;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gradient&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;linear-gradient(to right, #000000, #FFFFFF)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;isPattern&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="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checkerboard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#fff&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;backgroundImage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;linear-gradient(45deg, #000 25%, transparent 25%, transparent 75%, #000 75%, #000), linear-gradient(45deg, #000 25%, transparent 25%, transparent 75%, #000 75%, #000)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;backgroundSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;20px 20px&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;backgroundPosition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0 0, 10px 10px&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;isPattern&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gradient isn't for pixel defects at all — it's for banding. A cheap panel (or a panel driven at a reduced color depth) can't render a smooth 0–255 transition, so instead of a clean fade you see visible steps. The checkerboard does something similar for contrast and sharpness: alternating black/white squares make it obvious if edges look soft or if there's visible bleed between adjacent pixels, neither of which a flat color would show you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fullscreen isn't cosmetic — it's the whole point, and it's annoyingly stateful
&lt;/h2&gt;

&lt;p&gt;The naive version of this tool is "put a colored div on the page." The actual requirement is that the color has to cover &lt;em&gt;every pixel&lt;/em&gt;, including the space normally eaten by the browser's tab bar, address bar, and OS taskbar — otherwise you're not testing the whole panel. That means the Fullscreen API, not just big CSS:&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;enterFullscreen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;index&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;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;currentIndex&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;index&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;elem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fullscreenContainer&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="k"&gt;try&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;elem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requestFullscreen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;elem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestFullscreen&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;elem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webkitRequestFullscreen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;elem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;webkitRequestFullscreen&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;elem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;msRequestFullscreen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;elem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;msRequestFullscreen&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;isFullscreen&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="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;displayHint&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="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;screenTest.fullscreenError&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;Two things about this bit me before I wired it up correctly. First, &lt;code&gt;requestFullscreen()&lt;/code&gt; returns a promise that rejects if it's not called as a direct result of a user gesture — you can't call it inside a &lt;code&gt;setTimeout&lt;/code&gt; or on page load and expect it to work, which is exactly why the tool requires a click on a color swatch rather than trying to auto-launch. Second, there's no unprefixed-only world yet: Safari still wants &lt;code&gt;webkitRequestFullscreen&lt;/code&gt;, and old Edge/IE wanted &lt;code&gt;msRequestFullscreen&lt;/code&gt;, so the fallback chain isn't legacy cruft, it's still load-bearing.&lt;/p&gt;

&lt;p&gt;The other half of the problem is knowing when you've &lt;em&gt;left&lt;/em&gt; fullscreen — the user can hit Escape at any point, and the app needs to notice and reset its own state, not just rely on the browser's native exit animation:&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;handleFullscreenChange&lt;/span&gt; &lt;span class="o"&gt;=&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="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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fullscreenElement&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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="nx"&gt;webkitIsFullScreen&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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="nx"&gt;mozFullScreen&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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="nx"&gt;msFullscreenElement&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;isFullscreen&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="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;stopAutoPlay&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fullscreenchange&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleFullscreenChange&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;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;webkitfullscreenchange&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleFullscreenChange&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without that listener, pressing Escape exits the OS-level fullscreen but leaves the app thinking it's still showing the color tester, so the next click does something confusing. The fixed-position overlay underneath (&lt;code&gt;position: fixed; width: 100vw; height: 100vh; z-index: 99999&lt;/code&gt;) is really just a safety net for browsers where the fullscreen request silently fails — it covers the viewport even without real fullscreen, just not the parts of the screen the browser chrome still owns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auto-play is a plain interval, not a rendering-timed loop
&lt;/h2&gt;

&lt;p&gt;The "auto play" button just steps through the color list on a timer:&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;startAutoPlay&lt;/span&gt; &lt;span class="o"&gt;=&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;isAutoPlaying&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="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;enterFullscreen&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="nx"&gt;autoPlayTimer&lt;/span&gt; &lt;span class="o"&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="nf"&gt;nextColor&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;3000&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;I want to flag what this &lt;em&gt;isn't&lt;/em&gt;, because it's tempting to assume any color-cycling tool is secretly doing frame-rate or response-time measurement with &lt;code&gt;requestAnimationFrame&lt;/code&gt;. It's not — there's no timing measurement here at all, just "switch to the next color every 3 seconds" so you can stand back and watch the whole panel without touching anything. &lt;code&gt;setInterval&lt;/code&gt; is fine for that because nobody's timing precision matters at a 3-second cadence; it would be the wrong tool if you were trying to measure actual pixel response time or refresh-rate behavior, which needs frame-accurate deltas, not a coarse timer that browsers are free to throttle when a tab is backgrounded.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this can't actually tell you
&lt;/h2&gt;

&lt;p&gt;A couple of honest gaps, since a full-screen color page will always be a cheap proxy for real display measurement, not a replacement for it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The colors you see aren't calibrated.&lt;/strong&gt; &lt;code&gt;#FF0000&lt;/code&gt; is just a CSS value — what actually hits your eyes depends on the OS color profile, the monitor's own color space, whether a blue-light filter or "night mode" is active, and how the GPU compositor happens to render it. Two perfectly healthy panels can render "pure red" slightly differently; this tool can't and doesn't try to correct for that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It only catches what a human eye can see in real time.&lt;/strong&gt; A pixel that's stuck a very &lt;em&gt;slightly&lt;/em&gt; wrong shade, viewing-angle color shift on cheaper IPS/VA panels, or backlight bleed that only shows up at specific brightness levels are all things a careful look under the right lighting conditions will catch far better than a quick scroll through nine colors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fullscreen support is inconsistent across mobile browsers.&lt;/strong&gt; Some mobile Safari versions restrict or ignore &lt;code&gt;requestFullscreen()&lt;/code&gt; on arbitrary elements entirely, so on iOS you may just get the fixed-overlay fallback rather than true fullscreen, meaning the OS status bar area doesn't actually get tested.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that makes it useless — it's still the fastest way to catch the obvious stuff (a fully dead pixel, an obviously stuck one, ugly gradient banding) before you decide whether a monitor is worth returning. I turned the version I built into a small free tool: &lt;a href="https://begoodtool.com/screenTest/en" rel="noopener noreferrer"&gt;Screen Test Tool&lt;/a&gt;. No install, no sign-up, just click a color and go fullscreen.&lt;/p&gt;




&lt;h2&gt;
  
  
  Available in other languages
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/screenTest" rel="noopener noreferrer"&gt;螢幕測試工具&lt;/a&gt; — 繁體中文&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/screenTest/cn" rel="noopener noreferrer"&gt;屏幕测试工具&lt;/a&gt; — 简体中文&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/screenTest/en" rel="noopener noreferrer"&gt;Screen Test Tool&lt;/a&gt; — English&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/screenTest/jp" rel="noopener noreferrer"&gt;画面テストツール&lt;/a&gt; — 日本語&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/screenTest/kr" rel="noopener noreferrer"&gt;화면 테스트 도구&lt;/a&gt; — 한국어&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/screenTest/fr" rel="noopener noreferrer"&gt;Outil de Test d'Écran&lt;/a&gt; — Français&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/screenTest/ru" rel="noopener noreferrer"&gt;Инструмент Тестирования Экрана&lt;/a&gt; — Русский&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/screenTest/de" rel="noopener noreferrer"&gt;Bildschirmtest-Tool&lt;/a&gt; — Deutsch&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/screenTest/id" rel="noopener noreferrer"&gt;Alat Tes Layar&lt;/a&gt; — Bahasa Indonesia&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/screenTest/es" rel="noopener noreferrer"&gt;Herramienta de Prueba de Pantalla&lt;/a&gt; — Español&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/screenTest/vi" rel="noopener noreferrer"&gt;Công Cụ Kiểm Tra Màn Hình&lt;/a&gt; — Tiếng Việt&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/screenTest/th" rel="noopener noreferrer"&gt;เครื่องมือทดสอบหน้าจอ&lt;/a&gt; — ไทย&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/screenTest/pl" rel="noopener noreferrer"&gt;Narzędzie do Testowania Ekranu&lt;/a&gt; — Polski&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/screenTest/tr" rel="noopener noreferrer"&gt;Ekran Test Aracı&lt;/a&gt; — Türkçe&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/screenTest/it" rel="noopener noreferrer"&gt;Strumento di Test dello Schermo&lt;/a&gt; — Italiano&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/screenTest/pt" rel="noopener noreferrer"&gt;Ferramenta de Teste de Tela&lt;/a&gt; — Português&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/screenTest/nl" rel="noopener noreferrer"&gt;Schermtesttool&lt;/a&gt; — Nederlands&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/screenTest/uk" rel="noopener noreferrer"&gt;Інструмент Тестування Екрану&lt;/a&gt; — Українська&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>vue</category>
      <category>css</category>
    </item>
    <item>
      <title>Generating unique random numbers is easy — swapping min and max broke my own validation</title>
      <dc:creator>Vera Yang</dc:creator>
      <pubDate>Fri, 07 Aug 2026 05:44:32 +0000</pubDate>
      <link>https://dev.to/begoodtool/generating-unique-random-numbers-is-easy-swapping-min-and-max-broke-my-own-validation-4ae1</link>
      <guid>https://dev.to/begoodtool/generating-unique-random-numbers-is-easy-swapping-min-and-max-broke-my-own-validation-4ae1</guid>
      <description>&lt;p&gt;I put together a small number picker for an office raffle: type a minimum, type a maximum, hit generate, optionally pull several numbers with no repeats so you can draw multiple winners at once. It felt like a five-minute job. It mostly was — right up until I typed the range in backwards and the tool told me a perfectly valid request was impossible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Letting min and max be typed in either order
&lt;/h2&gt;

&lt;p&gt;The core random-pick function doesn't trust that the "minimum" field actually holds the smaller 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;random&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;real_min&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;min&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="nx"&gt;max&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;let&lt;/span&gt; &lt;span class="nx"&gt;real_max&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;min&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="nx"&gt;max&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;let&lt;/span&gt; &lt;span class="nx"&gt;real_res&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="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&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;real_max&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;real_min&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;real_min&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;real_res&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;Math.min&lt;/code&gt;/&lt;code&gt;Math.max&lt;/code&gt; normalize the two inputs before doing anything else, so if you type 99 into "min" and 1 into "max," it just works — no error telling you to swap the fields. The actual random pick is the standard inclusive-integer-range formula: &lt;code&gt;Math.random()&lt;/code&gt; gives you &lt;code&gt;[0, 1)&lt;/code&gt;, multiplying by &lt;code&gt;(range + 1)&lt;/code&gt; and flooring spreads that across every integer in the range with equal probability, then you shift up by the minimum. At the range sizes this tool deals with (double-digit or triple-digit spans), there's no meaningful &lt;code&gt;Math.random()&lt;/code&gt; bias to worry about — that only becomes a real concern at ranges in the billions, where floating-point precision starts running out of distinct multiples.&lt;/p&gt;

&lt;h2&gt;
  
  
  The validation check that forgot about the swap
&lt;/h2&gt;

&lt;p&gt;Here's where it got interesting. The "generate multiple, no duplicates" path validates the range &lt;em&gt;before&lt;/em&gt; generating anything:&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;getRandom&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;real_res&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="nx"&gt;max&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;min&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="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;multiple&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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;random.error1&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="nx"&gt;isMultiple&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="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;multiple&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="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;multiple&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="mi"&gt;100&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;0&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;multiple&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="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;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;random&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;real_res&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;now&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;real_res&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;now&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;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="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;Look closely at that first &lt;code&gt;if&lt;/code&gt;. It computes &lt;code&gt;max.value - min.value + 1&lt;/code&gt; — the raw field values, with no &lt;code&gt;Math.min&lt;/code&gt;/&lt;code&gt;Math.max&lt;/code&gt; normalization. &lt;code&gt;random()&lt;/code&gt; happily tolerates a swapped min/max, but this check doesn't. Type min=99, max=1, ask for 5 unique numbers, and the check computes &lt;code&gt;1 - 99 + 1 = -97&lt;/code&gt;, which is less than 5, so it rejects a request that's actually completely fine — there are 99 valid numbers to draw from. The tool is inconsistent with itself: it's forgiving about swapped input everywhere else, then strict about it in exactly the one place where being strict produces a wrong answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retry until it's unique, not shuffle-and-slice
&lt;/h2&gt;

&lt;p&gt;The no-duplicates mode itself is worth a second look, because there are two very different ways to implement "give me N unique numbers from a range," and this uses the simpler-sounding one:&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;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;0&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;multiple&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="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;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;random&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;real_res&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;now&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;real_res&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;now&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;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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is rejection sampling: draw a number, check if it's already in the result array, and if it is, decrement the loop counter so the same slot gets tried again. It's unbiased — every still-available number is equally likely on each draw — and it needs zero extra memory beyond the result array itself.&lt;/p&gt;

&lt;p&gt;The alternative would be shuffle-and-slice: build an array of the entire range, Fisher–Yates shuffle it, and take the first N. That's guaranteed to finish in time proportional to the range size no matter what, but it has to allocate an array covering the &lt;em&gt;whole&lt;/em&gt; range even if you only want 3 numbers out of it.&lt;/p&gt;

&lt;p&gt;Retry-until-unique flips that trade-off: cheap when N is small relative to the range, but it gets worse the closer N gets to the range size. Near the end, most of the range is already taken, so a growing fraction of draws come back as duplicates and get thrown away — the classic coupon-collector problem. On top of that, each &lt;code&gt;real_res.includes(now)&lt;/code&gt; check is a linear scan, so as the result array grows the checks themselves get slower too. This tool caps &lt;code&gt;multiple&lt;/code&gt; at 100, so in practice you'd have to set a very tight range (like 1–100 while asking for 100 numbers) to actually feel the slowdown — but it's the kind of thing that would matter a lot if someone reused this loop for a bigger range without reusing the cap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;A few things worth knowing if you actually use this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The min/max-swap validation bug above is real — if your "no duplicates" request gets rejected as impossible, try swapping which number you put in which field before assuming your range is actually too small.&lt;/li&gt;
&lt;li&gt;That &lt;code&gt;multiple.value * 1 &amp;gt; 100&lt;/code&gt; clamp happens &lt;em&gt;after&lt;/em&gt; the range check already ran with whatever number you originally typed, and it silently caps your request to 100 with no message. Ask for 500 unique numbers from a range of 10,000 and you'll quietly get 100 back, not 500.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Math.random()&lt;/code&gt; is a pseudorandom generator, not a cryptographically secure one. That's completely fine for raffles, games, and sampling, but it's not the right tool for generating anything security-sensitive, like password seeds or real-money draws.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I cleaned up the version I built for the raffle into a small free tool: &lt;a href="https://begoodtool.com/random/en" rel="noopener noreferrer"&gt;Random Number Generator (RNG)&lt;/a&gt;. No sign-up, works for any range you throw at it — just watch which field you put the bigger number in.&lt;/p&gt;




&lt;h2&gt;
  
  
  Available in other languages
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/random" rel="noopener noreferrer"&gt;隨機亂數產生器&lt;/a&gt; — 繁體中文&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/random/cn" rel="noopener noreferrer"&gt;在线随机数生成器 (RNG)&lt;/a&gt; — 简体中文&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/random/en" rel="noopener noreferrer"&gt;Random Number Generator (RNG)&lt;/a&gt; — English&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/random/jp" rel="noopener noreferrer"&gt;乱数ジェネレーター (RNG)&lt;/a&gt; — 日本語&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/random/kr" rel="noopener noreferrer"&gt;랜덤 숫자 생성기&lt;/a&gt; — 한국어&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/random/fr" rel="noopener noreferrer"&gt;Générateur de nombres aléatoires&lt;/a&gt; — Français&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/random/ru" rel="noopener noreferrer"&gt;Генератор случайных чисел&lt;/a&gt; — Русский&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/random/de" rel="noopener noreferrer"&gt;Zufallszahlengenerator (RNG)&lt;/a&gt; — Deutsch&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/random/id" rel="noopener noreferrer"&gt;Pembangkit Angka Acak&lt;/a&gt; — Bahasa Indonesia&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/random/es" rel="noopener noreferrer"&gt;Generador de Números Aleatorios&lt;/a&gt; — Español&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/random/vi" rel="noopener noreferrer"&gt;Trình Tạo Số Ngẫu Nhiên&lt;/a&gt; — Tiếng Việt&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/random/th" rel="noopener noreferrer"&gt;เครื่องมือสร้างเลขสุ่ม&lt;/a&gt; — ไทย&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/random/pl" rel="noopener noreferrer"&gt;Generator Liczb Losowych&lt;/a&gt; — Polski&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/random/tr" rel="noopener noreferrer"&gt;Rastgele Sayı Üretici&lt;/a&gt; — Türkçe&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/random/it" rel="noopener noreferrer"&gt;Generatore di Numeri Casuali&lt;/a&gt; — Italiano&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/random/pt" rel="noopener noreferrer"&gt;Gerador de Números Aleatórios&lt;/a&gt; — Português&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/random/nl" rel="noopener noreferrer"&gt;Willekeurige Nummergenerator&lt;/a&gt; — Nederlands&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/random/uk" rel="noopener noreferrer"&gt;Генератор Випадкових Чисел&lt;/a&gt; — Українська&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>vue</category>
      <category>algorithms</category>
    </item>
    <item>
      <title>Why "must be subscribed to enter" is a checkbox I had to delete from a YouTube giveaway picker</title>
      <dc:creator>Vera Yang</dc:creator>
      <pubDate>Thu, 06 Aug 2026 13:00:03 +0000</pubDate>
      <link>https://dev.to/begoodtool/why-must-be-subscribed-to-enter-is-a-checkbox-i-had-to-delete-from-a-youtube-giveaway-picker-1ib9</link>
      <guid>https://dev.to/begoodtool/why-must-be-subscribed-to-enter-is-a-checkbox-i-had-to-delete-from-a-youtube-giveaway-picker-1ib9</guid>
      <description>&lt;p&gt;A YouTuber I know asked me to help pick a winner from a giveaway video fairly, because doing it by scrolling through comments and eyeballing one invites exactly the kind of "the mod's friend won" accusations you'd expect. I figured: grab the comments, roll a die, done — how hard can that be? Turns out, once you actually build it against the real YouTube Data API v3 instead of imagining how it probably works, there's pagination to chase, replies to exclude, duplicate commenters to catch, and one feature I had to rip out because it's technically impossible to build honestly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning whatever URL someone pastes into a video ID
&lt;/h2&gt;

&lt;p&gt;People paste video links in three different shapes — the desktop &lt;code&gt;watch?v=&lt;/code&gt; link, the shortened &lt;code&gt;youtu.be/&lt;/code&gt; link, and the &lt;code&gt;shorts/&lt;/code&gt; link — and none of them go through a real URL parser, just string splitting on whichever marker is present:&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;searchID&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchKey&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="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;v=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;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="nx"&gt;searchID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;searchKey&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="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;v=&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="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;searchKey&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="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;youtu.be&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;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="c1"&gt;// https://youtu.be/KtkW3Hx0MvA&lt;/span&gt;
  &lt;span class="nx"&gt;searchID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;searchKey&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="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;youtu.be/&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="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;searchKey&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="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shorts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;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="c1"&gt;// https://www.youtube.com/shorts/QWUTKpZmSX0&lt;/span&gt;
  &lt;span class="nx"&gt;searchID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;searchKey&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="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shorts/&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works for a clean link, but it doesn't strip anything that comes after the ID. Paste a link with a timestamp (&lt;code&gt;&amp;amp;t=45s&lt;/code&gt;) or a share-tracking suffix (&lt;code&gt;?si=xyz&lt;/code&gt;) and that junk rides along as part of &lt;code&gt;searchID&lt;/code&gt;, gets sent straight to the video-lookup call, and comes back as a generic "couldn't find that video" error — which is technically true but tells the organizer nothing about why. I didn't catch this until someone pasted a link straight from the YouTube mobile app's share sheet, which appends a tracking param by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Paginating through the comments API without losing your place
&lt;/h2&gt;

&lt;p&gt;YouTube's &lt;code&gt;commentThreads&lt;/code&gt; endpoint hands back comments in pages of up to 100, with a &lt;code&gt;nextPageToken&lt;/code&gt; when there's more. The fetch function just calls itself with that token until there isn't one:&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;getComment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&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;isLoading&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="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;$API&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;youtubeData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;youtube_getComment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageToken&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;res&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;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&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;data&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&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;comentData&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="na"&gt;authorChannelId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;topLevelComment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorChannelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;authorDisplayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;topLevelComment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorDisplayName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;likeCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;topLevelComment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;likeCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;textOriginal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;topLevelComment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textOriginal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;totalReplyCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalReplyCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="c1"&gt;// ...a few more fields for the table&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextPageToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;getComment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextPageToken&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;isLoading&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="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="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;Notice everything is pulled from &lt;code&gt;topLevelComment.snippet&lt;/code&gt; — never the replies themselves. &lt;code&gt;totalReplyCount&lt;/code&gt; is captured just so it can be displayed in the table, but the actual reply text is never fetched. So if a giveaway's rule is "reply to the pinned comment to enter" rather than "leave a top-level comment," this tool structurally can't see those entries — it only ever eats top-level comments, which is also why the UI explicitly labels its count as "comments (excluding replies)."&lt;/p&gt;

&lt;h2&gt;
  
  
  One entry per commenter, not per comment
&lt;/h2&gt;

&lt;p&gt;Before picking a winner, every comment runs through a filter chain, and the interesting one is duplicate removal:&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;MessageFrom&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;Set&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;filterList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;comentData&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;textDisplay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;likeCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authorChannelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;publishedAt&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mustContain&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;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;textDisplay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mustContainValue&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="o"&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="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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mustLength&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;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;textDisplay&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;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;mustLengthValue&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="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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mustLike&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;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;likeCount&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;mustLikeValue&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="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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mustNoRepeat&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="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;MessageFrom&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;authorChannelId&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;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;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;MessageFrom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authorChannelId&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="c1"&gt;// ...date-range check&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;Dedup is keyed on &lt;code&gt;authorChannelId&lt;/code&gt;, not the display name — a name is just cosmetic text a user can change at any moment, so two comments from the same account could show two different names. The channel ID is the one thing that's actually stable. Since &lt;code&gt;Array.filter&lt;/code&gt; walks the list in order, whichever comment from a given channel appears first is the one that survives; any later comment from that same account is silently dropped, keyword-spam or not.&lt;/p&gt;

&lt;p&gt;The date-range check on the next line is the one piece of this function I'd flag if I were reviewing someone else's PR:&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mustDate&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;&amp;amp;&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="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;mustDateStart&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="nf"&gt;toDateString&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="o"&gt;&amp;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;publishedAt&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="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="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;mustDateEnd&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="nf"&gt;toDateString&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="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;86400000&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&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;publishedAt&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="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parentheses only wrap &lt;code&gt;mustDate.value &amp;amp;&amp;amp; startCheck&lt;/code&gt; — the end-date half of the &lt;code&gt;||&lt;/code&gt; has no such guard, so it's evaluated regardless of whether the "date range" checkbox is even ticked. In practice it's harmless today only because an empty &lt;code&gt;mustDateEnd&lt;/code&gt; produces &lt;code&gt;Invalid Date&lt;/code&gt;, and &lt;code&gt;NaN &amp;lt; anything&lt;/code&gt; is always &lt;code&gt;false&lt;/code&gt;. But it's one stray value away from silently filtering out legitimate entries the moment someone fills in an end date without checking the box first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Picking winners without replacement, the brute-force way
&lt;/h2&gt;

&lt;p&gt;Once the pool is filtered, picking N winners isn't a shuffle — it's rejection sampling:&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;real_index&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;0&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;AwardNumber&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="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;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filterList&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="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;real_index&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;now&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;real_index&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;now&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;i&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// collision — roll again&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;random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;real_max&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="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="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&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;real_max&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="mi"&gt;0&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;Roll a random index; if it's already been picked, back the loop counter up and roll again. It's not a proper Fisher–Yates partial shuffle, and it doesn't need to be — the code already checked earlier that &lt;code&gt;filterList.length &amp;gt;= AwardNumber.value&lt;/code&gt;, so the loop is guaranteed to eventually land on N distinct indexes, and for a typical giveaway (a handful of winners out of hundreds of eligible comments) collisions are rare enough that a plain retry loop is simpler to read than a real shuffle and costs nothing anyone would notice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this quietly falls short
&lt;/h2&gt;

&lt;p&gt;There's a checkbox still sitting in the template, commented out, for "must be subscribed to the channel" — with a note next to it admitting that anyone who hasn't made their subscriptions public gets treated as unsubscribed. I left it disabled on purpose: YouTube's API has no endpoint that tells you "does channel A have viewer B subscribed," because a user's subscription list is private by default and there's no way to check it from public comment data without that specific user handing over their own OAuth token. Any tool claiming to enforce a subscribe-to-enter rule automatically is either lying or quietly excluding most of your actual subscribers.&lt;/p&gt;

&lt;p&gt;A couple other honest gotchas: the comments API has a daily quota, so a video with an unusually large comment count can exhaust it mid-fetch and just stop with a generic error. And dedup by channel ID stops someone from commenting five times on one account, but does nothing to stop someone running the exact same giveaway comment from two separate Google accounts — there's no way to detect that from a comment thread alone.&lt;/p&gt;

&lt;p&gt;I cleaned this up into a small free tool if you run YouTube giveaways and want to skip building your own version: &lt;a href="https://begoodtool.com/youtube-picker/en" rel="noopener noreferrer"&gt;YouTube Comment Picker Tool&lt;/a&gt;. No sign-up, just paste a video link.&lt;/p&gt;




&lt;h2&gt;
  
  
  Available in other languages
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/youtube-picker" rel="noopener noreferrer"&gt;Youtube留言抽獎工具&lt;/a&gt; — 繁體中文&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/youtube-picker/cn" rel="noopener noreferrer"&gt;YouTube评论抽奖工具&lt;/a&gt; — 简体中文&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/youtube-picker/en" rel="noopener noreferrer"&gt;YouTube Comment Picker Tool&lt;/a&gt; — English&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/youtube-picker/jp" rel="noopener noreferrer"&gt;YouTubeコメント抽選ツール&lt;/a&gt; — 日本語&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/youtube-picker/kr" rel="noopener noreferrer"&gt;Youtube 댓글 추첨 도구&lt;/a&gt; — 한국어&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/youtube-picker/fr" rel="noopener noreferrer"&gt;Outil de tirage au sort de commentaires Youtube&lt;/a&gt; — Français&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/youtube-picker/ru" rel="noopener noreferrer"&gt;Инструмент розыгрыша комментариев на Youtube&lt;/a&gt; — Русский&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/youtube-picker/de" rel="noopener noreferrer"&gt;YouTube-Kommentar-Picker-Tool&lt;/a&gt; — Deutsch&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/youtube-picker/id" rel="noopener noreferrer"&gt;Alat Undian Komentar YouTube&lt;/a&gt; — Bahasa Indonesia&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/youtube-picker/es" rel="noopener noreferrer"&gt;Selector de Comentarios de YouTube&lt;/a&gt; — Español&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/youtube-picker/vi" rel="noopener noreferrer"&gt;Công cụ chọn bình luận YouTube&lt;/a&gt; — Tiếng Việt&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/youtube-picker/th" rel="noopener noreferrer"&gt;เครื่องมือจับรางวัลคอมเม้นต์ยูทูบ&lt;/a&gt; — ไทย&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/youtube-picker/pl" rel="noopener noreferrer"&gt;Losowanie Komentarzy YouTube&lt;/a&gt; — Polski&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/youtube-picker/tr" rel="noopener noreferrer"&gt;YouTube Yorum Çekiliş Aracı&lt;/a&gt; — Türkçe&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/youtube-picker/it" rel="noopener noreferrer"&gt;Estrattore Commenti YouTube&lt;/a&gt; — Italiano&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/youtube-picker/pt" rel="noopener noreferrer"&gt;Sorteador de Comentários do YouTube&lt;/a&gt; — Português&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/youtube-picker/nl" rel="noopener noreferrer"&gt;YouTube Reactie Verloting Tool&lt;/a&gt; — Nederlands&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/youtube-picker/uk" rel="noopener noreferrer"&gt;Інструмент для витягування коментарів на YouTube&lt;/a&gt; — Українська&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>vue</category>
      <category>api</category>
    </item>
    <item>
      <title>Why my auto-scrolling teleprompter froze at low speed (and the hidden float that fixed it)</title>
      <dc:creator>Vera Yang</dc:creator>
      <pubDate>Thu, 06 Aug 2026 07:00:02 +0000</pubDate>
      <link>https://dev.to/begoodtool/why-my-auto-scrolling-teleprompter-froze-at-low-speed-and-the-hidden-float-that-fixed-it-3e45</link>
      <guid>https://dev.to/begoodtool/why-my-auto-scrolling-teleprompter-froze-at-low-speed-and-the-hidden-float-that-fixed-it-3e45</guid>
      <description>&lt;p&gt;I built a browser-based teleprompter for a friend prepping a conference talk — paste your script, hit play, text scrolls up the screen while you read it into the camera. Simple enough. Except when she set the scroll speed slider all the way down (she's a slow, deliberate speaker), the text just... didn't move. Not slowly. Not at all. For a solid few seconds, nothing happened, then it would suddenly jump a pixel.&lt;/p&gt;

&lt;p&gt;Turned out the bug wasn't in my speed math. It was in how &lt;code&gt;scrollTop&lt;/code&gt; handles numbers smaller than 1.&lt;/p&gt;

&lt;h2&gt;
  
  
  requestAnimationFrame, not setInterval
&lt;/h2&gt;

&lt;p&gt;The scroll loop is driven by &lt;code&gt;requestAnimationFrame&lt;/code&gt;, not &lt;code&gt;setInterval&lt;/code&gt;:&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;scrollStep&lt;/span&gt; &lt;span class="o"&gt;=&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="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;isPlaying&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;teleprompterViewRef&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="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;decimalScrollTop&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;speed&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="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;teleprompterViewRef&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="nx"&gt;scrollTop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;decimalScrollTop&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// ...end-of-script check omitted here...&lt;/span&gt;

  &lt;span class="nx"&gt;animationFrameId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scrollStep&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 matters more than it looks. &lt;code&gt;setInterval(fn, 16)&lt;/code&gt; fires on a wall-clock timer regardless of whether the browser is actually ready to paint — if the tab is busy or the display can't keep up, intervals pile up and scrolling gets jerky. &lt;code&gt;requestAnimationFrame&lt;/code&gt; instead asks the browser "call me right before your next repaint," so the scroll step is always synced to the actual frame rate. For something people are reading off in real time while talking to a camera, that smoothness is the whole point of the tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decimal accumulator that makes slow speeds actually scroll
&lt;/h2&gt;

&lt;p&gt;Here's the bug I mentioned. The speed slider is 1–100, and it's turned into pixels-per-frame with a flat multiplier: &lt;code&gt;speed.value * 0.05&lt;/code&gt;. At speed 1, that's 0.05 pixels per frame. At ~60fps, that's 3 pixels per second — deliberately slow, for someone reading carefully.&lt;/p&gt;

&lt;p&gt;The problem: &lt;code&gt;element.scrollTop = 0.05&lt;/code&gt; doesn't accumulate the way you'd hope. Browsers round or coerce &lt;code&gt;scrollTop&lt;/code&gt; toward whole pixels, so if you add 0.05 to it every frame and read it back before adding again, you can lose the fractional remainder on every single frame — the number never crosses the 1px threshold, and the text never visibly moves.&lt;/p&gt;

&lt;p&gt;The fix is a variable that lives outside the DOM entirely:&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;decimalScrollTop&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;scrollStep&lt;/span&gt; &lt;span class="o"&gt;=&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;decimalScrollTop&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;speed&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="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;teleprompterViewRef&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="nx"&gt;scrollTop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;decimalScrollTop&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;decimalScrollTop&lt;/code&gt; is a plain JS float, so it keeps every fractional pixel across frames — 0.05, 0.10, 0.15... — and only the DOM write gets truncated. The DOM forgets the remainder each frame; the JS variable never does. That's the entire fix: stop trusting the browser to remember fractions you handed it, and remember them yourself.&lt;/p&gt;

&lt;p&gt;There's a second wrinkle tied to this. When you press play, the code resyncs the accumulator to wherever the element actually is first:&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;startScrolling&lt;/span&gt; &lt;span class="o"&gt;=&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;teleprompterViewRef&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="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;decimalScrollTop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;teleprompterViewRef&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="nx"&gt;scrollTop&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;isPlaying&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="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;animationFrameId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scrollStep&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;Without that line, if someone manually scrolled the view while paused (or after seeking around), hitting play would snap back to wherever the stale accumulator last was, undoing their manual scroll. It only reads from the DOM at the moment playback starts, though — while it's actively playing, &lt;code&gt;scrollStep&lt;/code&gt; overwrites &lt;code&gt;scrollTop&lt;/code&gt; every frame, which means a mouse wheel nudge mid-playback gets silently stomped on the very next frame. You can only manually reposition while paused.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mirror mode is one CSS property, applied twice
&lt;/h2&gt;

&lt;p&gt;The feature that actually makes this a "teleprompter" and not just an autoscrolling text box is mirror mode — for people using real teleprompter rigs with a beam-splitter glass in front of the camera lens, where the display underneath has to show mirrored text for it to look correct once reflected. The whole implementation is a single transform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="nc"&gt;.teleprompter-content&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nc"&gt;.mirrored&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;scaleX&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;scaleX(-1)&lt;/code&gt; flips the element horizontally around its own vertical axis, which is exactly what a reflection off glass needs. The only non-obvious part is that it has to be applied to &lt;em&gt;two&lt;/em&gt; elements independently — the script text and the countdown overlay:&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;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"countdown-overlay"&lt;/span&gt; &lt;span class="na"&gt;:class=&lt;/span&gt;&lt;span class="s"&gt;"{ mirrored: isMirrored }"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"teleprompter-content"&lt;/span&gt; &lt;span class="na"&gt;:class=&lt;/span&gt;&lt;span class="s"&gt;"{ mirrored: isMirrored }"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you only flip the content and forget the countdown, you get an unmirrored "3, 2, 1, Get Ready..." for a couple of seconds before the mirrored script kicks in — a small but very obvious continuity break if you're actually staring through a beam splitter at the time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;A few things worth knowing if you're relying on this for a real shoot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backgrounding the tab pauses scrolling.&lt;/strong&gt; Because the loop runs on &lt;code&gt;requestAnimationFrame&lt;/code&gt;, browsers throttle or fully suspend it when the tab isn't visible/focused. If your recording setup involves switching windows (e.g. controlling OBS on another monitor while this runs unfocused), the scroll can stall completely until you click back into the tab. &lt;code&gt;setInterval&lt;/code&gt; wouldn't have this problem — but it also wouldn't scroll as smoothly, which is the tradeoff.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fullscreen isn't guaranteed.&lt;/strong&gt; &lt;code&gt;startTeleprompter&lt;/code&gt; calls &lt;code&gt;requestFullscreen()&lt;/code&gt; and just logs a warning to the console if it's denied — it doesn't tell the user or fall back to anything. iOS Safari in particular doesn't support the Fullscreen API on arbitrary elements at all, so on an iPhone you'll never get a true fullscreen call to succeed. It still mostly works visually, because the teleprompter view is &lt;code&gt;position: fixed&lt;/code&gt; at &lt;code&gt;100vw&lt;/code&gt;/&lt;code&gt;100vh&lt;/code&gt; regardless of fullscreen state — but the browser's own UI chrome stays on screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The stop-at-the-end check is nested oddly.&lt;/strong&gt; It first checks if you're within 20px of the bottom, and only inside that branch checks if you're within 1px of the true max scroll before actually stopping. In practice it works fine, but it means the "are we done" logic runs two slightly different distance checks back to back rather than one clean comparison — a good reminder that shipped code doesn't have to be elegant to work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I turned this into a small free tool if you want to use it without wiring up your own &lt;code&gt;requestAnimationFrame&lt;/code&gt; loop: &lt;a href="https://begoodtool.com/online-teleprompter/en" rel="noopener noreferrer"&gt;Online Teleprompter&lt;/a&gt;. No sign-up, and your script never leaves your browser.&lt;/p&gt;




&lt;h2&gt;
  
  
  Available in other languages
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/online-teleprompter" rel="noopener noreferrer"&gt;線上提詞機&lt;/a&gt; — 繁體中文&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/online-teleprompter/cn" rel="noopener noreferrer"&gt;在线提词器&lt;/a&gt; — 简体中文&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/online-teleprompter/en" rel="noopener noreferrer"&gt;Online Teleprompter&lt;/a&gt; — English&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/online-teleprompter/jp" rel="noopener noreferrer"&gt;オンラインプロンプター&lt;/a&gt; — 日本語&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/online-teleprompter/kr" rel="noopener noreferrer"&gt;온라인 텔레프롬프터&lt;/a&gt; — 한국어&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/online-teleprompter/fr" rel="noopener noreferrer"&gt;Téléprompteur En Ligne&lt;/a&gt; — Français&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/online-teleprompter/ru" rel="noopener noreferrer"&gt;Онлайн-телесуфлер&lt;/a&gt; — Русский&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/online-teleprompter/de" rel="noopener noreferrer"&gt;Online Teleprompter&lt;/a&gt; — Deutsch&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/online-teleprompter/id" rel="noopener noreferrer"&gt;Teleprompter Online&lt;/a&gt; — Bahasa Indonesia&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/online-teleprompter/es" rel="noopener noreferrer"&gt;Teleprompter Online&lt;/a&gt; — Español&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/online-teleprompter/vi" rel="noopener noreferrer"&gt;Máy Nhắc Chữ Online&lt;/a&gt; — Tiếng Việt&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/online-teleprompter/th" rel="noopener noreferrer"&gt;Teleprompter ออนไลน์&lt;/a&gt; — ไทย&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/online-teleprompter/pl" rel="noopener noreferrer"&gt;Teleprompter Online&lt;/a&gt; — Polski&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/online-teleprompter/tr" rel="noopener noreferrer"&gt;Online Teleprompter&lt;/a&gt; — Türkçe&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/online-teleprompter/it" rel="noopener noreferrer"&gt;Teleprompter Online&lt;/a&gt; — Italiano&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/online-teleprompter/pt" rel="noopener noreferrer"&gt;Teleprompter Online&lt;/a&gt; — Português&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/online-teleprompter/nl" rel="noopener noreferrer"&gt;Online Teleprompter&lt;/a&gt; — Nederlands&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/online-teleprompter/uk" rel="noopener noreferrer"&gt;Онлайн-телесуфлер&lt;/a&gt; — Українська&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>vue</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why splitting text into sentences by punctuation breaks on "Dr. Smith" (I found out building a case converter)</title>
      <dc:creator>Vera Yang</dc:creator>
      <pubDate>Wed, 05 Aug 2026 13:00:03 +0000</pubDate>
      <link>https://dev.to/begoodtool/why-splitting-text-into-sentences-by-punctuation-breaks-on-dr-smith-i-found-out-building-a-case-5g25</link>
      <guid>https://dev.to/begoodtool/why-splitting-text-into-sentences-by-punctuation-breaks-on-dr-smith-i-found-out-building-a-case-5g25</guid>
      <description>&lt;p&gt;I added a "Sentence case" mode to a text case converter I was building — the kind of tool that turns pasted text into UPPERCASE, lowercase, Title Case, or "just capitalize the start of each sentence." The first three are one-liners. The fourth one, sentence case, is the one that quietly ate an afternoon, because "find the start of a sentence" turns out to be a much fuzzier problem than "find a period."&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;.&lt;/code&gt;split() trap I started with
&lt;/h2&gt;

&lt;p&gt;The file I was working in still has my first attempt sitting there, commented out, as a reminder of how I got it wrong:&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;// const capitalizeSentences = () =&amp;gt; {&lt;/span&gt;
&lt;span class="c1"&gt;//   outputArea.value = inputArea.value.replace(&lt;/span&gt;
&lt;span class="c1"&gt;//     /.+?[\.\?\!](\s|$)/g,&lt;/span&gt;
&lt;span class="c1"&gt;//     function (txt) {&lt;/span&gt;
&lt;span class="c1"&gt;//       return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();&lt;/span&gt;
&lt;span class="c1"&gt;//     }&lt;/span&gt;
&lt;span class="c1"&gt;//   );&lt;/span&gt;
&lt;span class="c1"&gt;// };&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks reasonable: lazily match everything up to a &lt;code&gt;.&lt;/code&gt;, &lt;code&gt;?&lt;/code&gt;, or &lt;code&gt;!&lt;/code&gt; followed by whitespace (or end of string), then capitalize the first letter and lowercase everything else. The bug is in that last part — &lt;code&gt;.toLowerCase()&lt;/code&gt; on the rest of the "sentence." Paste in "I love NASA missions. The ISS orbits every 90 minutes." and this version happily lowercases NASA and ISS along with everything else, because it doesn't know the difference between "the rest of a sentence" and "a proper noun or acronym that happens to not be first." It also had no concept of line breaks, so multi-paragraph text got flattened into a single blob-replace pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually shipped
&lt;/h2&gt;

&lt;p&gt;The version that replaced it splits the problem into two passes instead of one regex:&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;capitalizeSentences&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;inputArea&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="k"&gt;return&lt;/span&gt; &lt;span class="dl"&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;trimmedText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inputArea&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="nf"&gt;trim&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;group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;trimmedText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(?&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;=&lt;/span&gt;&lt;span class="se"&gt;\n)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;group&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;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&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;sentences&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(?&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;=&lt;/span&gt;&lt;span class="se"&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;+/&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;capitalizedSentences&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sentences&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;sentence&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;trimmedSentence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sentence&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;firstChar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;trimmedSentence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charAt&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="nf"&gt;toUpperCase&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;restOfSentence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;trimmedSentence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&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="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;firstChar&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;restOfSentence&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;return&lt;/span&gt; &lt;span class="nx"&gt;capitalizedSentences&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="s2"&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;outputArea&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;group&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="se"&gt;\n&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;Two things changed on purpose here. First, it splits on lines before splitting on sentences, using a lookbehind &lt;code&gt;(?&amp;lt;=\n)&lt;/code&gt; so the newline stays attached to the chunk before it — that keeps paragraph structure intact instead of merging everything into one line. Second, and more importantly, sentence-splitting uses &lt;code&gt;(?&amp;lt;=[.?!])\s+&lt;/code&gt; instead of a consuming match. A lookbehind checks that a period/question mark/exclamation point came right before the split point without actually consuming it, so the punctuation stays glued to the end of the sentence it belongs to instead of getting lost or duplicated.&lt;/p&gt;

&lt;p&gt;The other deliberate choice: it only touches &lt;code&gt;firstChar&lt;/code&gt;. &lt;code&gt;restOfSentence&lt;/code&gt; is used exactly as typed, no &lt;code&gt;.toLowerCase()&lt;/code&gt; anywhere. That's what fixes the NASA/ISS problem from the naive version — if a word was already capitalized correctly, sentence case just leaves it alone instead of "helpfully" re-lowercasing it. It's a much dumber piece of code than the first attempt, and that's exactly why it behaves better.&lt;/p&gt;

&lt;h2&gt;
  
  
  Title Case has a completely different bug
&lt;/h2&gt;

&lt;p&gt;Title Case (capitalize every word) doesn't need sentence boundaries at all — it just needs "word" boundaries, so it uses a much simpler regex:&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;capitalizeWords&lt;/span&gt; &lt;span class="o"&gt;=&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;outputArea&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;inputArea&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="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;\w\S&lt;/span&gt;&lt;span class="sr"&gt;*/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;txt&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;txt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charAt&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="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;txt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substr&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;toLowerCase&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;&lt;code&gt;\w\S*&lt;/code&gt; means "a word character, followed by any run of non-whitespace." That last part is the trap: &lt;code&gt;\S*&lt;/code&gt; doesn't stop at hyphens, apostrophes, or slashes, so "well-known" is matched as a single token, not two words. Capitalizing the first letter and lowercasing the rest turns it into "Well-known" instead of "Well-Known." Same story for anything with an internal capital that isn't at a word boundary a human would recognize — "NASA," for instance, becomes "Nasa," because the regex only sees one long non-whitespace run and blindly lowercases everything after its first character.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this still gets it wrong
&lt;/h2&gt;

&lt;p&gt;Even the shipped sentence-case version has real gaps, because it has no idea what an abbreviation is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Abbreviations fool it completely.&lt;/strong&gt; "He is 6 ft. tall and strong." splits into "He is 6 ft." and "tall and strong." because a period followed by a space is treated as a sentence end every time — there's no list of "Dr.", "Mr.", "etc.", "e.g." that get skipped. The output capitalizes "Tall" as if it started a new sentence, because as far as the regex is concerned, it did.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual line wraps get treated as sentence boundaries too.&lt;/strong&gt; Since each line is processed independently before being rejoined with &lt;code&gt;\n&lt;/code&gt;, a sentence that was soft-wrapped across two lines without ending punctuation still gets its second line's first letter capitalized — even though grammatically it's the middle of a sentence, not the start of one.&lt;/li&gt;
&lt;li&gt;One detail I noticed while writing localized copy for this tool: a couple of the translated marketing blurbs claim the sentence-case mode "recognizes common abbreviations (Dr., Mr., etc.) that don't end sentences." That line isn't true of the actual code above — there's no abbreviation list anywhere in it. It's a good reminder to go check the source before believing your own product description.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this makes it useless — it still saves you from manually re-capitalizing a pasted paragraph — but it's pattern-matching on punctuation, not parsing grammar, and it's worth knowing exactly where that line is.&lt;/p&gt;

&lt;p&gt;I cleaned this up into a small free tool if you want to see the four modes side by side: &lt;a href="https://begoodtool.com/uppercase/en" rel="noopener noreferrer"&gt;Online Case Converter&lt;/a&gt;. No sign-up, just paste and click.&lt;/p&gt;




&lt;h2&gt;
  
  
  Available in other languages
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/uppercase" rel="noopener noreferrer"&gt;英文大小寫轉換器&lt;/a&gt; — 繁體中文&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/uppercase/cn" rel="noopener noreferrer"&gt;在线英文大小写转换器&lt;/a&gt; — 简体中文&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/uppercase/en" rel="noopener noreferrer"&gt;Online Case Converter&lt;/a&gt; — English&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/uppercase/jp" rel="noopener noreferrer"&gt;英語大文字小文字変換ツール&lt;/a&gt; — 日本語&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/uppercase/kr" rel="noopener noreferrer"&gt;영문 대소문자 변환기&lt;/a&gt; — 한국어&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/uppercase/fr" rel="noopener noreferrer"&gt;Convertisseur de casse en anglais&lt;/a&gt; — Français&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/uppercase/ru" rel="noopener noreferrer"&gt;Конвертер регистра английского текста&lt;/a&gt; — Русский&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/uppercase/de" rel="noopener noreferrer"&gt;Online-Konverter für Groß- und Kleinschreibung&lt;/a&gt; — Deutsch&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/uppercase/id" rel="noopener noreferrer"&gt;Konverter Kasus Bahasa Inggris&lt;/a&gt; — Bahasa Indonesia&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/uppercase/es" rel="noopener noreferrer"&gt;Convertidor de mayúsculas y minúsculas en inglés&lt;/a&gt; — Español&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/uppercase/vi" rel="noopener noreferrer"&gt;Bộ chuyển đổi kiểu chữ tiếng Anh&lt;/a&gt; — Tiếng Việt&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/uppercase/th" rel="noopener noreferrer"&gt;เครื่องมือแปลงตัวพิมพ์ภาษาอังกฤษ&lt;/a&gt; — ไทย&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/uppercase/pl" rel="noopener noreferrer"&gt;Konwerter wielkości liter angielskich&lt;/a&gt; — Polski&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/uppercase/tr" rel="noopener noreferrer"&gt;İngilizce Büyük Küçük Harf Dönüştürücüsü&lt;/a&gt; — Türkçe&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/uppercase/it" rel="noopener noreferrer"&gt;Convertitore di maiuscole e minuscole inglesi&lt;/a&gt; — Italiano&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/uppercase/pt" rel="noopener noreferrer"&gt;Conversor de Maiúsculas e Minúsculas em Inglês&lt;/a&gt; — Português&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/uppercase/nl" rel="noopener noreferrer"&gt;Engelse hoofdletteromzetter&lt;/a&gt; — Nederlands&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/uppercase/uk" rel="noopener noreferrer"&gt;Конвертер регістру англійського тексту&lt;/a&gt; — Українська&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>vue</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why "the date exactly halfway between these two dates" doesn't always have one answer</title>
      <dc:creator>Vera Yang</dc:creator>
      <pubDate>Wed, 05 Aug 2026 02:00:02 +0000</pubDate>
      <link>https://dev.to/begoodtool/why-the-date-exactly-halfway-between-these-two-dates-doesnt-always-have-one-answer-22e8</link>
      <guid>https://dev.to/begoodtool/why-the-date-exactly-halfway-between-these-two-dates-doesnt-always-have-one-answer-22e8</guid>
      <description>&lt;p&gt;A friend asked me to help her find the exact midpoint between her and her partner's birthdays — apparently it's a whole thing people do, so they get a "shared" anniversary to celebrate every year. I figured it'd be a five-minute &lt;code&gt;(date1 + date2) / 2&lt;/code&gt; kind of problem. It's not. Building the date calculator that eventually answered her question, I ran into a genuinely annoying edge case: sometimes there just isn't a single day exactly in the middle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Counting days between two dates without native &lt;code&gt;Date&lt;/code&gt; math
&lt;/h2&gt;

&lt;p&gt;The tool doesn't do &lt;code&gt;new Date(a) - new Date(b)&lt;/code&gt; and divide by 86400000. It uses &lt;a href="https://moment.github.io/luxon/" rel="noopener noreferrer"&gt;Luxon&lt;/a&gt;, parses both inputs as calendar dates, and asks Luxon for a calendar-aware diff:&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;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromISO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;twoDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date1&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;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromISO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;twoDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date2&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;diffInMonths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;diff&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;days&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;betweenRes&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;diffInMonths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toObject&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That variable name is not a typo I'm inventing for the article — it's sitting in the actual source, and I only noticed it while re-reading the file for this post. It's clearly a leftover from an earlier version of the function that computed something else, and it's now permanently doing a &lt;code&gt;days&lt;/code&gt; diff while being named &lt;code&gt;diffInMonths&lt;/code&gt;. Harmless, since &lt;code&gt;.diff(start, "days")&lt;/code&gt; is unambiguous regardless of variable naming, but it's a good reminder that variable names lie the moment you stop actively maintaining them.&lt;/p&gt;

&lt;p&gt;The more deliberate design choice is letting Luxon own the calendar math instead of hand-rolling it. Raw millisecond subtraction on native &lt;code&gt;Date&lt;/code&gt; objects gets you into trouble the moment DST or timezone offsets are involved; a calendar-aware &lt;code&gt;diff("days")&lt;/code&gt; over ISO date strings just counts calendar days, which is what a "how many days between these two dates" tool actually needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fencepost question nobody asks out loud: is today "day 0" or "day 1"?
&lt;/h2&gt;

&lt;p&gt;Say two dates are 3 days apart. Is the second date "the 3rd day after" the first, or "the 4th day" if you count the first date itself as day 1? This is the classic fencepost ambiguity, and the tool resolves it by spelling out its own convention in the result text instead of leaving it implicit:&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="nx"&gt;between_res_detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;以隔天為第1天來說，{date1}過後的第{betweenRes}天，就是{data2}。&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="c1"&gt;// "Counting the day after {date1} as day 1, the {betweenRes}th day after that is {data2}."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So a 3-day gap is explicitly defined as: the day after date1 is day 1, and date2 is day 3. It's a plain diff under the hood, but the UI text exists specifically so nobody has to guess whether the tool is counting inclusively or exclusively — which is exactly the kind of ambiguity that makes people distrust a date calculator's output even when the math is correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding the midpoint: what happens when the gap is an odd number of days
&lt;/h2&gt;

&lt;p&gt;This is the part that actually stopped me. If two dates are, say, 6 days apart, the midpoint is clean: 3 days from either end. But if they're 7 days apart, dividing by 2 gives 3.5 — there is no calendar day at "3.5 days from the start." The tool handles this by branching on parity and returning a range instead of a single date when the gap is odd:&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;days&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;diffInMonths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toObject&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="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;%&lt;/span&gt; &lt;span class="mi"&gt;2&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="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// even gap: one exact midpoint&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;middle&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;abs&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="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;middleRes&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;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="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;middle&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;toFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yyyy-MM-dd (ccc)&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;start&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;middle&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;toFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yyyy-MM-dd (ccc)&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;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// odd gap: midpoint falls between two candidate days&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;middle1&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;abs&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="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;2&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;middle2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;middle1&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;middleRes&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;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="s2"&gt;`&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="nf"&gt;plus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;middle1&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;toFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yyyy-MM-dd (ccc)&lt;/span&gt;&lt;span class="dl"&gt;"&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;end&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;middle2&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;toFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yyyy-MM-dd (ccc)&lt;/span&gt;&lt;span class="dl"&gt;"&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="s2"&gt;`&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="nf"&gt;plus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;middle1&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;toFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yyyy-MM-dd (ccc)&lt;/span&gt;&lt;span class="dl"&gt;"&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;start&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plus&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;middle2&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;toFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yyyy-MM-dd (ccc)&lt;/span&gt;&lt;span class="dl"&gt;"&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;Even gap → one exact answer. Odd gap → two adjacent dates shown as a range, because both are equally "the middle" and picking one over the other would just be arbitrary. The &lt;code&gt;days &amp;lt; 0&lt;/code&gt; branch also handles a detail that's easy to overlook: the user isn't required to type the earlier date into the first field, so the function figures out which of &lt;code&gt;start&lt;/code&gt;/&lt;code&gt;end&lt;/code&gt; is actually earlier before it adds the offset, rather than assuming input order. The same input-order tolerance shows up in the days-between result too — before display, the component swaps which date is shown first based on the sign of the raw diff, so &lt;code&gt;date1&lt;/code&gt; and &lt;code&gt;date2&lt;/code&gt; in the output text always resolve to the chronologically earlier and later date, regardless of which one the user typed first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this falls short
&lt;/h2&gt;

&lt;p&gt;A few honest limitations, since none of this is magic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The single-date calculator only adds or subtracts a plain count of &lt;strong&gt;days&lt;/strong&gt; — there's no "add 1 month" or "add 1 year" option. If you want a month from now, you have to already know how many days that is (28–31, depending), the tool won't do that translation for you.&lt;/li&gt;
&lt;li&gt;The date inputs allow anything up to year 9999 with no realistic lower bound. Luxon will happily compute a diff against a date in year 1, but that's applying the proleptic Gregorian calendar backwards past 1582, when the Gregorian calendar didn't actually exist yet — the math is internally consistent, it just isn't "historically real" that far back.&lt;/li&gt;
&lt;li&gt;The "at least 1" rule on the day-count input for the single-date calculator is enforced by a UI hint and a truthy check (&lt;code&gt;!inferNumber.value&lt;/code&gt;), not a strict numeric range check — it stops empty/zero input but isn't the same as validating that the value is a genuine positive integer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are bugs exactly, just the edges you hit once you stop treating "days between two dates" as a solved problem.&lt;/p&gt;

&lt;p&gt;I cleaned up the version I built for my friend's shared-birthday math into a small free tool: &lt;a href="https://begoodtool.com/dateCalculator/en" rel="noopener noreferrer"&gt;Online Date Calculator&lt;/a&gt;. No sign-up, and yes, it shows the range when the midpoint lands between two days instead of quietly rounding.&lt;/p&gt;




&lt;h2&gt;
  
  
  Available in other languages
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/dateCalculator" rel="noopener noreferrer"&gt;日期計算機&lt;/a&gt; — 繁體中文&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/dateCalculator/cn" rel="noopener noreferrer"&gt;在线日期计算器&lt;/a&gt; — 简体中文&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/dateCalculator/en" rel="noopener noreferrer"&gt;Online Date Calculator&lt;/a&gt; — English&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/dateCalculator/jp" rel="noopener noreferrer"&gt;オンライン日付計算ツール&lt;/a&gt; — 日本語&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/dateCalculator/kr" rel="noopener noreferrer"&gt;온라인 날짜 계산기&lt;/a&gt; — 한국어&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/dateCalculator/fr" rel="noopener noreferrer"&gt;Calculateur de Date en Ligne&lt;/a&gt; — Français&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/dateCalculator/ru" rel="noopener noreferrer"&gt;Онлайн Калькулятор Дат&lt;/a&gt; — Русский&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/dateCalculator/de" rel="noopener noreferrer"&gt;Online Datumsrechner&lt;/a&gt; — Deutsch&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/dateCalculator/id" rel="noopener noreferrer"&gt;Kalkulator Tanggal Online&lt;/a&gt; — Bahasa Indonesia&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/dateCalculator/es" rel="noopener noreferrer"&gt;Calculadora de Fechas Online&lt;/a&gt; — Español&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/dateCalculator/vi" rel="noopener noreferrer"&gt;Máy Tính Ngày Trực Tuyến&lt;/a&gt; — Tiếng Việt&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/dateCalculator/th" rel="noopener noreferrer"&gt;เครื่องคิดเลขวันที่ออนไลน์&lt;/a&gt; — ไทย&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/dateCalculator/pl" rel="noopener noreferrer"&gt;Internetowy Kalkulator Dat&lt;/a&gt; — Polski&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/dateCalculator/tr" rel="noopener noreferrer"&gt;Çevrimiçi Tarih Hesaplayıcı&lt;/a&gt; — Türkçe&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/dateCalculator/it" rel="noopener noreferrer"&gt;Calcolatore di Date Online&lt;/a&gt; — Italiano&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/dateCalculator/pt" rel="noopener noreferrer"&gt;Calculadora de Datas Online&lt;/a&gt; — Português&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/dateCalculator/nl" rel="noopener noreferrer"&gt;Online Datumcalculator&lt;/a&gt; — Nederlands&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/dateCalculator/uk" rel="noopener noreferrer"&gt;Онлайн Калькулятор Дат&lt;/a&gt; — Українська&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why `new Date(garbage) === "Invalid Date"` is always false (a timestamp converter taught me this the hard way)</title>
      <dc:creator>Vera Yang</dc:creator>
      <pubDate>Tue, 04 Aug 2026 07:00:01 +0000</pubDate>
      <link>https://dev.to/begoodtool/why-new-dategarbage-invalid-date-is-always-false-a-timestamp-converter-taught-me-this-362</link>
      <guid>https://dev.to/begoodtool/why-new-dategarbage-invalid-date-is-always-false-a-timestamp-converter-taught-me-this-362</guid>
      <description>&lt;p&gt;I built a small Unix timestamp converter — paste in seconds or milliseconds, get a date back, or go the other way. Simple enough that I figured the only real work would be the date math. Then I went to add "tell the user when they typed garbage" and ran straight into a JavaScript gotcha that's been quietly living in more codebases than anyone wants to admit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The unit dropdown is secretly a multiplier, not a branch
&lt;/h2&gt;

&lt;p&gt;The converter has a unit selector next to the input — "Seconds" or "Milliseconds." The tempting way to wire that up is an if/else: if seconds, do this math; if milliseconds, do that math. That's not what's in the actual component. The &lt;code&gt;&amp;lt;a-select&amp;gt;&lt;/code&gt; options are bound directly to the numbers &lt;code&gt;1&lt;/code&gt; and &lt;code&gt;1000&lt;/code&gt;, and those numbers get used as literal arithmetic operands everywhere downstream:&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;unixToNoraml_getOutput&lt;/span&gt; &lt;span class="o"&gt;=&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;let&lt;/span&gt; &lt;span class="nx"&gt;unix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unixToNoraml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unix&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;unit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unixToNoraml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 1 or 1000&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;output&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="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;unix&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;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unixToNoraml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;unit&lt;/code&gt; is &lt;code&gt;1&lt;/code&gt; (seconds), that's &lt;code&gt;unix * 1000 / 1&lt;/code&gt; — converts seconds to the milliseconds &lt;code&gt;Date&lt;/code&gt; wants. If &lt;code&gt;unit&lt;/code&gt; is &lt;code&gt;1000&lt;/code&gt; (milliseconds), it's &lt;code&gt;unix * 1000 / 1000&lt;/code&gt;, which cancels out and leaves the value untouched, because it was already in milliseconds. Same one-liner handles both cases with no conditional at all.&lt;/p&gt;

&lt;p&gt;The reverse direction does the same trick in reverse — compute the answer in seconds, then multiply by the unit to get either seconds or milliseconds back out:&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;normalToUnixOutputFormat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&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="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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;normalToUnix&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="k"&gt;return&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="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;normalToUnix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;normalToUnix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unit&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;It's a small thing, but reusing the dropdown's raw value as a multiplier instead of introducing a &lt;code&gt;"seconds" | "milliseconds"&lt;/code&gt; enum saved a branch in four separate places in the component.&lt;/p&gt;

&lt;h2&gt;
  
  
  The validation check that doesn't actually validate anything
&lt;/h2&gt;

&lt;p&gt;Here's the one that got me. When you type a date string into the "normal time" field and hit convert, the code tries to reject bad input before doing math on it:&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;if &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;normal&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="s2"&gt;Invalid Date&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;return&lt;/span&gt; &lt;span class="nx"&gt;Swal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fire&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="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timestamp.error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;warning&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;confirmButtonColor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#1890ff&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;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;output&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;normal&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="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;&lt;code&gt;new Date("not a real date")&lt;/code&gt; doesn't throw and doesn't return the string &lt;code&gt;"Invalid Date"&lt;/code&gt; — it returns an actual &lt;code&gt;Date&lt;/code&gt; object whose internal time value is &lt;code&gt;NaN&lt;/code&gt;. Calling &lt;code&gt;.toString()&lt;/code&gt; on that object &lt;em&gt;prints&lt;/em&gt; &lt;code&gt;"Invalid Date"&lt;/code&gt;, but the object itself is never, ever &lt;code&gt;===&lt;/code&gt; to a string, no matter what garbage you feed it. So that guard clause is dead code. It will never fire, for any input, ever.&lt;/p&gt;

&lt;p&gt;What actually keeps the tool from displaying literal "NaN" to the user is something else entirely, three lines away in a computed property:&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;normalToUnixOutputFormat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&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="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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;normalToUnix&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="k"&gt;return&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="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;normalToUnix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;normalToUnix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unit&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;NaN / 1000&lt;/code&gt; is still &lt;code&gt;NaN&lt;/code&gt;, and &lt;code&gt;!NaN&lt;/code&gt; evaluates to &lt;code&gt;true&lt;/code&gt; in JavaScript — so the falsy check blanks the output field instead of the intended validation ever running. The tool behaves correctly, but by accident: the real safety net is a coincidental side effect of how &lt;code&gt;NaN&lt;/code&gt; interacts with &lt;code&gt;!&lt;/code&gt;, not the &lt;code&gt;Swal.fire()&lt;/code&gt; warning dialog that was written to handle exactly this case. The correct check would be &lt;code&gt;isNaN(new Date(normal).getTime())&lt;/code&gt;, but that's not what's in the file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The live clock ticks in your local time, not UTC
&lt;/h2&gt;

&lt;p&gt;The page also has a running "current timestamp" display that updates once a second, with pause/continue buttons:&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;init_clock&lt;/span&gt; &lt;span class="o"&gt;=&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timer&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;now&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentUnix&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;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&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="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentLocalString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentUnix&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yyyy/MM/dd HH:mm:ss&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;span class="mi"&gt;1000&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;pause_clock&lt;/span&gt; &lt;span class="o"&gt;=&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="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&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;Two things worth calling out. First, &lt;code&gt;setInterval(..., 1000)&lt;/code&gt; doesn't guarantee a tick every exact second — it just re-reads &lt;code&gt;Date.now()&lt;/code&gt; and rounds each time it fires, so under heavy main-thread load a tick can land late; the displayed number just catches up rather than drifting permanently. Second, &lt;code&gt;DateTime.fromSeconds()&lt;/code&gt; from Luxon defaults to the &lt;em&gt;local system time zone&lt;/em&gt; when you don't pass one — so the human-readable clock next to the raw Unix number is your machine's local time, not UTC, even though a Unix timestamp is by definition timezone-independent. There's no UTC toggle for it. If you're in Tokyo and I'm in Berlin, we see the same integer but a different clock face next to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations, honestly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No digit-count auto-detection.&lt;/strong&gt; I assumed a paste-in converter like this would sniff "10 digits, must be seconds" vs "13 digits, must be milliseconds." It doesn't — the unit is a manual dropdown. Paste a 13-digit millisecond value while "Seconds" is still selected and you silently get a date several thousand years in the future. No warning, no clamp.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything renders in local time.&lt;/strong&gt; The seconds-since-epoch value is timezone-agnostic by definition, but every human-readable output on the page — the live clock, the converted date — is displayed in whatever timezone the browser is set to, with no UTC option. If you're debugging a server log recorded in UTC, you have to do the offset math yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The "type a date manually" field parses whatever &lt;code&gt;new Date(string)&lt;/code&gt; accepts&lt;/strong&gt;, which is a browser-implementation detail, not a fully spec'd format. It works fine for the &lt;code&gt;YYYY/MM/DD HH:MM:SS&lt;/code&gt; shape the placeholder suggests, but it's not the same guarantee you'd get from an explicit parser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Invalid Date check discussed above never runs.&lt;/strong&gt; It's harmless here because &lt;code&gt;NaN&lt;/code&gt;'s falsiness happens to save it, but it means the warning dialog telling you "please enter a valid time" is unreachable code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I turned the cleaned-up version into a small free tool: &lt;a href="https://begoodtool.com/timestamp/en" rel="noopener noreferrer"&gt;Unix Timestamp Converter&lt;/a&gt;. No sign-up, works entirely in the browser.&lt;/p&gt;




&lt;h2&gt;
  
  
  Available in other languages
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/timestamp" rel="noopener noreferrer"&gt;Unix時間戳轉換工具&lt;/a&gt; — 繁體中文&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/timestamp/cn" rel="noopener noreferrer"&gt;Unix时间戳转换器&lt;/a&gt; — 简体中文&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/timestamp/en" rel="noopener noreferrer"&gt;Unix Timestamp Converter&lt;/a&gt; — English&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/timestamp/jp" rel="noopener noreferrer"&gt;Unixタイムスタンプ変換ツール&lt;/a&gt; — 日本語&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/timestamp/kr" rel="noopener noreferrer"&gt;Unix 타임스탬프 변환기&lt;/a&gt; — 한국어&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/timestamp/fr" rel="noopener noreferrer"&gt;Outil de Conversion de Timestamp Unix&lt;/a&gt; — Français&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/timestamp/ru" rel="noopener noreferrer"&gt;Конвертер Unix Timestamp&lt;/a&gt; — Русский&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/timestamp/de" rel="noopener noreferrer"&gt;Unix-Zeitstempel-Konverter&lt;/a&gt; — Deutsch&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/timestamp/id" rel="noopener noreferrer"&gt;Alat Konversi Unix Timestamp&lt;/a&gt; — Bahasa Indonesia&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/timestamp/es" rel="noopener noreferrer"&gt;Conversor de Unix Timestamp&lt;/a&gt; — Español&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/timestamp/vi" rel="noopener noreferrer"&gt;Công cụ chuyển đổi Unix Timestamp&lt;/a&gt; — Tiếng Việt&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/timestamp/th" rel="noopener noreferrer"&gt;เครื่องมือแปลง Unix Timestamp&lt;/a&gt; — ไทย&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/timestamp/pl" rel="noopener noreferrer"&gt;Konwerter znacznika czasu Unix&lt;/a&gt; — Polski&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/timestamp/tr" rel="noopener noreferrer"&gt;Unix Timestamp Dönüştürme Aracı&lt;/a&gt; — Türkçe&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/timestamp/it" rel="noopener noreferrer"&gt;Strumento di conversione Unix Timestamp&lt;/a&gt; — Italiano&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/timestamp/pt" rel="noopener noreferrer"&gt;Conversor de Unix Timestamp&lt;/a&gt; — Português&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/timestamp/nl" rel="noopener noreferrer"&gt;Unix Timestamp Conversie Tool&lt;/a&gt; — Nederlands&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/timestamp/uk" rel="noopener noreferrer"&gt;Інструмент конвертації Unix Timestamp&lt;/a&gt; — Українська&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I built a calorie lookup table and realized the search box was already a category filter</title>
      <dc:creator>Vera Yang</dc:creator>
      <pubDate>Tue, 04 Aug 2026 02:00:04 +0000</pubDate>
      <link>https://dev.to/begoodtool/i-built-a-calorie-lookup-table-and-realized-the-search-box-was-already-a-category-filter-4n9p</link>
      <guid>https://dev.to/begoodtool/i-built-a-calorie-lookup-table-and-realized-the-search-box-was-already-a-category-filter-4n9p</guid>
      <description>&lt;p&gt;A while back I wanted a fast way to answer "how many calories are in this" while eating instead of after — no app to install, no account, just type the food and see a number. I figured I'd build a small searchable table over a list of common foods. There's no clever algorithm hiding in something like this. It's a filtered array rendered as rows. What actually took the time was the boring stuff: how do you store "1 bowl (200g)" as data, what happens when someone searches for a category instead of a food, and whether the number you show is even the number you meant to show.&lt;/p&gt;

&lt;h2&gt;
  
  
  The search box already does more than the category dropdown
&lt;/h2&gt;

&lt;p&gt;The table has both a text search and a category dropdown (Staple Food, Fruit, Drinks, Vegetable, Seafood, Meat, Egg). I built them as two separate filters, applied one after the other:&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;showTable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&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;let&lt;/span&gt; &lt;span class="nx"&gt;searchResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;foodCalories&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="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&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="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;groupName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keyword&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="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&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;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;groupName_en&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keyword&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="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&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;item&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;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keyword&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="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&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;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name_en&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keyword&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="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;&amp;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;searchResult&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;item&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;showGroup&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;All&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;true&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;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;groupName_en&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nx"&gt;showGroup&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="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 text filter checks four fields per item, not one: the Chinese name, the English name, the Chinese category, and the English category. I added the category fields mostly so a &lt;code&gt;groupName_en&lt;/code&gt; typo somewhere wouldn't make an item unfindable. What I didn't plan for is that typing "fruit" into the search box now returns every fruit in the table, with zero interaction with the dropdown next to it — the dropdown and the search box quietly do the same job through two different UI elements. It's not a bug exactly, it's just a filter that's wider than the label on the box suggests. Nobody's complained, but if I rebuilt this I'd probably narrow the text search to just the name fields and let the dropdown own category filtering exclusively.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calories are stored as a string, and that decision has a shelf life
&lt;/h2&gt;

&lt;p&gt;Each row looks like this:&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;name&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="nx"&gt;name_en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;White rice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1碗&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;unit_en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1 bowl&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;200g&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;calories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;225&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;groupName&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="nx"&gt;groupName_en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Staple Food&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;&lt;code&gt;calories&lt;/code&gt; is a string, &lt;code&gt;"225"&lt;/code&gt;, not the number &lt;code&gt;225&lt;/code&gt;. That was fine as long as all I ever did with it was print it next to the name. The table has no sort feature today, and that's exactly why it's gotten away with this so far — &lt;code&gt;"9"&lt;/code&gt; sorting after &lt;code&gt;"80"&lt;/code&gt; because &lt;code&gt;"9" &amp;gt; "8"&lt;/code&gt; lexicographically only matters the moment you try to sort. The day I add "sort by calories," every row with a triple-digit value will need &lt;code&gt;parseInt&lt;/code&gt; first, or the ordering will look randomly wrong to anyone who doesn't know why. I left it as a string because the data came from a spreadsheet of copy-pasted nutrition labels, and normalizing every value to a real number felt like busywork for a feature that didn't exist yet. That's a fair trade until it isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  A &lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt; your browser never actually shows you
&lt;/h2&gt;

&lt;p&gt;The template renders the food data twice. Once as a real HTML &lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt; with &lt;code&gt;&amp;lt;thead&amp;gt;&lt;/code&gt;/&lt;code&gt;&amp;lt;tbody&amp;gt;&lt;/code&gt;, and once as a grid of styled &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;s that's the thing you actually see and interact with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="nc"&gt;.foodCalories__table&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;height&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;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&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 semantic table exists purely for crawlers and screen readers — search engines and assistive tech get proper table markup with real headers, while the visible UI is a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; grid because that's what let me do per-cell click-to-copy, a font-size toggle, and a mobile layout that reflows without fighting &lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt; layout rules. Two representations of the same 300+ rows, kept in sync only because they're both generated from the same &lt;code&gt;v-for&lt;/code&gt; over the same array. If I ever changed one loop without the other, they'd silently drift apart with no error to catch it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the numbers don't quite line up
&lt;/h2&gt;

&lt;p&gt;The honest gotchas, since none of this is as tidy as it looks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Click-to-copy doesn't always match what's on screen.&lt;/strong&gt; The visible unit column shows the Chinese unit for both &lt;code&gt;tw&lt;/code&gt; and &lt;code&gt;cn&lt;/code&gt; locales (&lt;code&gt;locale == "tw" || locale == "cn" ? item.unit : item.unit_en&lt;/code&gt;), but the click-to-copy handler for that same cell only checks &lt;code&gt;locale == "tw"&lt;/code&gt;. A &lt;code&gt;cn&lt;/code&gt;-locale user sees "1碗" on screen and copies "1 bowl" to their clipboard. Small, easy to miss, and exactly the kind of thing that survives for years in a component nobody re-reads line by line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calories are per labeled serving, not per 100g&lt;/strong&gt;, and the serving unit and its gram weight are just two adjacent strings (&lt;code&gt;unit&lt;/code&gt;, &lt;code&gt;weight&lt;/code&gt;) glued together for display — &lt;code&gt;"1碗（200g）"&lt;/code&gt; — with no actual unit conversion behind them. Comparing two foods with different serving sizes means doing the per-gram math yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The category dropdown is a hand-written list&lt;/strong&gt;, separate from the actual &lt;code&gt;groupName_en&lt;/code&gt; values in the data file. If a future data update adds a new category, it's searchable by text immediately but invisible in the dropdown until someone remembers to add it there too.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that makes the table wrong, exactly — it makes it a spreadsheet with a search box, and it's worth knowing that's what you're looking at before you trust it for anything more precise than "roughly how much is this."&lt;/p&gt;

&lt;p&gt;I cleaned this up into a small free tool if you want to poke at the real thing: &lt;a href="https://begoodtool.com/food-calories/en" rel="noopener noreferrer"&gt;Common Food Calorie Table&lt;/a&gt;. No sign-up, just search and filter over 300+ foods.&lt;/p&gt;




&lt;h2&gt;
  
  
  Available in other languages
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/food-calories" rel="noopener noreferrer"&gt;常見食物熱量表&lt;/a&gt; — 繁體中文&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/food-calories/cn" rel="noopener noreferrer"&gt;常见食物热量表&lt;/a&gt; — 简体中文&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/food-calories/en" rel="noopener noreferrer"&gt;Common Food Calorie Table&lt;/a&gt; — English&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/food-calories/jp" rel="noopener noreferrer"&gt;食品カロリー表（300種類以上）&lt;/a&gt; — 日本語&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/food-calories/kr" rel="noopener noreferrer"&gt;식품 칼로리 표&lt;/a&gt; — 한국어&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/food-calories/fr" rel="noopener noreferrer"&gt;Tableau des Calories des Aliments Courants&lt;/a&gt; — Français&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/food-calories/ru" rel="noopener noreferrer"&gt;Таблица калорийности продуктов&lt;/a&gt; — Русский&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/food-calories/de" rel="noopener noreferrer"&gt;Kalorientabelle für gängige Lebensmittel&lt;/a&gt; — Deutsch&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/food-calories/id" rel="noopener noreferrer"&gt;Tabel Kalori Makanan Umum&lt;/a&gt; — Bahasa Indonesia&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/food-calories/es" rel="noopener noreferrer"&gt;Tabla de calorías de alimentos comunes&lt;/a&gt; — Español&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/food-calories/vi" rel="noopener noreferrer"&gt;Bảng calo thực phẩm thông thường&lt;/a&gt; — Tiếng Việt&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/food-calories/th" rel="noopener noreferrer"&gt;ตารางแคลอรี่อาหารทั่วไป&lt;/a&gt; — ไทย&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/food-calories/pl" rel="noopener noreferrer"&gt;Tabela kalorii popularnych produktów spożywczych&lt;/a&gt; — Polski&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/food-calories/tr" rel="noopener noreferrer"&gt;Yaygın Yiyecek Kalori Tablosu&lt;/a&gt; — Türkçe&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/food-calories/it" rel="noopener noreferrer"&gt;Tabella delle calorie dei cibi comuni&lt;/a&gt; — Italiano&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/food-calories/pt" rel="noopener noreferrer"&gt;Tabela de calorias de alimentos comuns&lt;/a&gt; — Português&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/food-calories/nl" rel="noopener noreferrer"&gt;Tabel met calorieën van veelvoorkomende voedingsmiddelen&lt;/a&gt; — Nederlands&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/food-calories/uk" rel="noopener noreferrer"&gt;Таблиця калорійності популярних продуктів&lt;/a&gt; — Українська&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why three "characters" in my HTML symbol table aren't actually there</title>
      <dc:creator>Vera Yang</dc:creator>
      <pubDate>Mon, 03 Aug 2026 07:00:01 +0000</pubDate>
      <link>https://dev.to/begoodtool/why-three-characters-in-my-html-symbol-table-arent-actually-there-5f4</link>
      <guid>https://dev.to/begoodtool/why-three-characters-in-my-html-symbol-table-arent-actually-there-5f4</guid>
      <description>&lt;p&gt;I built a click-to-copy table of HTML special characters — the kind of reference page you land on from a Google search when you need &lt;code&gt;&amp;amp;mdash;&lt;/code&gt; or &lt;code&gt;&amp;amp;copy;&lt;/code&gt; and don't want to remember the numeric code. I figured the hard part would be assembling a few hundred symbols. It wasn't. The hard part was three rows where the "symbol" you're supposed to click on is invisible, plus a search box that only works if you already know what you're looking for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The table is a flat array, not a category system
&lt;/h2&gt;

&lt;p&gt;The marketing copy on the page talks about symbols being "neatly organized by category" — arrows, currency, math, Greek letters. In the actual data file, there's no category field anywhere. It's one flat array of ~360 plain 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;let&lt;/span&gt; &lt;span class="nx"&gt;table&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;symbol&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;Alpha;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;#913;&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;symbol&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;larr;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;#8592;&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;symbol&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;cent;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;#162;&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;symbol&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;copy;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;#169;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ...roughly 360 of these&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "categories" you notice while scrolling are just an artifact of the order the list was assembled in — Greek letters cluster because whoever built the table pasted them in as a block, not because there's a &lt;code&gt;category: "greek"&lt;/code&gt; key anywhere. Filtering is a plain substring match across all three 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;let&lt;/span&gt; &lt;span class="nx"&gt;showTable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;htmlsymbols&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="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&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="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keyword&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="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&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;item&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;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keyword&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="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&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;item&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="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keyword&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="o"&gt;&amp;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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which means typing "arr" finds &lt;code&gt;&amp;amp;larr;&lt;/code&gt;, &lt;code&gt;&amp;amp;rarr;&lt;/code&gt;, &lt;code&gt;&amp;amp;uarr;&lt;/code&gt; — because those entity names literally contain "arr" — but typing "arrow" finds nothing, because the string "arrow" doesn't appear anywhere in the data. There's no synonym layer, no description field, no fuzzy matching. The search only works if you already half-know HTML entity naming conventions, which is a little backwards for a tool aimed at people who don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making invisible characters clickable
&lt;/h2&gt;

&lt;p&gt;Three rows in the table are spaces — &lt;code&gt;&amp;amp;ensp;&lt;/code&gt;, &lt;code&gt;&amp;amp;emsp;&lt;/code&gt;, &lt;code&gt;&amp;amp;nbsp;&lt;/code&gt; — and a plain space rendered in a table cell gives you nothing to click on. No visual weight, no hover target you'd notice, nothing. So those three rows carry an extra field the rest of the table doesn't have:&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;symbol&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="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;ensp;&lt;/span&gt;&lt;span class="dl"&gt;"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;#8194;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rmk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en space&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;symbol&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;emsp;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;#8195;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;rmk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;em space&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;symbol&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;nbsp;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;#160;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;rmk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;non-breaking&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;And the template checks for it before deciding what to display:&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;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"tableGroup__item__symbol"&lt;/span&gt; &lt;span class="na"&gt;:class=&lt;/span&gt;&lt;span class="s"&gt;"{ canCopy: item.symbol }"&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"copy(item.symbol)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  {{ item.rmk ? `(${item.rmk})` : item.symbol || " " }}
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The label you see is &lt;code&gt;(non-breaking)&lt;/code&gt;, but the click handler is still wired to &lt;code&gt;item.symbol&lt;/code&gt; — the actual invisible space character, not the label text. So the displayed text and the copied text are deliberately different things. It's a small trick, but it's the only reason those three rows are usable at all instead of being dead, empty-looking cells nobody would think to click.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not every cell is copyable, and one code is just wrong
&lt;/h2&gt;

&lt;p&gt;Each of the three columns — symbol, entity name, decimal code — copies independently, and the &lt;code&gt;canCopy&lt;/code&gt; styling (and the click handler itself) is gated on the value actually being truthy:&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;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&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;content&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="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;content&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&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="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;message.copySuccess&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;copySuccess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;content&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 &lt;code&gt;res.value&lt;/code&gt; fallback in the ternary is dead code — &lt;code&gt;res&lt;/code&gt; isn't defined anywhere in this component, and it's unreachable anyway because the &lt;code&gt;if (!content) return;&lt;/code&gt; guard already exits before you'd get there. Looks like a leftover from a copy-pasted pattern used in one of the other tools on the site that never got cleaned up here; harmless, but a good reminder that "it works" isn't the same as "every line does something."&lt;/p&gt;

&lt;p&gt;The guard matters for real data, though — the apostrophe row has no named entity at all:&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;symbol&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="nx"&gt;name&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="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;#39;&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;&lt;code&gt;name&lt;/code&gt; is an empty string, so that cell never gets the &lt;code&gt;canCopy&lt;/code&gt; class and clicking it does nothing — correctly, since HTML never defined a named entity for a plain apostrophe.&lt;/p&gt;

&lt;p&gt;There's also a plain typo sitting in the data: the micro sign's decimal code is written &lt;code&gt;&amp;amp;#181&lt;/code&gt; instead of &lt;code&gt;&amp;amp;#181;&lt;/code&gt; — missing the closing semicolon that every other row in the table has. Copy that one into an HTML document and depending on what follows it in your markup, it may not render as &lt;code&gt;µ&lt;/code&gt; at all, because the semicolon terminator got dropped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this actually falls short
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Search is pure substring matching on exactly what's stored — no semantic search, no "symbols that look like an X."&lt;/li&gt;
&lt;li&gt;The "clean categories" description in the page copy doesn't match the underlying data structure — there's no category field to filter by, just scroll order.&lt;/li&gt;
&lt;li&gt;It only covers the classic HTML4/Latin-1/general-punctuation/Greek/math/arrows range — no emoji, no CJK punctuation, no anything from more recent Unicode blocks.&lt;/li&gt;
&lt;li&gt;The invisible-character trick (&lt;code&gt;rmk&lt;/code&gt;) only exists for three rows; any other zero-width or hard-to-see character added later would need the same manual override or it'd be effectively unclickable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I cleaned up the version I use into a small free tool if you need to grab an entity code without digging through W3Schools tables: &lt;a href="https://begoodtool.com/htmlsymbols/en" rel="noopener noreferrer"&gt;HTML Symbol Entity Reference&lt;/a&gt;. No sign-up, click any cell to copy it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Available in other languages
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/htmlsymbols" rel="noopener noreferrer"&gt;HTML特殊符號對照表&lt;/a&gt; — 繁體中文&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/htmlsymbols/cn" rel="noopener noreferrer"&gt;HTML特殊符号对照表&lt;/a&gt; — 简体中文&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/htmlsymbols/en" rel="noopener noreferrer"&gt;HTML Symbol Entity Reference&lt;/a&gt; — English&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/htmlsymbols/jp" rel="noopener noreferrer"&gt;HTML特殊記号対照表&lt;/a&gt; — 日本語&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/htmlsymbols/kr" rel="noopener noreferrer"&gt;HTML 특수문자 코드 대조표&lt;/a&gt; — 한국어&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/htmlsymbols/fr" rel="noopener noreferrer"&gt;Tableau de référence des entités HTML&lt;/a&gt; — Français&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/htmlsymbols/ru" rel="noopener noreferrer"&gt;Справочник спецсимволов HTML&lt;/a&gt; — Русский&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/htmlsymbols/de" rel="noopener noreferrer"&gt;HTML-Sonderzeichenreferenz&lt;/a&gt; — Deutsch&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/htmlsymbols/id" rel="noopener noreferrer"&gt;Tabel Referensi Simbol HTML&lt;/a&gt; — Bahasa Indonesia&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/htmlsymbols/es" rel="noopener noreferrer"&gt;Tabla de símbolos HTML&lt;/a&gt; — Español&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/htmlsymbols/vi" rel="noopener noreferrer"&gt;Bảng ký tự đặc biệt HTML&lt;/a&gt; — Tiếng Việt&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/htmlsymbols/th" rel="noopener noreferrer"&gt;ตารางอ้างอิงสัญลักษณ์ HTML&lt;/a&gt; — ไทย&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/htmlsymbols/pl" rel="noopener noreferrer"&gt;Tabela znaków specjalnych HTML&lt;/a&gt; — Polski&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/htmlsymbols/tr" rel="noopener noreferrer"&gt;HTML Özel Karakterler Referansı&lt;/a&gt; — Türkçe&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/htmlsymbols/it" rel="noopener noreferrer"&gt;Tabella di riferimento simboli HTML&lt;/a&gt; — Italiano&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/htmlsymbols/pt" rel="noopener noreferrer"&gt;Tabela de Símbolos HTML&lt;/a&gt; — Português&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/htmlsymbols/nl" rel="noopener noreferrer"&gt;Tabel met HTML Speciale Tekens&lt;/a&gt; — Nederlands&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/htmlsymbols/uk" rel="noopener noreferrer"&gt;Довідник символів HTML&lt;/a&gt; — Українська&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why "business days between two dates" isn't a division problem (I only saw it clearly building a workday calculator)</title>
      <dc:creator>Vera Yang</dc:creator>
      <pubDate>Mon, 03 Aug 2026 02:00:02 +0000</pubDate>
      <link>https://dev.to/begoodtool/why-business-days-between-two-dates-isnt-a-division-problem-i-only-saw-it-clearly-building-a-49h9</link>
      <guid>https://dev.to/begoodtool/why-business-days-between-two-dates-isnt-a-division-problem-i-only-saw-it-clearly-building-a-49h9</guid>
      <description>&lt;p&gt;A while back I needed to know how many working days I had left before a deadline. My first instinct was: total days divided by 7, times 5, done. That works fine as long as your date range happens to be a whole number of weeks. The moment you have leftover days at either end — which is basically always — that formula falls apart, because whether a leftover day "counts" depends entirely on which day of the week it lands on. I ended up building a small workday calculator just to get this right, and the actual logic turned out to be less "math" and more "walk the calendar and check."&lt;/p&gt;

&lt;h2&gt;
  
  
  Full weeks are cheap, partial weeks aren't
&lt;/h2&gt;

&lt;p&gt;The calculator lets you define your own work week (a set of weekday numbers, 0=Sunday through 6=Saturday, defaulting to Mon–Fri) instead of assuming a fixed 5-day week. That's the part that breaks the naive division trick — you can't just multiply by 5 if the user's work week has 4 days or includes Saturday.&lt;/p&gt;

&lt;p&gt;Here's the "count workdays between two dates" function, close to what's actually running:&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;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromISO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;twoDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date1&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;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromISO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;twoDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date2&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;source_Day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;diff&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;days&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toObject&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;res_week&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="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source_Day&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;7&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;firstDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;source_Day&lt;/span&gt; &lt;span class="o"&gt;&amp;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="nx"&gt;twoDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;twoDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date2&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;lastDate&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;source_Day&lt;/span&gt; &lt;span class="o"&gt;&amp;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="nx"&gt;twoDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date2&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;twoDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date1&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;firstDate_weekday&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;firstDate&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;lastDate_weekday&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;lastDate&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;resDays&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res_week&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;myWorkday&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="nx"&gt;length&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;tempEnd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;lastDate_weekday&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;firstDate_weekday&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;lastDate_weekday&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lastDate_weekday&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;7&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="nx"&gt;firstDate_weekday&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;tempEnd&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;myWorkday&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="nf"&gt;includes&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;7&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="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;resDays&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trick is splitting the range into two very different problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Complete 7-day blocks.&lt;/strong&gt; &lt;code&gt;res_week = floor(totalDays / 7)&lt;/code&gt;. Any full 7-day span always contains exactly one occurrence of every weekday, so it always contains exactly &lt;code&gt;myWorkday.length&lt;/code&gt; workdays — no matter which day it starts on. That's why this part &lt;em&gt;can&lt;/em&gt; be pure multiplication.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The leftover partial week.&lt;/strong&gt; This can't be reduced to a day &lt;em&gt;count&lt;/em&gt;, because whether those leftover days count depends on which weekdays they actually are. So instead of computing "how many leftover days," the code computes the leftover's start and end &lt;em&gt;weekdays&lt;/em&gt;, and walks weekday-by-weekday from one to the other (&lt;code&gt;tempEnd&lt;/code&gt; bumps by 7 if the end weekday number is smaller than the start, to handle wrapping across a week boundary, e.g. Friday → Monday), checking each one against the custom workday set.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both the start date and end date are included in the count — that's an explicit design choice in the tool, not an accident of the math.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding N workdays to a date has the same problem, mirrored
&lt;/h2&gt;

&lt;p&gt;The other half of the tool answers a different question: "what date is 10 workdays from now?" Same underlying issue — you can't just add &lt;code&gt;10 * 7/5&lt;/code&gt; calendar days, because it matters which weekday you start counting from:&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;infer_Week&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;inferNumber&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;myWorkday&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="nx"&gt;length&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;infer_remainder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inferNumber&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;myWorkday&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="nx"&gt;length&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;infer_Week&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;infer_Week&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;infer_Week&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;infer_remainder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;infer_remainder&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;myWorkday&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="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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;start_weekday&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;singleDate&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;getDay&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;nowWeekday&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dateDirection&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="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;start_weekday&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;7&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start_weekday&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="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;7&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;inferDays&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;infer_Week&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;7&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="nx"&gt;infer_remainder&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;gt;&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;myWorkday&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="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nowWeekday&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="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;inferDays&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;nowWeekday&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dateDirection&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="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;nowWeekday&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;7&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nowWeekday&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="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;7&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;Same split as before: &lt;code&gt;infer_Week&lt;/code&gt; full weeks get resolved by multiplication, and the leftover (&lt;code&gt;infer_remainder&lt;/code&gt;) gets resolved by actually walking day-by-day and checking membership in the workday set, incrementing &lt;code&gt;inferDays&lt;/code&gt; every calendar day but only decrementing the counter &lt;code&gt;i&lt;/code&gt; on days that are actual workdays.&lt;/p&gt;

&lt;p&gt;The one non-obvious line is the &lt;code&gt;if (infer_Week &amp;gt; 0)&lt;/code&gt; block, which "borrows back" one full week into the remainder. Without it, a small remainder (say, needing 2 more workdays) combined with an unlucky starting weekday could theoretically need to walk further than the remainder value allows before hitting enough workdays. Padding the remainder with one extra full week's worth of days guarantees the while loop always has enough calendar days to walk through to satisfy the count, regardless of which weekday you started on.&lt;/p&gt;

&lt;p&gt;Note also that the start date itself is &lt;em&gt;not&lt;/em&gt; counted here — the loop deliberately starts from &lt;code&gt;start_weekday + 1&lt;/code&gt; (or &lt;code&gt;- 1&lt;/code&gt; going backwards), so "5 workdays from Monday" lands on the following Monday, not the Friday four days later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this actually breaks
&lt;/h2&gt;

&lt;p&gt;A few real gotchas I ran into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The weekday lookup uses &lt;code&gt;new Date(dateString).getDay()&lt;/code&gt;, not the same date library used everywhere else.&lt;/strong&gt; The rest of the tool parses dates with Luxon's &lt;code&gt;DateTime.fromISO&lt;/code&gt;, but the weekday check falls back to plain &lt;code&gt;new Date("2024-01-01").getDay()&lt;/code&gt;. A bare &lt;code&gt;YYYY-MM-DD&lt;/code&gt; string is parsed by JS as UTC midnight, while &lt;code&gt;.getDay()&lt;/code&gt; reads it back in the browser's &lt;em&gt;local&lt;/em&gt; time zone. For anyone west of UTC, that mismatch can roll the date back to the previous day right at midnight-ish boundaries and return the wrong weekday — which then throws off which days count as workdays for that edge of the range.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No public holidays.&lt;/strong&gt; This only excludes the weekdays you tell it to exclude. If a national holiday falls inside your range, you have to manually adjust the result yourself — the tool doesn't know your country's calendar and doesn't try to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deselecting every workday checkbox isn't guarded against.&lt;/strong&gt; Nothing stops you from unchecking all seven days. In the "between two dates" mode that just quietly returns 0. In the "add/subtract workdays" mode it's worse: &lt;code&gt;myWorkday.value.length&lt;/code&gt; becomes the divisor in &lt;code&gt;infer_Week = Math.floor(N / 0)&lt;/code&gt;, which produces &lt;code&gt;Infinity&lt;/code&gt;/&lt;code&gt;NaN&lt;/code&gt;, and the result is a broken date instead of a warning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The two modes disagree on whether the start date counts&lt;/strong&gt;, and that's intentional rather than a bug — "days between" includes both endpoints, "add N workdays" excludes the starting point — but it's easy to get bitten by the asymmetry if you assume they behave the same way.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I turned the cleaned-up version of this into a small free tool if you want to poke at it: &lt;a href="https://begoodtool.com/workCalculator/en" rel="noopener noreferrer"&gt;Business Day Calculator&lt;/a&gt;. No sign-up, and you can set your own work week instead of assuming Mon–Fri.&lt;/p&gt;




&lt;h2&gt;
  
  
  Available in other languages
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/workCalculator" rel="noopener noreferrer"&gt;工作天計算機&lt;/a&gt; — 繁體中文&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/workCalculator/cn" rel="noopener noreferrer"&gt;工作日计算器&lt;/a&gt; — 简体中文&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/workCalculator/en" rel="noopener noreferrer"&gt;Business Day Calculator&lt;/a&gt; — English&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/workCalculator/jp" rel="noopener noreferrer"&gt;営業日計算機&lt;/a&gt; — 日本語&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/workCalculator/kr" rel="noopener noreferrer"&gt;근무일 계산기&lt;/a&gt; — 한국어&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/workCalculator/fr" rel="noopener noreferrer"&gt;Calculateur de Jours Ouvrables&lt;/a&gt; — Français&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/workCalculator/ru" rel="noopener noreferrer"&gt;Калькулятор рабочих дней&lt;/a&gt; — Русский&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/workCalculator/de" rel="noopener noreferrer"&gt;Arbeitstage-Rechner&lt;/a&gt; — Deutsch&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/workCalculator/id" rel="noopener noreferrer"&gt;Kalkulator Hari Kerja&lt;/a&gt; — Bahasa Indonesia&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/workCalculator/es" rel="noopener noreferrer"&gt;Calculadora de días laborables&lt;/a&gt; — Español&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/workCalculator/vi" rel="noopener noreferrer"&gt;Máy tính ngày làm việc&lt;/a&gt; — Tiếng Việt&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/workCalculator/th" rel="noopener noreferrer"&gt;เครื่องคิดวันทำงาน&lt;/a&gt; — ไทย&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/workCalculator/pl" rel="noopener noreferrer"&gt;Kalkulator dni roboczych&lt;/a&gt; — Polski&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/workCalculator/tr" rel="noopener noreferrer"&gt;İş Günü Hesaplayıcı&lt;/a&gt; — Türkçe&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/workCalculator/it" rel="noopener noreferrer"&gt;Calcolatore Giorni Lavorativi&lt;/a&gt; — Italiano&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/workCalculator/pt" rel="noopener noreferrer"&gt;Calculadora de dias úteis&lt;/a&gt; — Português&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/workCalculator/nl" rel="noopener noreferrer"&gt;Werkdagen Calculator&lt;/a&gt; — Nederlands&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/workCalculator/uk" rel="noopener noreferrer"&gt;Калькулятор робочих днів&lt;/a&gt; — Українська&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Turns out a "random" spin wheel doesn't pick a winner and rotate to it — it spins first, then checks who's standing there</title>
      <dc:creator>Vera Yang</dc:creator>
      <pubDate>Fri, 31 Jul 2026 07:00:01 +0000</pubDate>
      <link>https://dev.to/begoodtool/turns-out-a-random-spin-wheel-doesnt-pick-a-winner-and-rotate-to-it-it-spins-first-then-1pke</link>
      <guid>https://dev.to/begoodtool/turns-out-a-random-spin-wheel-doesnt-pick-a-winner-and-rotate-to-it-it-spins-first-then-1pke</guid>
      <description>&lt;p&gt;A while back I needed a wheel for picking a raffle winner from a list of names. My mental model going in was basically a slot machine: roll a weighted random number, figure out which name that number lands on, then animate the wheel spinning around until it visually lines up with that pre-chosen name. Reasonable assumption — that's how I'd have built it.&lt;/p&gt;

&lt;p&gt;That's not what happens. Building the random-wheel tool, I found the actual sequence is backwards: the wheel spins to a random angle &lt;em&gt;first&lt;/em&gt;, and only after it physically stops does the code check which segment happens to be sitting under the pointer. The winner isn't decided — it's discovered.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wheel doesn't know who won until after it stops
&lt;/h2&gt;

&lt;p&gt;The tool hands its segment list to &lt;a href="https://github.com/zarocknz/javascript-winwheel" rel="noopener noreferrer"&gt;Winwheel.js&lt;/a&gt;, a canvas wheel library, configured like this:&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;theWheel&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;Winwheel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;outerRadius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;canvasWdith&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;textFontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;textOrientation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vertical&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;textAlignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;outer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;numSegments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;segments&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="na"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;spinToStop&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;spins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;callbackFinished&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alertPrize&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;Nowhere does this app set &lt;code&gt;animation.stopAngle&lt;/code&gt;. That matters, because inside the library, when &lt;code&gt;stopAngle&lt;/code&gt; is left unset, it fills it in itself with a plain random 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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_stopAngle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stopAngle&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&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;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;359&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;random&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;360&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stopAngle&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pointerAngle&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final rotation the wheel animates to is just &lt;code&gt;spins * 360 + _stopAngle&lt;/code&gt; — some number of full turns, plus that random leftover angle. TweenMax eases the &lt;code&gt;rotationAngle&lt;/code&gt; property from its current value to that target over the 10-second &lt;code&gt;duration&lt;/code&gt;, using a decelerating curve so it looks like it's slowing down naturally. Only once the tween finishes does &lt;code&gt;callbackFinished&lt;/code&gt; fire, and only then does the library work out which segment is physically under the fixed pointer by taking the final rotation modulo 360 and comparing it against each segment's angular range. That result — whatever segment happens to be there — is what gets handed to the app's &lt;code&gt;alertPrize&lt;/code&gt; function. There's no "chosen winner" object anywhere before that point. The randomness lives entirely in one &lt;code&gt;Math.random()&lt;/code&gt; call buried in a third-party script tag (&lt;code&gt;/Winwheel.min.js&lt;/code&gt;), loaded outside the normal build pipeline — which is an easy thing to miss if you only read the Vue component and assume the picking logic is in there.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Wheel speed" doesn't change the odds — it changes how long you have to wait
&lt;/h2&gt;

&lt;p&gt;The UI lets you pick slow/medium/fast before spinning:&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wheelPower&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;theWheel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spins&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wheelPower&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;theWheel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spins&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wheelPower&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;theWheel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spins&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&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;More "power" just means more full 360° turns before the wheel settles, all crammed into the same fixed 10-second &lt;code&gt;duration&lt;/code&gt; — so higher power genuinely does spin faster on screen, since the wheel has to cover more distance in the same time. But since every extra full turn is a multiple of 360, it has zero effect on which segment ends up at the top; &lt;code&gt;spins * 360&lt;/code&gt; disappears entirely once you take the final angle modulo 360. The random offset from &lt;code&gt;getRandomForSegment&lt;/code&gt;-style logic is the only thing that decides the outcome. "Fast" mode is pure theater for suspense, not a different fairness setting — which took me a minute to convince myself of, because visually it really does look like a different kind of spin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Segments are colored like a line, but the wheel is a circle
&lt;/h2&gt;

&lt;p&gt;Each item becomes a wedge, colored by cycling through three colors based on index:&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="nf"&gt;segments&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;colorList&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="s2"&gt;#7ecefc&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="s2"&gt;#fff6e6&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="s2"&gt;#ff8066&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;newArray&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wheelItems&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;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;fillStyle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nx"&gt;colorList&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wheelItems&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;&amp;amp;&amp;amp;&lt;/span&gt;
          &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wheelItems&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;3&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="mi"&gt;1&lt;/span&gt;
            &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&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="na"&gt;textFontSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nx"&gt;item&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="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
          &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;canvasWdith&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;item&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="nx"&gt;length&lt;/span&gt;
          &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&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;newArray&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;index % 3&lt;/code&gt; alone is a fine way to alternate three colors down a flat list. But a wheel wraps around — segment 0 sits right next to the &lt;em&gt;last&lt;/em&gt; segment, not just the next one. Cycling colors mod 3 on a list whose length is a multiple-of-3-plus-one (4, 7, 10, 13...) means the last segment's natural color (&lt;code&gt;(length-1) % 3&lt;/code&gt;, which works out to &lt;code&gt;0&lt;/code&gt;) collides with the first segment's color, and since they're physically adjacent on the circle, you'd get two same-colored wedges sitting side by side. The &lt;code&gt;length % 3 == 1&lt;/code&gt; check catches exactly that case and forces the last segment to color index &lt;code&gt;1&lt;/code&gt; instead, breaking up the collision. It's a small, easy-to-miss piece of circular-list logic that a flat-list mental model won't catch until you actually render a 4- or 7-item wheel and stare at the seam.&lt;/p&gt;

&lt;p&gt;The font-size line next to it is solving an unrelated problem: text is drawn radiating outward (&lt;code&gt;textOrientation: "vertical"&lt;/code&gt;, &lt;code&gt;textAlignment: "outer"&lt;/code&gt;), and a long label at the default 24px font will run off the edge of a narrow wedge, so anything over 5 characters gets shrunk proportionally to &lt;code&gt;canvasWidth / 3 / length&lt;/code&gt; instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and things that bit me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Duplicate item text isn't handled specially, and it has a real consequence.&lt;/strong&gt; The wheel has a "remove previous winner" button, and it filters the item list by matching text: &lt;code&gt;wheelItems.filter(item =&amp;gt; item.value != itemText)&lt;/code&gt;. If you type "Alice" into two different slots, spin, and win "Alice," clicking remove deletes &lt;em&gt;both&lt;/em&gt; segments named Alice at once — because the filter has no way to know you meant one specific wedge. Duplicate labels also quietly double that label's odds without any warning in the UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No dedupe, no cap.&lt;/strong&gt; The only validation before building the wheel is &lt;code&gt;AwardSetting.Items.length &amp;lt; 2&lt;/code&gt; (must have at least two non-empty items). You can paste in a few hundred lines and it will happily render a few hundred slivers — readable-text-per-wedge just isn't something the tool checks for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's a uniform pick, not a weighted one.&lt;/strong&gt; There's dead code in the source (&lt;code&gt;rateWin&lt;/code&gt;, fully commented out) that computes cumulative weighted ranges from a &lt;code&gt;rate&lt;/code&gt; field — clearly an earlier attempt at weighted odds that never shipped. As it stands today, every segment has exactly equal probability regardless of how many times a name repeats structurally (aside from the duplicate-label trick above).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The random-number call isn't in the app's own code at all.&lt;/strong&gt; It's inside a vendored library loaded via a plain &lt;code&gt;&amp;lt;script src="/Winwheel.min.js"&amp;gt;&lt;/code&gt; tag, separate from the npm-managed dependencies. If you're auditing "is this actually random," reading the Vue component alone won't show you — you have to go find the library file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I turned the working version of this into a small free tool if you want an actual raffle wheel without wiring up the animation library yourself: &lt;a href="https://begoodtool.com/wheel/en" rel="noopener noreferrer"&gt;Online Random Wheel Generator&lt;/a&gt;. No sign-up, add as many items as you want.&lt;/p&gt;




&lt;h2&gt;
  
  
  Available in other languages
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/wheel" rel="noopener noreferrer"&gt;隨機抽獎輪盤產生器&lt;/a&gt; — 繁體中文&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/wheel/cn" rel="noopener noreferrer"&gt;在线随机抽奖轮盘生成器&lt;/a&gt; — 简体中文&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/wheel/en" rel="noopener noreferrer"&gt;Online Random Wheel Generator&lt;/a&gt; — English&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/wheel/jp" rel="noopener noreferrer"&gt;オンラインランダムルーレットジェネレーター&lt;/a&gt; — 日本語&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/wheel/kr" rel="noopener noreferrer"&gt;온라인 랜덤 휠 생성기&lt;/a&gt; — 한국어&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/wheel/fr" rel="noopener noreferrer"&gt;Générateur de roulettes aléatoires en ligne&lt;/a&gt; — Français&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/wheel/ru" rel="noopener noreferrer"&gt;Онлайн-генератор случайных колес&lt;/a&gt; — Русский&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/wheel/de" rel="noopener noreferrer"&gt;Online Zufallsrad-Generator&lt;/a&gt; — Deutsch&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/wheel/id" rel="noopener noreferrer"&gt;Pembuat Roda Acak Online&lt;/a&gt; — Bahasa Indonesia&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/wheel/es" rel="noopener noreferrer"&gt;Generador de Ruleta Aleatoria en Línea&lt;/a&gt; — Español&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/wheel/vi" rel="noopener noreferrer"&gt;Công Cụ Tạo Máy Quay Ngẫu Nhiên Trực Tuyến&lt;/a&gt; — Tiếng Việt&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/wheel/th" rel="noopener noreferrer"&gt;สร้างรถหมุนสุ่มออนไลน์&lt;/a&gt; — ไทย&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/wheel/pl" rel="noopener noreferrer"&gt;Generator Losujący Ruletkę Online&lt;/a&gt; — Polski&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/wheel/tr" rel="noopener noreferrer"&gt;Çevrimiçi Rastgele Rulet Oluşturucu&lt;/a&gt; — Türkçe&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/wheel/it" rel="noopener noreferrer"&gt;Generatore di Roulette Casuale Online&lt;/a&gt; — Italiano&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/wheel/pt" rel="noopener noreferrer"&gt;Gerador de Roleta Aleatória Online&lt;/a&gt; — Português&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/wheel/nl" rel="noopener noreferrer"&gt;Online Willekeurig Roulette Generator&lt;/a&gt; — Nederlands&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://begoodtool.com/wheel/uk" rel="noopener noreferrer"&gt;Онлайн Генератор Випадкової Рулетки&lt;/a&gt; — Українська&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
