<?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: zhihu wu</title>
    <description>The latest articles on DEV Community by zhihu wu (@zhihu_wu_dea1d82af01a04d7).</description>
    <link>https://dev.to/zhihu_wu_dea1d82af01a04d7</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%2F3921997%2Fa775d61f-ed57-461b-ac46-ed108350189e.png</url>
      <title>DEV Community: zhihu wu</title>
      <link>https://dev.to/zhihu_wu_dea1d82af01a04d7</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zhihu_wu_dea1d82af01a04d7"/>
    <language>en</language>
    <item>
      <title>Your String Length Is a Lie: Four Unicode Traps in Everyday Code</title>
      <dc:creator>zhihu wu</dc:creator>
      <pubDate>Sat, 24 Oct 2026 13:01:57 +0000</pubDate>
      <link>https://dev.to/zhihu_wu_dea1d82af01a04d7/your-string-length-is-a-lie-four-unicode-traps-in-everyday-code-2p82</link>
      <guid>https://dev.to/zhihu_wu_dea1d82af01a04d7/your-string-length-is-a-lie-four-unicode-traps-in-everyday-code-2p82</guid>
      <description>&lt;p&gt;Every character counter you have ever written is wrong for at least one of your users. Here are the four Unicode traps I keep finding in code reviews — each one verified in Node, not recalled from a blog post.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;.length&lt;/code&gt; counts UTF-16 code units, not characters
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;u{1F44D}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;                    &lt;span class="c1"&gt;// 2  (one thumbs up)&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;u{1F1E8}&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;u{1F1F3}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;           &lt;span class="c1"&gt;// 4  (one flag)&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;u{1F468}&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;u200D&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;u{1F469}&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;u200D&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;u{1F467}&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;u200D&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;u{1F466}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;  &lt;span class="c1"&gt;// 11 (one family)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code points are closer: &lt;code&gt;[...str].length&lt;/code&gt; gives 7 for that family. But only grapheme clusters give the real answer:&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="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Segmenter&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&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;granularity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;grapheme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;  &lt;span class="c1"&gt;// 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dangerous part is slicing. &lt;code&gt;str.substring(0, 1)&lt;/code&gt; on a thumbs up returns a lone high surrogate. Encode that and you get 3 bytes of replacement character — no exception, no warning, just corrupted data in your database.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. "é" is sometimes two characters
&lt;/h2&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;nfc&lt;/span&gt; &lt;span class="o"&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="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NFC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// 1 unit, 2 bytes&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nfd&lt;/span&gt; &lt;span class="o"&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="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NFD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// 2 units, 3 bytes&lt;/span&gt;
&lt;span class="nx"&gt;nfc&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;nfd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                        &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Identical pixels, different strings. &lt;code&gt;new Map([[nfd, 1]]).has(nfc)&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt;, so dedupe, rate-limit keys, email lookups and "already exists" checks quietly miss. macOS filenames arrive decomposed, so this is not a theoretical case. Normalize to NFC at the boundary, before hashing or comparing.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Invisible characters survive &lt;code&gt;trim()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;"  \u200B  ".trim().length&lt;/code&gt; is &lt;code&gt;1&lt;/code&gt; — a zero-width space is not whitespace in JavaScript, and &lt;code&gt;/\s/.test("\u200B")&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt;. A non-breaking space, by contrast, &lt;em&gt;is&lt;/em&gt; stripped. So text pasted from a browser can look perfectly clean and still not match.&lt;/p&gt;

&lt;p&gt;Markdown is where it hurts most. I tested rendered output in a GFM previewer: a "blank" line containing only &lt;code&gt;U+00A0&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; split paragraphs, and a line where the space before &lt;code&gt;#&lt;/code&gt; is &lt;code&gt;U+00A0&lt;/code&gt; never becomes a heading — it renders as &lt;code&gt;&amp;lt;p&amp;gt; # Title&amp;lt;/p&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Bytes are a third number
&lt;/h2&gt;

&lt;p&gt;The same family emoji is 11 UTF-16 units, 7 code points, 1 grapheme and &lt;strong&gt;25 UTF-8 bytes&lt;/strong&gt;. Column lengths, index key limits (utf8mb4 reserves 4 bytes per character), and base64 payloads all run on bytes, so a 768-character field is not a 768-byte field.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb:&lt;/strong&gt; units for strings, code points for iteration, graphemes for UI, bytes for storage.&lt;/p&gt;

&lt;p&gt;When I need to find which of these is biting me, I dump the string into a character-level inspector — I use &lt;a href="https://codetoolbox.pro/tools/unicode-inspector.html" rel="noopener noreferrer"&gt;Unicode Inspector&lt;/a&gt;, which runs entirely in the browser and shows the code point, UTF-16 units, UTF-8 bytes and entity for every character, plus filters for invisible ones. Paste a failing value in, find the &lt;code&gt;U+200B&lt;/code&gt; you could not see, done.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Your Amortization Loop Never Reaches Zero: 4 Bugs in Loan Math Code</title>
      <dc:creator>zhihu wu</dc:creator>
      <pubDate>Mon, 19 Oct 2026 13:02:41 +0000</pubDate>
      <link>https://dev.to/zhihu_wu_dea1d82af01a04d7/your-amortization-loop-never-reaches-zero-4-bugs-in-loan-math-code-n39</link>
      <guid>https://dev.to/zhihu_wu_dea1d82af01a04d7/your-amortization-loop-never-reaches-zero-4-bugs-in-loan-math-code-n39</guid>
      <description>&lt;p&gt;If you've ever generated an amortization schedule by hand, you've probably hit this: you loop 360 times and the balance lands on... -$3.07. Or +$0.84. Never exactly zero.&lt;/p&gt;

&lt;p&gt;Here are four bugs I keep running into, with numbers you can check yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The loop's last step is a lie
&lt;/h2&gt;

&lt;p&gt;$300,000 at 7% over 360 months. The exact payment is 1,995.9074..., which you round to 1,995.91. Run &lt;code&gt;balance = balance + balance*r - payment&lt;/code&gt; 360 times and the balance ends at &lt;strong&gt;-$3.07&lt;/strong&gt; - you overpaid by three dollars because you rounded the payment up. Round down and you finish with a small balance the borrower still owes.&lt;/p&gt;

&lt;p&gt;Switching to integer cents doesn't fix it; it just relocates the error (same loan, same 360 iterations: -355 cents).&lt;/p&gt;

&lt;p&gt;The fix is to stop trusting the last iteration. After payment 359 the balance is $1,981.28, so the final payment must be &lt;code&gt;balance * (1 + r)&lt;/code&gt; = &lt;strong&gt;$1,992.84&lt;/strong&gt;. Lenders do exactly this: a fixed payment for every month except the last.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Accruing interest after subtracting the payment
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;payment&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="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// wrong&lt;/span&gt;
&lt;span class="nx"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// right&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both look reasonable. They differ by one period of interest on the payment itself, and the first version "pays off" in 354 payments instead of 360 while reporting $406,552 of total interest instead of $418,527 - &lt;strong&gt;$11,975 short&lt;/strong&gt;. Nobody notices, because the schedule still ends near zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. setMonth() drifts
&lt;/h2&gt;

&lt;p&gt;A schedule assumes "the same day each month". JavaScript disagrees:&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;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&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="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setMonth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// -&amp;gt; Mar 3, not Feb 28&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 30-year schedule built month-by-month slides forward every time it crosses a short month. The dates in the table look plausible; they're just wrong by a few days that keep accumulating.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. "Extra payment" is not principal by default
&lt;/h2&gt;

&lt;p&gt;Many servicers apply extra money to your &lt;strong&gt;next installment&lt;/strong&gt;, which only advances the due date. The loan still runs the full term and total interest is unchanged. You have to mark it principal-only, then verify on the next statement that the principal balance actually dropped.&lt;/p&gt;

&lt;p&gt;Get it right and the payoff is real: $100/month extra on that $300k loan clears it in 310 payments - about 4.2 years early, roughly $69,300 of interest saved.&lt;/p&gt;

&lt;p&gt;I keep a free &lt;a href="https://codetoolbox.pro/tools/loan-calculator.html" rel="noopener noreferrer"&gt;loan calculator&lt;/a&gt; open for checking these by hand - it runs entirely in the browser, shows the full schedule and the year-end summaries, which is how I caught the rounding bug in my own script.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Your Placeholder Text Is Lying to You: 4 Layout Bugs Lorem Ipsum Hides</title>
      <dc:creator>zhihu wu</dc:creator>
      <pubDate>Fri, 16 Oct 2026 13:04:23 +0000</pubDate>
      <link>https://dev.to/zhihu_wu_dea1d82af01a04d7/your-placeholder-text-is-lying-to-you-4-layout-bugs-lorem-ipsum-hides-24dk</link>
      <guid>https://dev.to/zhihu_wu_dea1d82af01a04d7/your-placeholder-text-is-lying-to-you-4-layout-bugs-lorem-ipsum-hides-24dk</guid>
      <description>&lt;p&gt;Every mockup I reviewed this year used lorem ipsum. Several of those layouts broke in production - not because the design was bad, but because the placeholder text was too friendly.&lt;/p&gt;

&lt;p&gt;Here is what lorem ipsum optimizes for, and what it quietly hides.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. It has no long, unbreakable strings
&lt;/h2&gt;

&lt;p&gt;Lorem ipsum words are short, lowercase, and separated by spaces. Real content contains things the browser cannot hyphenate: a 90-character signed URL, an address like &lt;code&gt;firstname.lastname@international-partners.example.com&lt;/code&gt;, an API token, a German compound noun. Those are what overflow a flex row - &lt;code&gt;flex: 1&lt;/code&gt; will not shrink below the content's min-content width unless you also set &lt;code&gt;min-width: 0&lt;/code&gt; (or &lt;code&gt;overflow-wrap: anywhere&lt;/code&gt;). You will never see that bug in a lorem ipsum mock. Paste one long token into your design before you call it done.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. It never expands
&lt;/h2&gt;

&lt;p&gt;German text runs roughly 30% longer than English, Finnish invents longer compounds, and CJK wraps between any two characters instead of at spaces. A button label that fits at 320px in English can overflow in two languages. Test with the longest realistic translation - or at minimum add 30% to every label and re-check the tight rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. It hides truncation and empty states
&lt;/h2&gt;

&lt;p&gt;Lorem ipsum paragraphs are all the same length, so list rows never collapse, cards never hit their one-line truncation, and &lt;code&gt;-webkit-line-clamp&lt;/code&gt; never gets exercised. Real data puts a one-word title and a 300-word title in the same list. Feed the layout a deliberately awkward set of strings - empty string, a single character, one 200-character sentence - and look at each row again.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. It hides the labels
&lt;/h2&gt;

&lt;p&gt;Placeholder copy tends to be descriptive nouns. Real UI copy is verbs and consequences: "Delete workspace", not "Workspace deletion". If a screen only reads correctly with lorem ipsum in it, the placeholder is doing layout work that the layout should be doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  When lorem ipsum is the right call
&lt;/h2&gt;

&lt;p&gt;Before the copy exists at all - early explorations where you are deciding spacing, hierarchy, and density. Once the layout is settled, swap in realistic strings. When I need filler fast I use a &lt;a href="https://codetoolbox.pro/tools/lorem-ipsum.html" rel="noopener noreferrer"&gt;lorem ipsum generator&lt;/a&gt;: it runs entirely in the browser (nothing uploaded, no signup), you choose the paragraph and sentence counts, and the output pastes straight into test fixtures.&lt;/p&gt;

&lt;p&gt;The rule I keep coming back to: if your placeholder text would pass a type checker, it is not testing anything.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>design</category>
      <category>ux</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Four Ways a Git Diff Lies to You (and How to Catch Each)</title>
      <dc:creator>zhihu wu</dc:creator>
      <pubDate>Wed, 14 Oct 2026 13:02:46 +0000</pubDate>
      <link>https://dev.to/zhihu_wu_dea1d82af01a04d7/four-ways-a-git-diff-lies-to-you-and-how-to-catch-each-25b3</link>
      <guid>https://dev.to/zhihu_wu_dea1d82af01a04d7/four-ways-a-git-diff-lies-to-you-and-how-to-catch-each-25b3</guid>
      <description>&lt;p&gt;Every code review is a promise: &lt;em&gt;this is what changed&lt;/em&gt;. Most days the tooling keeps that promise. Sometimes it quietly breaks it, and the break is subtle enough to survive approval.&lt;/p&gt;

&lt;p&gt;Four diff blind spots I keep running into — each one makes a change set look bigger, smaller, or simply different than reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Two dots and three dots are not the same comparison
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git diff main..feature     &lt;span class="c"&gt;# tip vs tip&lt;/span&gt;
git diff main...feature    &lt;span class="c"&gt;# merge-base vs tip&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;main..feature&lt;/code&gt; compares the two branch tips directly. &lt;code&gt;main...feature&lt;/code&gt; (three dots) compares from the &lt;strong&gt;merge base&lt;/strong&gt; — the last commit both branches share. GitHub pull requests show you the three-dot view.&lt;/p&gt;

&lt;p&gt;So if you review locally with two dots after &lt;code&gt;main&lt;/code&gt; has moved on, you see every commit merged into &lt;code&gt;main&lt;/code&gt; since the branch split — none of which your PR touched. A 12-line PR suddenly reads like 400 lines. When a change count looks absurd, check the dot count before you blame your own branch.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Plain &lt;code&gt;git diff&lt;/code&gt; only shows unstaged changes
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;git diff&lt;/code&gt; compares the working tree against the index. Run &lt;code&gt;git add&lt;/code&gt; and those edits drop out of that comparison entirely — the diff goes quiet while your changes are still there. Use &lt;code&gt;git diff --cached&lt;/code&gt; for staged work, or &lt;code&gt;git diff HEAD&lt;/code&gt; to see both at once.&lt;/p&gt;

&lt;p&gt;Untracked files are a harder case: brand-new files never appear in any &lt;code&gt;git diff&lt;/code&gt; output. Only &lt;code&gt;git status&lt;/code&gt; lists them.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. A moved file arrives as a delete + add
&lt;/h2&gt;

&lt;p&gt;Rename detection is a heuristic, not a fact. Move a file with plain &lt;code&gt;mv&lt;/code&gt; and the diff shows a 300-line deletion plus a 300-line addition — so a reviewer's eye slides past the four lines you actually edited.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;git diff -M --stat
someFile.ts | 2 +-
similarity index 98%
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-M&lt;/code&gt; turns rename detection on explicitly (tune with &lt;code&gt;-M40%&lt;/code&gt; if a heavily edited move is missed). &lt;code&gt;--stat&lt;/code&gt; gives you the one-line summary that makes the real edit visible again.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. That "whole file changed" diff is formatting, not data
&lt;/h2&gt;

&lt;p&gt;Minified vs pretty JSON, CRLF vs LF, reordered keys: a one-field change to a 200-line API response renders as the entire file. Normalize both sides before comparing — &lt;code&gt;jq -S&lt;/code&gt; for key order, a formatter for whitespace, &lt;code&gt;dos2unix&lt;/code&gt; for line endings — and then everything left in the diff is real.&lt;/p&gt;

&lt;p&gt;And when the two versions don't live in a repo at all (a log viewer, a chat message, a CMS), there's no file to run &lt;code&gt;diff&lt;/code&gt; on. I paste both sides into &lt;a href="https://codetoolbox.pro/tools/diff-checker.html" rel="noopener noreferrer"&gt;CodeToolbox's diff checker&lt;/a&gt; — it runs in the browser, uploads nothing, and shows the added/removed lines plus a summary count before you scroll.&lt;/p&gt;

&lt;p&gt;Blind spot 1 is the expensive one: the extra lines are real commits, just not yours. Check the dot count first.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>git</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Your Cron Job Runs at the Wrong Time (or Twice): Four Traps in Five Fields</title>
      <dc:creator>zhihu wu</dc:creator>
      <pubDate>Sun, 11 Oct 2026 13:01:44 +0000</pubDate>
      <link>https://dev.to/zhihu_wu_dea1d82af01a04d7/your-cron-job-runs-at-the-wrong-time-or-twice-four-traps-in-five-fields-2fmp</link>
      <guid>https://dev.to/zhihu_wu_dea1d82af01a04d7/your-cron-job-runs-at-the-wrong-time-or-twice-four-traps-in-five-fields-2fmp</guid>
      <description>&lt;p&gt;Cron is 50 years old, has five fields, and still owns a special place in every on-call rotation. Nearly every "why did this run at 3am" ticket traces back to one of four things.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Two restricted date fields mean OR, not AND
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;0 9 1 * 1&lt;/code&gt; reads like "9:00 on the 1st, if it's a Monday." Cron reads it as "9:00 on the 1st &lt;strong&gt;or&lt;/strong&gt; any Monday." When both day-of-month and day-of-week are restricted, you get a union — so a monthly job quietly also runs four Mondays a month.&lt;/p&gt;

&lt;p&gt;Fix: keep one field as &lt;code&gt;*&lt;/code&gt; and filter inside the command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Mondays only&lt;/span&gt;
0 9 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; 1 /app/monthly.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you specifically need "the Monday that falls in the first week," schedule every Monday and let the script check &lt;code&gt;date +%d&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;code&gt;*/2&lt;/code&gt; in day-of-month doesn't mean "every two days"
&lt;/h2&gt;

&lt;p&gt;Steps in the day-of-month field start at 1, so &lt;code&gt;0 3 */2 * *&lt;/code&gt; fires on 1, 3, 5 … 31 and then resets. The gap across the month boundary is one day after a 31-day month and two days after a 30-day month. Any job doing interval arithmetic — log rotation, rolling windows, invoice cutoffs — drifts or double-fires.&lt;/p&gt;

&lt;p&gt;Fix: run daily and store the last-run date in a file, or use a scheduler with real interval semantics.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Cron uses the server's timezone, and containers are UTC
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;0 0 * * *&lt;/code&gt; means midnight on the host. Inside a container that is midnight UTC: 8am in Beijing, 5pm the previous day in California. GitHub Actions cron is always UTC regardless of what your repo implies.&lt;/p&gt;

&lt;p&gt;Fix: be explicit. &lt;code&gt;CRON_TZ=America/New_York&lt;/code&gt; (cronie), or set &lt;code&gt;TZ&lt;/code&gt; on the container, or keep everything UTC on purpose and convert in the application. All three beat "we assume it's local time."&lt;/p&gt;

&lt;h2&gt;
  
  
  4. An unescaped &lt;code&gt;%&lt;/code&gt; eats the rest of your command
&lt;/h2&gt;

&lt;p&gt;In a crontab file, &lt;code&gt;%&lt;/code&gt; is not a literal character — it terminates the command and everything after it becomes the job's stdin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;0 4 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /usr/bin/backup.sh &lt;span class="nt"&gt;--name&lt;/span&gt; daily-%Y%m%d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That runs &lt;code&gt;backup.sh --name daily-&lt;/code&gt; with &lt;code&gt;Y%m%d&lt;/code&gt; piped into it, and still exits 0. Escape each percent sign: &lt;code&gt;daily-\%Y\%m\%d&lt;/code&gt;. GitHub Actions and Kubernetes treat &lt;code&gt;%&lt;/code&gt; literally, so this trap is crontab-only — which makes it harder to catch when you move a workflow between the two.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I check a schedule before it ships
&lt;/h2&gt;

&lt;p&gt;I paste the expression into a visual builder that renders the next five run times in plain English — &lt;a href="https://codetoolbox.pro/tools/cron-generator.html" rel="noopener noreferrer"&gt;CodeToolbox's cron generator&lt;/a&gt; does it locally in the browser with no signup, and shows the day-of-month/day-of-week OR behaviour explicitly — then I compare those timestamps against my own calendar before the expression reaches a server. Five run times surface all four traps above.&lt;/p&gt;

&lt;p&gt;What's the worst cron bug you've had to debug?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>devops</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>UUIDs That Look Identical Aren't: The Case, Braces, and Hyphen Trap</title>
      <dc:creator>zhihu wu</dc:creator>
      <pubDate>Fri, 09 Oct 2026 13:02:36 +0000</pubDate>
      <link>https://dev.to/zhihu_wu_dea1d82af01a04d7/uuids-that-look-identical-arent-the-case-braces-and-hyphen-trap-192j</link>
      <guid>https://dev.to/zhihu_wu_dea1d82af01a04d7/uuids-that-look-identical-arent-the-case-braces-and-hyphen-trap-192j</guid>
      <description>&lt;h2&gt;
  
  
  The bug that makes no sense
&lt;/h2&gt;

&lt;p&gt;A row exists. The API returns its ID. You paste that ID into a query and get… nothing. You look at both strings side by side. They are the same. Except they are not, because "the same" for a UUID string means byte-for-byte identical, and four sneaky formatting differences break that all the time.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Case
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;.NET&lt;/code&gt;'s &lt;code&gt;Guid.ToString()&lt;/code&gt; returns lowercase. SQL Server hands back uppercase. Postgres normalizes to lowercase, and an API gateway somewhere in the middle may have done whatever it wanted. In every case-sensitive comparison — a JS &lt;code&gt;===&lt;/code&gt;, a SQL &lt;code&gt;WHERE id = '...'&lt;/code&gt;, a Redis key — &lt;code&gt;"ABC..."&lt;/code&gt; and &lt;code&gt;"abc..."&lt;/code&gt; are different values. Same UUID, failed lookup.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Hyphens
&lt;/h2&gt;

&lt;p&gt;Canonical form is 8-4-4-4-12, 36 characters. But UUIDs travel through places that strip punctuation: URL slugs, log parsers, spreadsheet columns, that one time someone hit &lt;code&gt;sed 's/-//g'&lt;/code&gt;. The 32-char hex form is a legitimate representation of the same value and an instant mismatch against a stored canonical string.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Braces and parentheses
&lt;/h2&gt;

&lt;p&gt;C# and PowerShell are happy to hand you &lt;code&gt;{550e8400-e29b-41d4-a716-446655440000}&lt;/code&gt; or &lt;code&gt;(550e8400-...)&lt;/code&gt;. If that string is what went into your DB or your cache key, your canonical-form lookups will never hit.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Hex vs bytes
&lt;/h2&gt;

&lt;p&gt;MySQL's &lt;code&gt;UUID_TO_BIN()&lt;/code&gt; stores 16 raw bytes for indexing efficiency — correct and fast. It also means a plain string comparison against that column returns nothing at all, and &lt;code&gt;BIN_TO_UUID()&lt;/code&gt; has to be in both your &lt;code&gt;SELECT&lt;/code&gt; and your &lt;code&gt;WHERE&lt;/code&gt;, or you are comparing apples to BLOBs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: normalize at the boundary
&lt;/h2&gt;

&lt;p&gt;The rule I now follow: parse on input, format on output, never compare raw strings from two different sources.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Normalize&lt;/strong&gt; to lowercase, hyphen-free hex before any equality check or cache key. One &lt;code&gt;toLowerCase().replaceAll('-', '')&lt;/code&gt; kills cases 1–3.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parse, don't regex&lt;/strong&gt;, when you can: &lt;code&gt;UUID.fromString()&lt;/code&gt; in Java, &lt;code&gt;uuid.UUID()&lt;/code&gt; in Python, &lt;code&gt;uuid.Parse()&lt;/code&gt; in Go all accept the mixed forms and &lt;em&gt;reject&lt;/em&gt; genuinely malformed input — a validation bonus you don't get from a loose pattern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate with the version/variant bits in mind&lt;/strong&gt; if you care about v4 vs v7, since a regex that only checks the 8-4-4-4-12 shape happily accepts a v7 ID where you expected v4 (and vice versa).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log the raw form&lt;/strong&gt; when debugging. Half of these bugs are invisible because whatever printed the ID for you already normalized it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When I need a handful of IDs in a known-good canonical form to test a normalization path — or to check what my parser does with the uppercase, braced, hyphen-less variants — I generate them in a free in-browser &lt;a href="https://codetoolbox.pro/tools/uuid-generator" rel="noopener noreferrer"&gt;UUID generator&lt;/a&gt; that runs entirely on the client, no upload, no signup. It does bulk output up to 100 at a time and the uppercase/lowercase toggle is handy for exactly the comparison bug above.&lt;/p&gt;

&lt;p&gt;None of this is exotic. It is just the reason two strings that "look identical" quietly fail the only test that matters.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Your Base64 Decoding Failed Because of Whitespace (and 3 Other Silent Traps)</title>
      <dc:creator>zhihu wu</dc:creator>
      <pubDate>Tue, 06 Oct 2026 13:03:03 +0000</pubDate>
      <link>https://dev.to/zhihu_wu_dea1d82af01a04d7/your-base64-decoding-failed-because-of-whitespace-and-3-other-silent-traps-1kio</link>
      <guid>https://dev.to/zhihu_wu_dea1d82af01a04d7/your-base64-decoding-failed-because-of-whitespace-and-3-other-silent-traps-1kio</guid>
      <description>&lt;p&gt;You paste a Base64 string into a decoder, and it throws &lt;code&gt;InvalidCharacterError&lt;/code&gt; or returns garbage. Nine times out of ten the alphabet is fine — it's invisible whitespace, padding, or the URL-safe variant that broke it.&lt;/p&gt;

&lt;p&gt;Here are the four traps I hit most often, and how to spot each one in seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Line breaks (the MIME 76-column wrap)
&lt;/h2&gt;

&lt;p&gt;Email-era encoders still wrap output at 76 characters with &lt;code&gt;\r\n&lt;/code&gt;. Python's &lt;code&gt;base64.encodebytes()&lt;/code&gt; does this, and so do PEM/JWKS files. &lt;code&gt;atob()&lt;/code&gt; in the browser refuses any whitespace, so a string that looks perfect silently fails. Strip all whitespace before decoding, and prefer &lt;code&gt;base64.b64encode()&lt;/code&gt; over &lt;code&gt;encodebytes()&lt;/code&gt; when you control the encoder.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Missing or excess padding
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;=&lt;/code&gt; pads the final group to a multiple of 4 characters. &lt;code&gt;YWJjZA&lt;/code&gt; (6 chars) and &lt;code&gt;YWJjZA==&lt;/code&gt; are the same bytes, but many strict decoders reject the unpadded form. Some SDKs strip padding; others require it. If a decoder fails, add &lt;code&gt;=&lt;/code&gt; until &lt;code&gt;length % 4 == 0&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. base64url vs base64
&lt;/h2&gt;

&lt;p&gt;JWTs, URL query params, and filenames use the URL-safe alphabet: &lt;code&gt;-&lt;/code&gt; and &lt;code&gt;_&lt;/code&gt; replace &lt;code&gt;+&lt;/code&gt; and &lt;code&gt;/&lt;/code&gt;, and padding is usually dropped. Feed a JWT segment straight into a standard decoder and it breaks. In Python: &lt;code&gt;base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))&lt;/code&gt;. In JS, do the character swap yourself — &lt;code&gt;replace(/-/g, '+').replace(/_/g, '/')&lt;/code&gt; — before calling &lt;code&gt;atob&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Encoding text is not encoding bytes
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;atob()&lt;/code&gt; returns a binary string, so UTF-8 text comes back as mojibake. Decode to bytes first, then to text: &lt;code&gt;new TextDecoder().decode(Uint8Array.from(atob(s), c =&amp;gt; c.charCodeAt(0)))&lt;/code&gt;, or &lt;code&gt;base64.b64decode(s).decode('utf-8')&lt;/code&gt; in Python.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five-second check
&lt;/h2&gt;

&lt;p&gt;Before you debug your code, look at the string itself: any whitespace, any &lt;code&gt;-&lt;/code&gt; or &lt;code&gt;_&lt;/code&gt;, does the length divide by 4? That diagnoses most "corrupted Base64" bugs without touching the source.&lt;/p&gt;

&lt;p&gt;When I need to eyeball a payload — checking padding, spotting a base64url segment inside a JWT, or confirming a decode round-trips my UTF-8 — I use &lt;a href="https://codetoolbox.pro/tools/base64.html" rel="noopener noreferrer"&gt;CodeToolbox's Base64 encoder/decoder&lt;/a&gt;. It runs entirely in the browser (nothing is uploaded), accepts both alphabets, and shows character counts so you can see the padding at a glance.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Your JWT Works Locally but 401s in Production - Check These 5 Things First</title>
      <dc:creator>zhihu wu</dc:creator>
      <pubDate>Sun, 04 Oct 2026 13:02:41 +0000</pubDate>
      <link>https://dev.to/zhihu_wu_dea1d82af01a04d7/your-jwt-works-locally-but-401s-in-production-check-these-5-things-first-581f</link>
      <guid>https://dev.to/zhihu_wu_dea1d82af01a04d7/your-jwt-works-locally-but-401s-in-production-check-these-5-things-first-581f</guid>
      <description>&lt;p&gt;You paste the token your API just issued into a decoder. The header parses, the payload parses, everything looks right - and the request still comes back &lt;code&gt;401 Unauthorized&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Signature bugs get all the attention, but in practice most "valid-looking token rejected" incidents are one of five boring mismatches. Decode the token first, then walk this list in order.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Clock skew between two servers
&lt;/h2&gt;

&lt;p&gt;A token is minted with &lt;code&gt;iat&lt;/code&gt; and validated against &lt;code&gt;nbf&lt;/code&gt; and &lt;code&gt;exp&lt;/code&gt;. If the machine that issued it is a few seconds behind the machine validating it, a token created "just now" can look like it is not valid yet. Most libraries default to 30-60 seconds of leeway - if someone tightened that, put it back. The tell: the token decodes, &lt;code&gt;exp&lt;/code&gt; is clearly in the future, and the failure is intermittent.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The audience or issuer doesn't match
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;aud&lt;/code&gt; and &lt;code&gt;iss&lt;/code&gt; are the two claims people skip. Your auth service may issue tokens for &lt;code&gt;api.example.com&lt;/code&gt; while the service you are calling is configured to accept &lt;code&gt;*.internal&lt;/code&gt;. Paste the token, read &lt;code&gt;aud&lt;/code&gt; and &lt;code&gt;iss&lt;/code&gt;, and compare them character by character with the validator's config. It is almost always one environment variable.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Key rotation and the kid header
&lt;/h2&gt;

&lt;p&gt;When a provider rotates signing keys, the token's header carries a &lt;code&gt;kid&lt;/code&gt; (key id) saying which key signed it. If your validator only loads the old key, every freshly issued token fails while older ones still work - or the exact reverse. Compare the &lt;code&gt;kid&lt;/code&gt; in the header with the keys your verifier actually has loaded.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The algorithm is mis-declared
&lt;/h2&gt;

&lt;p&gt;A token signed with RS256 but handed to a service pinned to HS256 fails even though the payload is perfect. Worse: if the service reads &lt;code&gt;alg&lt;/code&gt; from the token itself, an &lt;code&gt;"alg": "none"&lt;/code&gt; token lets an attacker strip the signature entirely. Pin the accepted algorithm server-side and never trust the header's &lt;code&gt;alg&lt;/code&gt; field.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. You are testing the wrong copy of the token
&lt;/h2&gt;

&lt;p&gt;Embarrassingly common. The token in your clipboard came from a truncated log line, an earlier request, or a different user's session. Decode the exact string you are sending, not the one you think you are sending.&lt;/p&gt;

&lt;p&gt;The point is that decoding is free and instant, and it removes guesswork. I use &lt;a href="https://codetoolbox.pro/tools/jwt-decoder.html" rel="noopener noreferrer"&gt;CodeToolbox's JWT Decoder&lt;/a&gt; - it runs entirely in the browser with &lt;code&gt;atob&lt;/code&gt; and &lt;code&gt;JSON.parse&lt;/code&gt;, so the token never leaves your machine, which matters because a JWT is a bearer credential. It renders &lt;code&gt;exp&lt;/code&gt;/&lt;code&gt;iat&lt;/code&gt;/&lt;code&gt;nbf&lt;/code&gt; as readable dates and flags unsecured &lt;code&gt;alg: none&lt;/code&gt; tokens.&lt;/p&gt;

&lt;p&gt;Decode anywhere; verify the signature server-side with a real library.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>programming</category>
    </item>
    <item>
      <title>Your Regex Works in Python but Breaks in JavaScript - Here's Why</title>
      <dc:creator>zhihu wu</dc:creator>
      <pubDate>Thu, 01 Oct 2026 13:01:41 +0000</pubDate>
      <link>https://dev.to/zhihu_wu_dea1d82af01a04d7/your-regex-works-in-python-but-breaks-in-javascript-heres-why-54gg</link>
      <guid>https://dev.to/zhihu_wu_dea1d82af01a04d7/your-regex-works-in-python-but-breaks-in-javascript-heres-why-54gg</guid>
      <description>&lt;p&gt;You write a pattern, it passes your tests in a Python service, and then the same string in a Node script returns &lt;code&gt;null&lt;/code&gt;. Regular expressions are "the same everywhere" right up until they aren't — and the differences are exactly the ones that cost you an afternoon.&lt;/p&gt;

&lt;p&gt;Here are the four that bite hardest.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Named group syntax is not shared
&lt;/h2&gt;

&lt;p&gt;Python uses &lt;code&gt;(?P&amp;lt;year&amp;gt;\d{4})&lt;/code&gt;. JavaScript uses &lt;code&gt;(?&amp;lt;year&amp;gt;\d{4})&lt;/code&gt;. Paste the Python version into a &lt;code&gt;.js&lt;/code&gt; file and you get a &lt;code&gt;SyntaxError: Invalid regular expression&lt;/code&gt; with no line number pointing at the offending group.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;code&gt;\d&lt;/code&gt; and &lt;code&gt;\w&lt;/code&gt; disagree about Unicode
&lt;/h2&gt;

&lt;p&gt;In Python 3, &lt;code&gt;str&lt;/code&gt; patterns are Unicode-aware by default: &lt;code&gt;re.findall(r'\w+', 'café')&lt;/code&gt; returns &lt;code&gt;['café']&lt;/code&gt;. In JavaScript, &lt;code&gt;\w&lt;/code&gt; is ASCII-only — even with the &lt;code&gt;u&lt;/code&gt; flag — so &lt;code&gt;'café'.match(/\w+/g)&lt;/code&gt; stops at &lt;code&gt;caf&lt;/code&gt;. If you need Unicode categories in JS, you must spell it out with property escapes: &lt;code&gt;\p{L}&lt;/code&gt;, &lt;code&gt;\p{Nd}&lt;/code&gt;. Python spells them &lt;code&gt;\w&lt;/code&gt; and &lt;code&gt;\d&lt;/code&gt; and calls it a day.&lt;/p&gt;

&lt;p&gt;Rules extracted from a mixed-language codebase break in whichever language you didn't test.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Lookbehind: fixed width vs variable length
&lt;/h2&gt;

&lt;p&gt;Python's &lt;code&gt;re&lt;/code&gt; requires lookbehinds to be fixed width. &lt;code&gt;(?&amp;lt;=foo|foobar)&lt;/code&gt; raises &lt;code&gt;error: look-behind requires fixed-width pattern&lt;/code&gt;. JavaScript's engine accepts variable-length lookbehinds — and Safari only shipped them in 16.4, so a pattern that passes in Chrome can throw on an older iOS client.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. &lt;code&gt;match()&lt;/code&gt; does not mean the same thing
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;re.match()&lt;/code&gt; anchors at position zero; &lt;code&gt;re.search()&lt;/code&gt; doesn't. JavaScript's &lt;code&gt;String.prototype.match()&lt;/code&gt; behaves like Python's &lt;code&gt;search&lt;/code&gt;, not its &lt;code&gt;match&lt;/code&gt;. That single-word difference is behind a lot of "but it matched the whole string in my test!" confusion. If you need Python's strict behaviour in JS, anchor both ends explicitly — &lt;code&gt;^...$&lt;/code&gt; — because JavaScript has no &lt;code&gt;fullmatch&lt;/code&gt; equivalent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: test the pattern, not the language
&lt;/h2&gt;

&lt;p&gt;None of these are hard once you see them — the trick is seeing them in seconds instead of in production. I keep a regex tester open in a browser tab while writing patterns: paste the pattern and a sample of the real input, watch the matches highlight as you type, and check the capture groups panel before the pattern ever lands in code. It uses the browser's own engine, so what you see is exactly what JavaScript will do with it — no round-trip to a server, and nothing in your test string leaves the machine (which matters when the sample is a log line full of tokens).&lt;/p&gt;

&lt;p&gt;The PyPI-style patterns I copy into JS now get a two-second sanity check first. That's the whole habit, and it has saved me more debugs than any linter.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I use &lt;a href="https://codetoolbox.pro/tools/regex-tester.html" rel="noopener noreferrer"&gt;CodeToolbox's Regex Tester&lt;/a&gt; — it is free, runs entirely client-side, and has preset patterns for email, URL, IPv4 and dates that you can bend into shape instead of starting from an empty box.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>python</category>
      <category>regex</category>
      <category>webdev</category>
    </item>
    <item>
      <title>camelCase vs snake_case: The Naming Bug That Survives Code Review</title>
      <dc:creator>zhihu wu</dc:creator>
      <pubDate>Tue, 29 Sep 2026 13:01:41 +0000</pubDate>
      <link>https://dev.to/zhihu_wu_dea1d82af01a04d7/camelcase-vs-snakecase-the-naming-bug-that-survives-code-review-4epi</link>
      <guid>https://dev.to/zhihu_wu_dea1d82af01a04d7/camelcase-vs-snakecase-the-naming-bug-that-survives-code-review-4epi</guid>
      <description>&lt;p&gt;Every team eventually ships a payload like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-01T10:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"IsActive"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three fields, three conventions. It passes code review because every line looks fine in isolation. It breaks in production because every consumer now has to special-case field names.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode is silence
&lt;/h2&gt;

&lt;p&gt;The expensive version of this bug does not throw. Your backend is written in Python with snake_case columns, the frontend is TypeScript and sends camelCase, and the serializer you assumed existed... does not. So &lt;code&gt;userId&lt;/code&gt; arrives, nothing reads it, and the handler writes &lt;code&gt;user_id: null&lt;/code&gt;. No 422, no stack trace — just rows that look wrong three weeks later.&lt;/p&gt;

&lt;p&gt;Both ends were internally consistent. That is what makes it survive review: naming style is a contract between two codebases, and nothing in either repo checks the contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the two styles come from
&lt;/h2&gt;

&lt;p&gt;It is not a style disagreement, it is two ecosystems meeting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SQL, Python and Ruby idioms use snake_case: &lt;code&gt;user_id&lt;/code&gt;, &lt;code&gt;created_at&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;JavaScript, Java and C# idioms use camelCase / PascalCase: &lt;code&gt;userId&lt;/code&gt;, &lt;code&gt;createdAt&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the boundary belongs to one explicit layer — a serializer, a DTO, a mapper. When that layer is missing, ORM column names leak straight into your public JSON, and the API's naming becomes whatever the database happens to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Round-trip it, don't eyeball it
&lt;/h2&gt;

&lt;p&gt;Pick one convention per boundary and assert it in a test. Cheapest version: serialize a sample object, convert each key, and compare. Or snapshot one real response body in CI, so an accidental &lt;code&gt;created_at&lt;/code&gt; shows up as a diff instead of a support ticket.&lt;/p&gt;

&lt;p&gt;Two traps that break naive converters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Acronyms split inconsistently.&lt;/strong&gt; &lt;code&gt;parseHTTPResponse&lt;/code&gt; becomes &lt;code&gt;parse_httpresponse&lt;/code&gt; or &lt;code&gt;parse_http_response&lt;/code&gt; depending on the rule. Round-trip both directions in a test before trusting a bulk rename.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filesystem casing.&lt;/strong&gt; Renaming &lt;code&gt;UserService.ts&lt;/code&gt; to &lt;code&gt;userservice.ts&lt;/code&gt; builds fine on a case-insensitive macOS volume and fails on a case-sensitive Linux CI. Same root cause: casing treated as decoration instead of part of the name.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The practical rule
&lt;/h2&gt;

&lt;p&gt;One canonical spelling per layer — &lt;code&gt;user_id&lt;/code&gt; in Postgres, &lt;code&gt;userId&lt;/code&gt; in JSON, &lt;code&gt;USER_ID&lt;/code&gt; in the env file — and no variants in between. When I need to flip a list of identifiers between conventions (column names → JSON keys, or the reverse before a migration), I use the Case Converter at &lt;a href="https://codetoolbox.pro/tools/case-converter.html" rel="noopener noreferrer"&gt;codetoolbox.pro&lt;/a&gt; — paste the whole list once and it returns nine casings at the same time, running entirely in the browser, so nothing internal leaves the machine.&lt;/p&gt;

&lt;p&gt;What naming convention does your team enforce — and do you enforce it with a test, or with a convention doc nobody rereads?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why Your Diff Shows Every Line Changed (And It's Not Your Code)</title>
      <dc:creator>zhihu wu</dc:creator>
      <pubDate>Sat, 26 Sep 2026 13:02:11 +0000</pubDate>
      <link>https://dev.to/zhihu_wu_dea1d82af01a04d7/why-your-diff-shows-every-line-changed-and-its-not-your-code-19f8</link>
      <guid>https://dev.to/zhihu_wu_dea1d82af01a04d7/why-your-diff-shows-every-line-changed-and-its-not-your-code-19f8</guid>
      <description>&lt;p&gt;You open a pull request. You changed one line. The diff is 412 lines.&lt;/p&gt;

&lt;p&gt;You blame the linter, then yourself. Usually it's neither. A diff compares characters before it compares meaning, and the characters that changed are invisible.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Line endings: CRLF vs LF
&lt;/h2&gt;

&lt;p&gt;The classic culprit. A file written on Linux ends its lines with &lt;code&gt;\n&lt;/code&gt;; a file written on Windows ends them with &lt;code&gt;\r\n&lt;/code&gt;. Git stores bytes, so the moment a teammate's editor (or a script, or a Windows tool) rewrites the file with the other convention, every single line differs by one byte you cannot see.&lt;/p&gt;

&lt;p&gt;To confirm it, run &lt;code&gt;git diff --ignore-cr-at-eol&lt;/code&gt;. If the 412-line diff collapses to your one real change, that's your answer. When the two versions are not in git yet — two config files, two API response dumps — I paste both into the &lt;a href="https://codetoolbox.pro/tools/diff-checker.html" rel="noopener noreferrer"&gt;Diff Checker&lt;/a&gt; and flip the "ignore whitespace" toggle. It runs entirely in the browser, so pasting a private config is not a concern.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Trailing whitespace
&lt;/h2&gt;

&lt;p&gt;Editors with "trim trailing whitespace on save" enabled rewrite every line they touch, and any formatter run rewrites the whole file. You get lines marked as changed that look byte-identical, because the difference sits at the end, past where your eye stops rendering.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Tabs vs spaces, and indent conversion
&lt;/h2&gt;

&lt;p&gt;One contributor's editor converts indentation on save. Every nested block now reads as "modified". &lt;code&gt;git diff -w&lt;/code&gt; (ignore all whitespace) collapses it instantly — and if it does, you know the change is cosmetic.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Encoding and BOM
&lt;/h2&gt;

&lt;p&gt;A BOM added by a Windows editor, or a latin-1 → UTF-8 rewrite, changes every line containing a non-ASCII character. &lt;code&gt;file&lt;/code&gt; on both versions tells you the encoding, and a suspiciously huge &lt;code&gt;git diff --stat&lt;/code&gt; on a mostly-ASCII file is the hint.&lt;/p&gt;

&lt;h2&gt;
  
  
  The habit that fixes it permanently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.gitattributes&lt;/code&gt; with &lt;code&gt;* text=auto eol=lf&lt;/code&gt; normalizes line endings at commit time, once, for everyone.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.editorconfig&lt;/code&gt; with &lt;code&gt;end_of_line = lf&lt;/code&gt;, &lt;code&gt;insert_final_newline = true&lt;/code&gt;, &lt;code&gt;trim_trailing_whitespace = true&lt;/code&gt; so every editor agrees on the rules.&lt;/li&gt;
&lt;li&gt;Review with &lt;code&gt;git diff -w&lt;/code&gt; before you push. If the diff disappears, the change is whitespace-only: revert it or commit it on its own, so the real change stays reviewable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A diff never lies — it compares characters. When its report looks impossible, check the invisible ones first: end-of-line bytes, trailing spaces, indentation, encoding. A whitespace-aware diff view is the fastest way to stop guessing and get back to your actual change.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>git</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>A Day Is Not 86400 Seconds: The DST Bug in Your Date Math</title>
      <dc:creator>zhihu wu</dc:creator>
      <pubDate>Thu, 24 Sep 2026 13:02:29 +0000</pubDate>
      <link>https://dev.to/zhihu_wu_dea1d82af01a04d7/a-day-is-not-86400-seconds-the-dst-bug-in-your-date-math-3bc0</link>
      <guid>https://dev.to/zhihu_wu_dea1d82af01a04d7/a-day-is-not-86400-seconds-the-dst-bug-in-your-date-math-3bc0</guid>
      <description>&lt;p&gt;Most "date bugs" I've had to chase in production weren't off by a timezone. They were off by exactly one day, and they only appeared twice a year.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// "tomorrow, same time"&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tomorrow&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;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;86400&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;That code is correct 363 days a year. On the other two it is off by an hour, and if you round the result down to a date it is off by a whole day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The epoch is absolute; calendar units are not
&lt;/h2&gt;

&lt;p&gt;A Unix timestamp is a fixed instant. &lt;code&gt;1758700800&lt;/code&gt; is the same moment in Lagos, Lisbon and Lima - only the rendered local time differs. Arithmetic in seconds is therefore always exact in absolute time, and that is the trap: "one day later" is not an absolute duration. It is a calendar operation whose length depends on the timezone rules in effect at both moments.&lt;/p&gt;

&lt;p&gt;A spring-forward day is 82,800 seconds long; a fall-back day is 90,000. Add exactly 86,400 seconds to local midnight and you land at 01:00 (already past the boundary you wanted) or at 23:00 of the day before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same bug, four languages
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// adds exactly 86400 s - wrong across a DST boundary&lt;/span&gt;
&lt;span class="nc"&gt;Instant&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;plus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofDays&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="c1"&gt;// calendar-aware - what you usually mean&lt;/span&gt;
&lt;span class="nc"&gt;ZonedDateTime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;zone&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;plusDays&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&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;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;86400&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="c1"&gt;// +86400 s exactly&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// one calendar day&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go's &lt;code&gt;t.Add(24 * time.Hour)&lt;/code&gt; is a duration; &lt;code&gt;t.AddDate(0, 0, 1)&lt;/code&gt; is a calendar day. In SQL, &lt;code&gt;date_add(d, interval 1 day)&lt;/code&gt; follows the zone while &lt;code&gt;d + interval 86400 second&lt;/code&gt; does not. Python is the sneakiest: &lt;code&gt;dt + timedelta(days=1)&lt;/code&gt; is wall-clock arithmetic on naive datetimes, but adding timedelta to an aware datetime in DST-aware zones can still surprise you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually fixes it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Store the absolute instant - epoch seconds or UTC ISO 8601. Timestamps do not have DST.&lt;/li&gt;
&lt;li&gt;Make "which calendar day is this?" decisions in the user's timezone (the zone, not an offset: an offset is a snapshot, a zone is a rule set with a history).&lt;/li&gt;
&lt;li&gt;For "yesterday's rows", compute the day boundaries as calendar days in that zone and convert those boundaries to instants - instead of subtracting 86400 from now.&lt;/li&gt;
&lt;li&gt;Put a DST date in your test suite. Twice a year is exactly often enough to forget.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When one of these bites, the first move is converting the raw epoch values in the logs to local time. I use the &lt;a href="https://codetoolbox.pro/tools/timestamp-converter.html" rel="noopener noreferrer"&gt;timestamp converter on CodeToolbox&lt;/a&gt; for that - it shows local and UTC side by side and auto-detects seconds vs milliseconds, which is the other half of this bug family, and it runs entirely in the browser so staging log values never leave the machine.&lt;/p&gt;

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