<?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: Black</title>
    <description>The latest articles on DEV Community by Black (@blackspectre512).</description>
    <link>https://dev.to/blackspectre512</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%2F4201323%2F2e98c928-8e91-42aa-91fc-437ad74a29d5.png</url>
      <title>DEV Community: Black</title>
      <link>https://dev.to/blackspectre512</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/blackspectre512"/>
    <language>en</language>
    <item>
      <title>What a dog or cat really costs: year one, every year after, and the vet bill nobody budgets</title>
      <dc:creator>Black</dc:creator>
      <pubDate>Mon, 26 Oct 2026 08:06:34 +0000</pubDate>
      <link>https://dev.to/blackspectre512/what-a-dog-or-cat-really-costs-year-one-every-year-after-and-the-vet-bill-nobody-budgets-30jl</link>
      <guid>https://dev.to/blackspectre512/what-a-dog-or-cat-really-costs-year-one-every-year-after-and-the-vet-bill-nobody-budgets-30jl</guid>
      <description>&lt;p&gt;The adoption fee is the smallest part of owning a pet. What adds up is the second year, and every year after that. Here's what a dog or cat costs in a typical US home in 2026, split the way you'd budget for it. The free &lt;a href="https://blacksgit.github.io/pet-cost/" rel="noopener noreferrer"&gt;pet cost calculator&lt;/a&gt; does the same for your own choices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Year one vs every year after
&lt;/h2&gt;

&lt;p&gt;For a medium dog adopted from a shelter, with regular food, home grooming, no insurance and family covering travel:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Per year&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One-time: adoption&lt;/td&gt;
&lt;td&gt;$250&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One-time: spay or neuter, first vaccines, microchip&lt;/td&gt;
&lt;td&gt;$150&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One-time: starter supplies&lt;/td&gt;
&lt;td&gt;$300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Food&lt;/td&gt;
&lt;td&gt;$540&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Routine vet and vaccines&lt;/td&gt;
&lt;td&gt;$350&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flea, tick, heartworm prevention&lt;/td&gt;
&lt;td&gt;$320&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Treats, toys, chews&lt;/td&gt;
&lt;td&gt;$180&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grooming at home&lt;/td&gt;
&lt;td&gt;$40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License and misc.&lt;/td&gt;
&lt;td&gt;$60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;First year&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$2,190&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Every year after&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$1,490 (about $124 a month)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The choices that move it most
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Breeder vs shelter.&lt;/strong&gt; A breeder puppy often costs $2,000 or more. A shelter adoption usually includes spay or neuter and first vaccines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Boarding or a dog walker.&lt;/strong&gt; If you travel or work long days, this can be the single biggest line, often $900 a year or more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Size.&lt;/strong&gt; A large dog eats roughly twice what a small one does, and preventives are priced by weight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Professional grooming.&lt;/strong&gt; For some breeds it isn't optional, and it runs several hundred dollars a year.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Insurance.&lt;/strong&gt; Roughly $30 to $75 a month. It turns a surprise bill into a deductible.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The number nobody budgets: the emergency vet visit
&lt;/h2&gt;

&lt;p&gt;One unplanned visit (a swallowed sock, a torn nail, a limp after the dog park) often costs $1,500 to $2,500 by the time tests are done. Keep that much in a separate fund, or keep your insurance deductible plus the first bill on hand until the claim pays out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lifetime number
&lt;/h2&gt;

&lt;p&gt;Over a typical 12-year life, the medium dog above costs about $18,600. That's worth knowing before you choose, not after.&lt;/p&gt;

&lt;p&gt;For keeping track once they're home, my printable &lt;a href="https://payhip.com/b/h3avA" rel="noopener noreferrer"&gt;Pet Care Log and Expense Tracker&lt;/a&gt; has a pet profile with emergency info, vet visits, vaccines and preventives, a medication log, a feeding routine and a monthly cost page.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Middle-of-the-road US prices for planning. Your area and your pet's breed change these a lot.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>beginners</category>
      <category>discuss</category>
      <category>career</category>
    </item>
    <item>
      <title>Recipe cost per serving: unit conversion without lying about cups vs grams</title>
      <dc:creator>Black</dc:creator>
      <pubDate>Mon, 26 Oct 2026 08:06:12 +0000</pubDate>
      <link>https://dev.to/blackspectre512/recipe-cost-per-serving-unit-conversion-without-lying-about-cups-vs-grams-4gbi</link>
      <guid>https://dev.to/blackspectre512/recipe-cost-per-serving-unit-conversion-without-lying-about-cups-vs-grams-4gbi</guid>
      <description>&lt;p&gt;A recipe cost calculator sounds trivial: price times quantity. Then you meet the real input. The beans come in a 15.5 oz can and the recipe wants 31 oz. The chili powder is sold by weight and used by the tablespoon. The onions are sold "each". Here's how my free &lt;a href="https://blacksgit.github.io/recipe-cost/" rel="noopener noreferrer"&gt;recipe cost calculator&lt;/a&gt; handles units, in about ten lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  One table, three kinds of unit
&lt;/h2&gt;

&lt;p&gt;Every unit converts to a base unit of its own kind: grams for weight, millilitres for volume, and 1 for things you count.&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;U&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;g&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;w&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="na"&gt;kg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;w&lt;/span&gt;&lt;span class="dl"&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="na"&gt;oz&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;w&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;28.3495&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;lb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;w&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;453.592&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;ml&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;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="na"&gt;l&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;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;1000&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;tsp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;v&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;4.929&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;tbsp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;v&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;14.787&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;cup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;v&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;236.6&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fl oz&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;v&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;29.574&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;each&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;e&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;h2&gt;
  
  
  Cost of one ingredient
&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;function&lt;/span&gt; &lt;span class="nf"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sizeUnit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;used&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;usedUnit&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;kindA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;U&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;sizeUnit&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;kindB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;U&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;usedUnit&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;kindA&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;kindB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// weight vs volume: can't convert&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;used&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;b&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;size&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;a&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;Example: a $1.09 can of 15.5 oz beans, and the recipe uses 31 oz. That's &lt;code&gt;1.09 x 31 / 15.5 = $2.18&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why weight-to-volume returns null
&lt;/h2&gt;

&lt;p&gt;A cup of flour weighs about 125 g. A cup of sugar weighs about 200 g. A cup of water weighs 237 g. Any single conversion factor would be wrong for most foods. So instead of guessing, the tool says which side is a weight and which is a volume, and asks the user to pick matching units. Use the package's serving info, or weigh the ingredient once. An honest "I can't" beats a confident wrong price.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two numbers people want
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cost per serving = sum of ingredient costs / servings
yearly saving    = (takeout price per serving - cost per serving) x servings x 52
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a 6-serving pot of chili at $14.56 ($2.43 a serving) against $14 takeout, making it once a week saves about $3,600 a year. The tool also shows which ingredient takes the biggest share of the cost. Here it's the ground beef, at 45%. That's the first place to look for savings.&lt;/p&gt;

&lt;p&gt;To plan a whole week of meals, my &lt;a href="https://payhip.com/b/GDO0m" rel="noopener noreferrer"&gt;Meal Planner&lt;/a&gt; spreadsheet (Excel and Google Sheets) works out cost per serving for every recipe and builds the grocery list from the week plan.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>beginners</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>The 52-week savings challenge, solved for any goal (and two JavaScript date bugs)</title>
      <dc:creator>Black</dc:creator>
      <pubDate>Mon, 26 Oct 2026 08:05:55 +0000</pubDate>
      <link>https://dev.to/blackspectre512/the-52-week-savings-challenge-solved-for-any-goal-and-two-javascript-date-bugs-50of</link>
      <guid>https://dev.to/blackspectre512/the-52-week-savings-challenge-solved-for-any-goal-and-two-javascript-date-bugs-50of</guid>
      <description>&lt;p&gt;The 52-week savings challenge is simple: save $1 in week 1, $2 in week 2, and so on, up to $52 in week 52. At the end you have $1,378. That's a nice number, but it's probably not your goal. Here's the bit of math that lets you rebuild the challenge for any goal and any end date. I put it in a free &lt;a href="https://blacksgit.github.io/savings-challenge/" rel="noopener noreferrer"&gt;savings challenge maker&lt;/a&gt; with weeks you tap off as you save.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where $1,378 comes from
&lt;/h2&gt;

&lt;p&gt;Week amounts of 1, 2, 3 ... n add up to a triangular number:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 + 2 + ... + n = n(n + 1) / 2
52 x 53 / 2 = 1,378
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Solve it backwards
&lt;/h2&gt;

&lt;p&gt;Let each week grow by a step &lt;code&gt;s&lt;/code&gt;, so the amounts are s, 2s, 3s ... ns. The total is &lt;code&gt;s · n(n+1)/2&lt;/code&gt;. Set that equal to your goal G and solve for s:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s = 2G / (n(n + 1))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example: $2,000 in 52 weeks gives &lt;code&gt;s = 4000 / 2756 ≈ 1.45&lt;/code&gt;. So week 1 is about $1, week 2 about $3, and week 52 about $75.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rounding without missing the goal
&lt;/h2&gt;

&lt;p&gt;Nobody wants to save $1.45. Round each week to whole dollars, and the total drifts by a few dollars. The fix is to let one week absorb the difference:&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;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;goal&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;weeks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="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;step&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&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;diff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;goal&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;weeks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&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;weeks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;            &lt;span class="c1"&gt;// the last (biggest) week absorbs it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the reverse challenge, where the big weeks come first, reverse the array and let week 1 absorb it instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Number of weeks from two dates
&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;n&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;end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;start&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;7&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&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;Two date bugs are worth knowing about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;new Date("2026-10-26")&lt;/code&gt; is parsed as UTC midnight. In US time zones that is the evening of the 25th, so every week label shows one day early. Use &lt;code&gt;new Date("2026-10-26T00:00")&lt;/code&gt; to get local midnight.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Math.floor(days / 7) + 1&lt;/code&gt; gives 53 weeks for a 364-day span. &lt;code&gt;Math.round&lt;/code&gt; gives 52.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Which shape to pick
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Same each week:&lt;/strong&gt; $2,000 / 52 = about $38. The easiest to automate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Growing:&lt;/strong&gt; small at first, so it's easy to start, with the big weeks at the end. The classic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reverse:&lt;/strong&gt; big weeks first. Good when the end of the plan lands in December, when money is tight anyway.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you'd rather color it in on paper, my printable &lt;a href="https://payhip.com/b/3oEAU" rel="noopener noreferrer"&gt;Savings Challenge Tracker&lt;/a&gt; has the 52-week and reverse 52-week challenges, a $1,000 goal tracker, the $5 bill challenge and a no-spend days page.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>beginners</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Truck, container or movers? What a move really costs, plus the bills nobody quotes</title>
      <dc:creator>Black</dc:creator>
      <pubDate>Mon, 26 Oct 2026 08:05:29 +0000</pubDate>
      <link>https://dev.to/blackspectre512/truck-container-or-movers-what-a-move-really-costs-plus-the-bills-nobody-quotes-1ooj</link>
      <guid>https://dev.to/blackspectre512/truck-container-or-movers-what-a-move-really-costs-plus-the-bills-nobody-quotes-1ooj</guid>
      <description>&lt;p&gt;Moving quotes cover the truck or the crew. The full cost of a move is often half again as much, because the rest of it never shows up on a quote. Here's how to compare the three ways to move, and the list of costs that catch people out. To run it for your own home size and distance, use the free &lt;a href="https://blacksgit.github.io/moving-cost/" rel="noopener noreferrer"&gt;moving cost calculator&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to move
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;You do&lt;/th&gt;
&lt;th&gt;They do&lt;/th&gt;
&lt;th&gt;Best when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rental truck&lt;/td&gt;
&lt;td&gt;Pack, load (with paid helpers), drive&lt;/td&gt;
&lt;td&gt;Rent you the truck&lt;/td&gt;
&lt;td&gt;Short distance, small home, you can take the days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Moving container&lt;/td&gt;
&lt;td&gt;Pack, load&lt;/td&gt;
&lt;td&gt;Drive it, store it if needed&lt;/td&gt;
&lt;td&gt;Long distance, flexible dates, gap between homes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full-service movers&lt;/td&gt;
&lt;td&gt;Pack (or pay extra)&lt;/td&gt;
&lt;td&gt;Load, drive, unload&lt;/td&gt;
&lt;td&gt;Big home, stairs, little time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Rough 2026 US ranges for a 2-bedroom:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Distance&lt;/th&gt;
&lt;th&gt;Truck&lt;/th&gt;
&lt;th&gt;Container&lt;/th&gt;
&lt;th&gt;Movers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;15 miles&lt;/td&gt;
&lt;td&gt;$640 to $900&lt;/td&gt;
&lt;td&gt;$1,010 to $1,430&lt;/td&gt;
&lt;td&gt;$990 to $1,390&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;450 miles&lt;/td&gt;
&lt;td&gt;$1,590 to $2,240&lt;/td&gt;
&lt;td&gt;$1,810 to $2,550&lt;/td&gt;
&lt;td&gt;$2,920 to $4,130&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,500 miles&lt;/td&gt;
&lt;td&gt;$2,810 to $3,970&lt;/td&gt;
&lt;td&gt;$3,150 to $4,440&lt;/td&gt;
&lt;td&gt;$6,490 to $9,170&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These numbers include boxes and supplies, and paid helpers for the truck and container. For local moves, movers usually charge by the hour. For long moves, they charge by weight and distance, which is why the gap widens with distance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The costs people forget
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deposit and first month&lt;/strong&gt; at the new place. Often the biggest single number in the whole move.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overlap rent.&lt;/strong&gt; A lease that ends on the 31st and a new one that starts on the 1st leave no room for a moving day.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Utility setup and internet install fees.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cleaning and small repairs&lt;/strong&gt; to get your old deposit back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unpaid days off.&lt;/strong&gt; Moving yourself usually takes one or two days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The road trip.&lt;/strong&gt; On long moves, plan on a hotel and meals for roughly every 500 miles.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the calculator's default example (a 2-bedroom moving 450 miles), these add about $3,700 to a truck move of roughly $1,900. That is about two-thirds of the total.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to keep it down
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Avoid summer and month-ends&lt;/strong&gt; if you can. Demand, and prices, peak then.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Get three quotes.&lt;/strong&gt; For long-distance movers, check that they have a USDOT number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sell or donate before you pack.&lt;/strong&gt; Long-distance moves are priced by weight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask for the deposit back in writing,&lt;/strong&gt; with a move-out checklist from the landlord.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For the planning side, my printable &lt;a href="https://payhip.com/b/U5l4b" rel="noopener noreferrer"&gt;Moving Checklist and Budget Planner&lt;/a&gt; has an 8-week countdown, a moving budget with these forgotten costs, an address change list, a room-by-room box log and a moving-day plan.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Rough averages for comparing options, not quotes. Season, stairs and your area move prices a lot.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>beginners</category>
      <category>discuss</category>
      <category>career</category>
    </item>
    <item>
      <title>What a baby's first year really costs, split into the three buckets you can plan for</title>
      <dc:creator>Black</dc:creator>
      <pubDate>Mon, 26 Oct 2026 08:05:04 +0000</pubDate>
      <link>https://dev.to/blackspectre512/what-a-babys-first-year-really-costs-split-into-the-three-buckets-you-can-plan-for-13ol</link>
      <guid>https://dev.to/blackspectre512/what-a-babys-first-year-really-costs-split-into-the-three-buckets-you-can-plan-for-13ol</guid>
      <description>&lt;p&gt;Ask how much a baby costs in the first year and you get one big number with no breakdown. That number doesn't help you plan, because the first year isn't spread evenly. A large part of it comes before the birth, and childcare can triple the monthly cost the day it starts.&lt;/p&gt;

&lt;p&gt;Here's a way to break it down that you can actually budget against. There's also a free &lt;a href="https://blacksgit.github.io/baby-cost/" rel="noopener noreferrer"&gt;baby first-year cost calculator&lt;/a&gt; that does it as you tap through your choices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Split it into three buckets
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Before the birth (one-time)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The birth bill: what your insurance leaves you to pay. People with insurance often pay close to $3,000. Check your deductible and out-of-pocket maximum, because that maximum is the most you can owe.&lt;/li&gt;
&lt;li&gt;Gear: crib and mattress, car seat, stroller, nursery basics. About $1,400 mostly new, $800 mixed, $350 mostly used or gifted. Buy the car seat new.&lt;/li&gt;
&lt;li&gt;Cloth diapers, if you go that way: about $400 up front.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Every month&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Typical per month&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Formula (or ~$20 breastfeeding supplies)&lt;/td&gt;
&lt;td&gt;~$200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disposable diapers and wipes (cloth: ~$25 for washing)&lt;/td&gt;
&lt;td&gt;~$85&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clothes (new / hand-me-downs)&lt;/td&gt;
&lt;td&gt;~$50 / ~$15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extra insurance premium for the baby&lt;/td&gt;
&lt;td&gt;varies, often $100 to $200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medicine, bath, toiletries&lt;/td&gt;
&lt;td&gt;~$40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Solid food from month 6&lt;/td&gt;
&lt;td&gt;~$50&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;3. Childcare, from the month it starts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the biggest swing in the whole budget: about $900 to $2,000 a month for full-time care, depending on your state and the type of care. A daycare center at $1,400 a month that starts in month 4 adds $12,600 to the first year. Use your own quote here, not an average.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn it into a savings plan
&lt;/h2&gt;

&lt;p&gt;Add up bucket 1, then divide by the months left until the due date:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;monthly saving = (birth bill + gear + one-time supplies) / months until due
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a $3,000 birth bill and $860 of gear and supplies, five months out, that is about $780 a month. After the birth, the monthly buckets take over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where families get surprised
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The month childcare starts.&lt;/strong&gt; The monthly cost can jump from around $500 to almost $2,000 overnight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The birth bill shows up weeks later,&lt;/strong&gt; often as several bills from the hospital, doctors and anesthesia. Check each one against your insurer's explanation of benefits before paying.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A breast pump is often free.&lt;/strong&gt; Many US plans cover one, so ask before you buy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you'd like this on paper, my printable &lt;a href="https://payhip.com/b/vVl2c" rel="noopener noreferrer"&gt;First-Year Baby Budget Planner&lt;/a&gt; has cost ranges for each item, a savings goal tracker, a monthly baby budget and a hospital bill checklist.&lt;/p&gt;

&lt;p&gt;I post free tools like this regularly on &lt;a href="https://patreon.com/123black" rel="noopener noreferrer"&gt;patreon.com/123black&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Typical US prices for planning, not financial advice. Your plan and your area change these a lot.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>beginners</category>
      <category>career</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Debt snowball vs avalanche: settling it with a 40-line JavaScript simulation</title>
      <dc:creator>Black</dc:creator>
      <pubDate>Mon, 26 Oct 2026 07:58:27 +0000</pubDate>
      <link>https://dev.to/blackspectre512/debt-snowball-vs-avalanche-settling-it-with-a-40-line-javascript-simulation-2elp</link>
      <guid>https://dev.to/blackspectre512/debt-snowball-vs-avalanche-settling-it-with-a-40-line-javascript-simulation-2elp</guid>
      <description>&lt;p&gt;Personal finance arguments about the debt snowball and the debt avalanche usually stay abstract. They are easy to settle with a month-by-month simulation. Here is the one behind my free &lt;a href="https://blacksgit.github.io/debt-payoff/" rel="noopener noreferrer"&gt;snowball vs avalanche calculator&lt;/a&gt;, which runs entirely in the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two rules
&lt;/h2&gt;

&lt;p&gt;Both methods do the same three things every month:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pay the minimum on every debt.&lt;/li&gt;
&lt;li&gt;Put all extra money on one target debt.&lt;/li&gt;
&lt;li&gt;When a debt is paid off, roll its whole payment into the next target.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The only difference is the order. &lt;strong&gt;Snowball&lt;/strong&gt; targets the smallest balance first. &lt;strong&gt;Avalanche&lt;/strong&gt; targets the highest interest rate first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The simulation
&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;// debts: [{name, balance, apr, min}], order: indexes in target order&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;simulate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;debts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;extra&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;bal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;debts&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;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;balance&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;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;debts&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;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apr&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;           &lt;span class="c1"&gt;// monthly rate&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;budget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;debts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;d&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;extra&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;month&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;interest&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;paidOff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.005&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;month&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;month&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// 1. interest accrues&lt;/span&gt;
    &lt;span class="nx"&gt;bal&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;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;b&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="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="nx"&gt;bal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;interest&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;k&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;// 2. minimums&lt;/span&gt;
    &lt;span class="nx"&gt;bal&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;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;debts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nx"&gt;bal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="nx"&gt;p&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;// 3. everything left goes to the target, then the next one&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;order&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;left&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="k"&gt;break&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;bal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nx"&gt;bal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;bal&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;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;0.005&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="nx"&gt;paidOff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;paidOff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;month&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;interest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;paidOff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stuck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;month&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;600&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 "rollover" in step 3 needs no special code. The monthly budget stays fixed, so when a debt reaches zero, the money that used to pay its minimum is still in &lt;code&gt;left&lt;/code&gt;, and the loop sends it to the next target.&lt;/p&gt;

&lt;p&gt;Order is just a sort:&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;snowball&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;debts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;a&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;debts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;balance&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;avalanche&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;debts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;apr&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;debts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;apr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Sort a copy for each one. &lt;code&gt;sort&lt;/code&gt; changes the array in place.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Edge cases that matter
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Payments that never catch up.&lt;/strong&gt; If the minimums don't cover the monthly interest, the loop would run forever. The 600-month cap turns that into a "stuck" result, and the UI tells the user to raise the extra amount.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Floating-point dust.&lt;/strong&gt; Treat any balance under half a cent as paid. Otherwise a debt can sit at &lt;code&gt;0.0000001&lt;/code&gt; and stay "open" for an extra month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The final month.&lt;/strong&gt; &lt;code&gt;Math.min(balance, payment)&lt;/code&gt; keeps the last payment from going negative. The leftover rolls on in the same month, as it would in real life.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the numbers say
&lt;/h2&gt;

&lt;p&gt;With four sample debts (a $4,200 card at 24.9%, a $900 store card at 29.9%, an $11,500 car loan at 7.5% and a $3,000 personal loan at 13%) and $200 extra a month:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Debt-free&lt;/th&gt;
&lt;th&gt;Interest&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Snowball&lt;/td&gt;
&lt;td&gt;30 months&lt;/td&gt;
&lt;td&gt;$3,098&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avalanche&lt;/td&gt;
&lt;td&gt;29 months&lt;/td&gt;
&lt;td&gt;$2,860&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Minimums only&lt;/td&gt;
&lt;td&gt;44 months&lt;/td&gt;
&lt;td&gt;$5,552&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Avalanche saves $238 here. Paying the extra $200 at all saves far more than the choice of method. That is the usual pattern: the gap between the methods is often small, and the gap between paying extra and not paying extra is large. So pick the method you will stick with.&lt;/p&gt;

&lt;p&gt;For people who would rather track it on paper, I made a printable &lt;a href="https://payhip.com/b/4Ys7A" rel="noopener noreferrer"&gt;Monthly Budget and Debt Payoff Planner&lt;/a&gt;. It has a debt list, a month-by-month payoff tracker and milestones to tick off.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Interest is computed monthly at APR / 12, with no fees or promo rates. Real statements will differ slightly.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>What your car really costs per mile (the line most people leave out)</title>
      <dc:creator>Black</dc:creator>
      <pubDate>Mon, 26 Oct 2026 07:54:46 +0000</pubDate>
      <link>https://dev.to/blackspectre512/what-your-car-really-costs-per-mile-the-line-most-people-leave-out-3a1i</link>
      <guid>https://dev.to/blackspectre512/what-your-car-really-costs-per-mile-the-line-most-people-leave-out-3a1i</guid>
      <description>&lt;p&gt;Ask someone what their car costs and most people name a gas number. Gas is real money, but for a typical paid-off car it is often only a fifth to a third of the total. Here is the full calculation, with formulas you can put in a spreadsheet. If you'd rather click than type, there's a free &lt;a href="https://blacksgit.github.io/car-cost/" rel="noopener noreferrer"&gt;car cost calculator&lt;/a&gt; that runs in the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  The eight lines
&lt;/h2&gt;

&lt;p&gt;Per year:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gas          = miles / mpg * gas_price
value lost   = value_now * rate_by_age
insurance    = monthly_premium * 12
payment      = monthly_payment * 12
maintenance  = oil, brakes, filters, repairs
tires        = set_price * miles / 50000
registration = fees, inspection, emissions
parking      = parking, tolls, washes * 12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Divide the total by 12 for the monthly cost, and by miles for the cost per mile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Value lost is the line people skip
&lt;/h2&gt;

&lt;p&gt;A car loses value every year whether you drive it or not. You don't see a bill for it, but you pay it when you sell or trade the car in. Rough yearly rates by age:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Car age&lt;/th&gt;
&lt;th&gt;Value lost per year&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0 to 3 years&lt;/td&gt;
&lt;td&gt;about 15%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 to 7 years&lt;/td&gt;
&lt;td&gt;about 12%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8 to 12 years&lt;/td&gt;
&lt;td&gt;about 10%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13+ years&lt;/td&gt;
&lt;td&gt;about 7%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A $14,000 car at 12% loses about $1,680 a year, or $140 a month. That is more than the gas bill for many drivers.&lt;/p&gt;

&lt;h2&gt;
  
  
  A worked example
&lt;/h2&gt;

&lt;p&gt;12,000 miles a year, 28 MPG, $3.20 gas, $150/month insurance, no payment, worth $14,000, 4 to 7 years old:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Line&lt;/th&gt;
&lt;th&gt;Per month&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Insurance&lt;/td&gt;
&lt;td&gt;$150&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Value lost&lt;/td&gt;
&lt;td&gt;$140&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gas&lt;/td&gt;
&lt;td&gt;$114&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance and repairs&lt;/td&gt;
&lt;td&gt;$83&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parking, tolls, washes&lt;/td&gt;
&lt;td&gt;$20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tires&lt;/td&gt;
&lt;td&gt;$13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Registration&lt;/td&gt;
&lt;td&gt;$13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$533&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That comes to about 53 cents a mile. Gas is 21% of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two useful numbers that come out of this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Cost per trip.&lt;/strong&gt; A 20-mile round trip costs about $3.38 in gas and wear, or $10.66 if you count everything. The first number is what that trip costs you on the day. The second is what owning the car costs per mile, and it's the honest number to use when you decide whether you need a second car, or compare the car with transit or a rideshare.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gas as a share of the total.&lt;/strong&gt; If gas is a small share, driving a little less saves little, and the big savings are in insurance (shop it every year) and in how much car you own.&lt;/p&gt;

&lt;h2&gt;
  
  
  In a spreadsheet
&lt;/h2&gt;

&lt;p&gt;Put the eight inputs in column B, then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;B10 =B1/B2*B3                 gas
B11 =B6*CHOOSE(B7,0.15,0.12,0.10,0.07)   value lost (B7 = age band 1-4)
B12 =B4*12                    insurance
...
B18 =SUM(B10:B17)             year
B19 =B18/12                   month
B20 =B18/B1                   per mile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you'd rather keep it on paper, my &lt;a href="https://payhip.com/b/K9M8O" rel="noopener noreferrer"&gt;Car Maintenance Log and Expense Tracker&lt;/a&gt; (printable PDF) has a service schedule, a fuel and MPG log, and a yearly cost summary that ends in your cost per mile.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;These rates are rough averages for planning. Your car's real value loss depends on make, mileage and condition.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>beginners</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to calculate your freelance hourly rate backwards from take-home pay</title>
      <dc:creator>Black</dc:creator>
      <pubDate>Mon, 26 Oct 2026 07:49:11 +0000</pubDate>
      <link>https://dev.to/blackspectre512/how-to-calculate-your-freelance-hourly-rate-backwards-from-take-home-pay-bp6</link>
      <guid>https://dev.to/blackspectre512/how-to-calculate-your-freelance-hourly-rate-backwards-from-take-home-pay-bp6</guid>
      <description>&lt;p&gt;Most freelance rate advice says "take your old salary, divide by 2,080 hours, and double it." That rule is easy to remember, but it hides the three things that set your real rate: how many hours you can actually bill, self-employment tax, and the costs an employer used to pay for you.&lt;/p&gt;

&lt;p&gt;Here is how to work the rate out backwards from the money you want to take home. I also put the whole thing in a free &lt;a href="https://blacksgit.github.io/freelance-rate/" rel="noopener noreferrer"&gt;freelance rate calculator&lt;/a&gt; that runs in your browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Count billable hours, not working hours
&lt;/h2&gt;

&lt;p&gt;A full-time freelancer works about 40 hours a week. Only part of that time is billable. Email, quotes, invoicing, sales calls and learning take up the rest. For solo freelancers, 60 to 70% billable is a realistic range.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;billable hours = hours per week x billable share x (52 - weeks off)
               = 40 x 0.65 x (52 - 6)
               = 1,196 hours
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six weeks off covers holidays, a vacation, sick days and the slow week between projects. That's 1,196 hours, not 2,080. This one change explains most of the gap between a salary and a freelance rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Add the costs your employer used to pay
&lt;/h2&gt;

&lt;p&gt;Per year:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Health insurance, for example $550 a month = $6,600&lt;/li&gt;
&lt;li&gt;Software, gear, phone, accounting: $250 a month = $3,000&lt;/li&gt;
&lt;li&gt;Retirement: no employer match, so set aside your own (5% of $60,000 = $3,000)&lt;/li&gt;
&lt;li&gt;Platform or card fees, for example 3% of revenue&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Self-employment tax is the surprise
&lt;/h2&gt;

&lt;p&gt;An employee pays 7.65% for Social Security and Medicare, and the employer pays the other 7.65%. A freelancer pays both halves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SE tax = profit x 0.9235 x 15.3%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For 2026, the 12.4% Social Security part stops at $184,500 of earnings. The 2.9% Medicare part has no cap. Half of SE tax is deducted before income tax, and most freelancers also get the 20% qualified business income deduction.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Solve for revenue
&lt;/h2&gt;

&lt;p&gt;You can't just add everything up, because tax depends on revenue and revenue depends on tax. The easy fix is a binary search: guess a revenue, work out what you keep, and move the guess up or down. Sixty passes is far more than enough.&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;lo&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;hi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nx"&gt;e6&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="mi"&gt;60&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;const&lt;/span&gt; &lt;span class="nx"&gt;mid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;hi&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="nf"&gt;keep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mid&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;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;billableHours&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;keep(revenue)&lt;/code&gt; subtracts fees and costs, then SE tax, federal tax (2026 brackets and standard deduction) and state tax. A spreadsheet can do the same thing with Goal Seek.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;For $60,000 take-home with the numbers above (single filer, 4% state tax):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Revenue needed&lt;/td&gt;
&lt;td&gt;about $96,400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hourly rate&lt;/td&gt;
&lt;td&gt;about $81&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Day rate (8 h)&lt;/td&gt;
&lt;td&gt;about $645&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Take-home per hour actually worked&lt;/td&gt;
&lt;td&gt;about $29&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So $29 for each hour worked becomes $81 per billed hour. Tax, costs and unbilled time make up the difference, which is why a fair freelance rate looks high next to a salary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then track it
&lt;/h2&gt;

&lt;p&gt;A rate only works if clients pay on time. My &lt;a href="https://payhip.com/b/5CGT4" rel="noopener noreferrer"&gt;Invoice Tracker&lt;/a&gt; spreadsheet flags overdue invoices and shows how many days each client takes to pay. For the tax side, the &lt;a href="https://payhip.com/b/9ZU7G" rel="noopener noreferrer"&gt;Self-Employed Tax Tracker&lt;/a&gt; handles quarterly estimates. Both work in Excel and Google Sheets.&lt;/p&gt;

&lt;p&gt;If these free tools help, I post new ones on &lt;a href="https://patreon.com/123black" rel="noopener noreferrer"&gt;patreon.com/123black&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is an estimate for a single US filer, not tax advice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>freelance</category>
      <category>career</category>
      <category>javascript</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How to check the 50/30/20 rule in Excel or Google Sheets (with formulas)</title>
      <dc:creator>Black</dc:creator>
      <pubDate>Mon, 26 Oct 2026 07:40:50 +0000</pubDate>
      <link>https://dev.to/blackspectre512/how-to-check-the-503020-rule-in-excel-or-google-sheets-with-formulas-3lmk</link>
      <guid>https://dev.to/blackspectre512/how-to-check-the-503020-rule-in-excel-or-google-sheets-with-formulas-3lmk</guid>
      <description>&lt;p&gt;The 50/30/20 rule says 50% of take-home pay goes to needs, 30% to wants and 20% to savings. Checking it by hand each month is tedious. In a spreadsheet it takes four formulas, and it works the same in Excel and Google Sheets.&lt;/p&gt;

&lt;p&gt;If you just want the answer for your own numbers, I made a free &lt;a href="https://blacksgit.github.io/budget-503020/" rel="noopener noreferrer"&gt;50/30/20 budget check&lt;/a&gt; that runs in the browser. Nothing is uploaded. Below is how to build the same check yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Give every category a group
&lt;/h2&gt;

&lt;p&gt;Make a small table on a sheet called &lt;code&gt;Setup&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Group&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rent&lt;/td&gt;
&lt;td&gt;Needs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Groceries&lt;/td&gt;
&lt;td&gt;Needs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Car and gas&lt;/td&gt;
&lt;td&gt;Needs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eating out&lt;/td&gt;
&lt;td&gt;Wants&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subscriptions&lt;/td&gt;
&lt;td&gt;Wants&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Emergency fund&lt;/td&gt;
&lt;td&gt;Savings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extra debt payment&lt;/td&gt;
&lt;td&gt;Savings&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Name the range &lt;code&gt;Cats&lt;/code&gt; (A2:A40) and &lt;code&gt;Groups&lt;/code&gt; (B2:B40). Keep the group list to exactly &lt;code&gt;Needs&lt;/code&gt;, &lt;code&gt;Wants&lt;/code&gt;, &lt;code&gt;Savings&lt;/code&gt;, and add a data validation dropdown so nobody types &lt;code&gt;need&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Pull the group onto each transaction
&lt;/h2&gt;

&lt;p&gt;On the &lt;code&gt;Transactions&lt;/code&gt; sheet you have Date (A), Description (B), Amount (C) and Category (D). Add a Group column (E):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=IFERROR(INDEX(Groups, MATCH(D2, Cats, 0)), "")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;XLOOKUP&lt;/code&gt; is shorter, but &lt;code&gt;INDEX/MATCH&lt;/code&gt; works in every Excel version and in Google Sheets.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Total each group for one month
&lt;/h2&gt;

&lt;p&gt;Put the month you want in &lt;code&gt;H1&lt;/code&gt; as a real date (the 1st of the month). Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=SUMIFS(C:C, E:E, "Needs", A:A, "&amp;gt;="&amp;amp;H1, A:A, "&amp;lt;"&amp;amp;EDATE(H1,1))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy it for &lt;code&gt;Wants&lt;/code&gt; and &lt;code&gt;Savings&lt;/code&gt;. Using &lt;code&gt;EDATE&lt;/code&gt; for the upper bound avoids the end-of-month mistakes that &lt;code&gt;DATE(year, month, 31)&lt;/code&gt; causes in short months.&lt;/p&gt;

&lt;p&gt;If your bank export shows spending as negative numbers, wrap the amount in &lt;code&gt;ABS&lt;/code&gt;. &lt;code&gt;SUMIFS&lt;/code&gt; cannot do that, so use &lt;code&gt;SUMPRODUCT&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=SUMPRODUCT(ABS(C2:C2000) * (E2:E2000="Needs") * (A2:A2000&amp;gt;=H1) * (A2:A2000&amp;lt;EDATE(H1,1)))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Count unspent money as savings
&lt;/h2&gt;

&lt;p&gt;This step is the one most templates skip. If you earned $4,200 and your tracked spending adds up to $3,900, the other $300 is still in your account. Leave it out and your savings rate looks worse than it is. Count it, and remember to move it on payday:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unspent:        =MAX(0, Income - Needs - Wants - Savings)
Savings total:  =Savings + Unspent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Compare to the rule
&lt;/h2&gt;

&lt;p&gt;With income in &lt;code&gt;H2&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Needs %:   =Needs / H2
Gap:       =Needs - H2*0.5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do the same for wants (0.3) and savings (0.2). Add conditional formatting so the gap turns red when needs or wants go over target, or when savings fall under it. Watch the direction: for savings, under target is the bad case.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Say where the money comes from
&lt;/h2&gt;

&lt;p&gt;Showing a red number is not enough. The useful output is one sentence: "Savings are $455 short; wants are $435 over, mostly eating out." Add a third column &lt;code&gt;CatTotals&lt;/code&gt; (C2:C40) to the Setup table with each category's total for the month (the same &lt;code&gt;SUMIFS&lt;/code&gt;, matched on category instead of group). The biggest want is then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=INDEX(Cats, MATCH(MAXIFS(CatTotals, Groups, "Wants"), CatTotals, 0))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;MAXIFS&lt;/code&gt; needs Excel 2019 or newer, or Google Sheets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Notes on the rule itself
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use take-home pay (after tax), not salary.&lt;/li&gt;
&lt;li&gt;Minimum debt payments are needs. Anything above the minimum is savings.&lt;/li&gt;
&lt;li&gt;In high-rent cities 60/20/20 is common and still healthy, as long as the savings line holds. The rule is a starting point, not a test you pass or fail.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you would rather not build it, my &lt;a href="https://payhip.com/b/CrujX" rel="noopener noreferrer"&gt;Budget Dashboard 2027&lt;/a&gt; (Excel and Google Sheets) sorts a pasted bank export into needs, wants, savings and debt and runs this check for any month you pick. It also shows budget vs actual and charts. The &lt;a href="https://blacksgit.github.io/budget-503020/" rel="noopener noreferrer"&gt;browser version&lt;/a&gt; is free.&lt;/p&gt;

</description>
      <category>excel</category>
      <category>spreadsheets</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Reading 3D printer G-code in the browser: slicer comment formats and a price calculator with no upload</title>
      <dc:creator>Black</dc:creator>
      <pubDate>Mon, 26 Oct 2026 07:34:33 +0000</pubDate>
      <link>https://dev.to/blackspectre512/reading-3d-printer-g-code-in-the-browser-slicer-comment-formats-and-a-price-calculator-with-no-31j1</link>
      <guid>https://dev.to/blackspectre512/reading-3d-printer-g-code-in-the-browser-slicer-comment-formats-and-a-price-calculator-with-no-31j1</guid>
      <description>&lt;p&gt;I wanted a 3D print price calculator where you drop the sliced file and get a price, without uploading a 40 MB G-code file to anyone's server. It turned out to need about 30 lines of JavaScript. Here is how it works, and the slicer comment formats I had to handle.&lt;/p&gt;

&lt;p&gt;Try it first if you like: &lt;a href="https://blacksgit.github.io/print-price/" rel="noopener noreferrer"&gt;3D print price calculator&lt;/a&gt; (static page, nothing leaves the browser).&lt;/p&gt;

&lt;h2&gt;
  
  
  Only read the ends of the file
&lt;/h2&gt;

&lt;p&gt;Slicers write their summary as comments, either in the header or in the footer. The middle is millions of moves you do not care about. &lt;code&gt;Blob.slice()&lt;/code&gt; lets you read just the ends without loading the whole file into a string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&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;head&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;file&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;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;text&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;tail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;file&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="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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;80000&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;grams&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secs&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;head&lt;/span&gt; &lt;span class="o"&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="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;tail&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 is 140 KB read no matter how big the file is, so it stays fast on a phone. &lt;code&gt;file.text()&lt;/code&gt; would pull the whole file into one string first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The comment formats
&lt;/h2&gt;

&lt;p&gt;These are the lines I match, in order of preference:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Slicer&lt;/th&gt;
&lt;th&gt;Filament&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PrusaSlicer, OrcaSlicer&lt;/td&gt;
&lt;td&gt;&lt;code&gt;; filament used [g] = 12.34&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;; estimated printing time (normal mode) = 1h 2m 3s&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bambu Studio&lt;/td&gt;
&lt;td&gt;&lt;code&gt;; total filament weight [g] : 12.34&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;; total estimated time: 1h 2m 3s&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cura&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;;Filament used: 4.12m&lt;/code&gt; (length only)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;;TIME:3723&lt;/code&gt; (seconds)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Multi-material files list one value per extruder, comma separated (&lt;code&gt;= 5.10, 2.30&lt;/code&gt;), so sum them:&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;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;g&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="nx"&gt;secs&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="nx"&gt;mm&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="nx"&gt;x&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;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/;&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;total &lt;/span&gt;&lt;span class="se"&gt;)?&lt;/span&gt;&lt;span class="sr"&gt;filament &lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;used|weight&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt; &lt;/span&gt;&lt;span class="se"&gt;\[&lt;/span&gt;&lt;span class="sr"&gt;g&lt;/span&gt;&lt;span class="se"&gt;\]\s&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="se"&gt;([\d&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;)&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;x&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;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/;&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*filament used &lt;/span&gt;&lt;span class="se"&gt;\[&lt;/span&gt;&lt;span class="sr"&gt;mm&lt;/span&gt;&lt;span class="se"&gt;\]\s&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="se"&gt;([\d&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;)&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nx"&gt;mm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;x&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;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/;&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;total estimated time|estimated printing time&lt;/span&gt;&lt;span class="se"&gt;[^&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="se"&gt;)\s&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="se"&gt;([&lt;/span&gt;&lt;span class="sr"&gt;0-9dhms &lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nx"&gt;secs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secs&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/;&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*TIME:&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;(\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="nx"&gt;secs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;x&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;mm&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/;&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*Filament used:&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;([\d&lt;/span&gt;&lt;span class="sr"&gt;.&lt;/span&gt;&lt;span class="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;*m/&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="nx"&gt;mm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;x&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="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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;mm&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="nx"&gt;g&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="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.0875&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mm&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.24&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 1.75 mm PLA&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;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secs&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;hms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&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;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;([&lt;/span&gt;&lt;span class="sr"&gt;dhms&lt;/span&gt;&lt;span class="se"&gt;])&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;x&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="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;s&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;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]];&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;t&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 gotchas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;[^=\n]*&lt;/code&gt; in the time pattern matters. Without the &lt;code&gt;\n&lt;/code&gt;, PrusaSlicer's "(normal mode)" and "(silent mode)" lines can run together and the match grabs the wrong one.&lt;/li&gt;
&lt;li&gt;Cura only gives length. Grams = cross-section area x length x density. For 1.75 mm filament the radius is 0.0875 cm, length in cm, PLA about 1.24 g/cm³. PETG is 1.27, ABS 1.04, so the estimate is a little high for ABS.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  From grams and hours to a price
&lt;/h2&gt;

&lt;p&gt;Once you have grams and hours, the cost is mostly arithmetic people skip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;filament  = grams / 1000 * spool price
power     = watts * hours / 1000 * price per kWh
wear      = printer cost / expected life hours * hours
buffer    = (filament + power + wear) * failure %
your time = hands-on minutes / 60 * hourly rate
price     = cost * (1 + markup) / (1 - selling fee %)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last line is the one most calculators get wrong. If Etsy takes 9.5% of the sale, adding 9.5% to your price does not cover it; you divide by 0.905 instead. And hands-on time (removing supports, packing) is usually worth more than the filament.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same idea, other tools
&lt;/h2&gt;

&lt;p&gt;I built a few more single-file, no-backend calculators the same way this week, all phone-first with the numbers saved in &lt;code&gt;localStorage&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://blacksgit.github.io/resale-price/" rel="noopener noreferrer"&gt;Resale price calculator&lt;/a&gt;: list price per platform (eBay, Poshmark with its flat fee under $15, Mercari, Depop, Facebook, Whatnot, Etsy) for a target profit.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blacksgit.github.io/gig-hourly/" rel="noopener noreferrer"&gt;Gig driver real hourly&lt;/a&gt;: what a shift paid after gas, car wear and tax.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blacksgit.github.io/tax-set-aside/" rel="noopener noreferrer"&gt;Tax set-aside&lt;/a&gt;: how much of a freelance payment to save, with 2026 brackets and the next quarterly due date.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blacksgit.github.io/gift-budget/" rel="noopener noreferrer"&gt;Christmas gift budget splitter&lt;/a&gt;: a fair amount per person from one total.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you sell prints and want the whole job log, spool tracking and printer payback in a spreadsheet, that is the &lt;a href="https://payhip.com/b/Dy4Pq" rel="noopener noreferrer"&gt;3D Print Shop Tracker&lt;/a&gt;. The parser above is free to copy.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>3dprinting</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Christmas gift budget in a spreadsheet: per-person totals, a late-package alert, and the formulas</title>
      <dc:creator>Black</dc:creator>
      <pubDate>Mon, 26 Oct 2026 07:24:56 +0000</pubDate>
      <link>https://dev.to/blackspectre512/christmas-gift-budget-in-a-spreadsheet-per-person-totals-a-late-package-alert-and-the-formulas-3jn7</link>
      <guid>https://dev.to/blackspectre512/christmas-gift-budget-in-a-spreadsheet-per-person-totals-a-late-package-alert-and-the-formulas-3jn7</guid>
      <description>&lt;p&gt;Every December I see the same thing happen: the gift list grows, a few "small" purchases pile up, and nobody knows the real total until the card statement shows up. A spreadsheet fixes that, but only if it works without you thinking about it. Here is the setup I use, with formulas that run in both Excel and Google Sheets.&lt;/p&gt;

&lt;p&gt;Not sure what each person should get yet? This free &lt;a href="https://blacksgit.github.io/gift-budget/" rel="noopener noreferrer"&gt;Christmas gift budget splitter&lt;/a&gt; turns a total budget into a fair amount per person and a weekly savings target. Then the sheet below keeps you on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three tabs are enough
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;People&lt;/strong&gt;: one row per person with a budget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gifts&lt;/strong&gt;: one row per gift: who it is for, a rough price, the price you paid, the normal price, a status.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashboard&lt;/strong&gt;: totals, so you never add anything up by hand.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trick is that the People tab never gets typed into after the budget. Everything else is pulled from Gifts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gifts tab columns
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;For (drop-down of names)&lt;/td&gt;
&lt;td&gt;Mom&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;Gift&lt;/td&gt;
&lt;td&gt;Cozy robe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;Rough price&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;Price paid (empty until bought)&lt;/td&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;G&lt;/td&gt;
&lt;td&gt;Normal price (only if it was a deal)&lt;/td&gt;
&lt;td&gt;55&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H&lt;/td&gt;
&lt;td&gt;Saved&lt;/td&gt;
&lt;td&gt;&lt;code&gt;=IF(OR(F6="",G6=""),"",G6-F6)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;Status&lt;/td&gt;
&lt;td&gt;Idea / Ordered / Arrived / Wrapped / Given&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;K&lt;/td&gt;
&lt;td&gt;Arrives by&lt;/td&gt;
&lt;td&gt;Dec 2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Make column B a drop-down that reads from the People names (Data &amp;gt; Data validation &amp;gt; list from a range). Typos in names are the main reason these sheets give wrong totals, and a drop-down removes them.&lt;/p&gt;

&lt;h2&gt;
  
  
  People tab: spent, planned, left
&lt;/h2&gt;

&lt;p&gt;With the name in B6 and the budget in D6:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Spent         =SUMIFS(Gifts!$F$6:$F$405, Gifts!$B$6:$B$405, $B6)
Still planned =SUMIFS(Gifts!$E$6:$E$405, Gifts!$B$6:$B$405, $B6, Gifts!$F$6:$F$405, "")
Left          =D6 - Spent - Still planned
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Still planned" is the useful one. It counts the rough price only for gifts you have not bought yet, so the number left is honest &lt;em&gt;before&lt;/em&gt; you shop, not after. When you type the paid price, the gift moves from planned to spent by itself.&lt;/p&gt;

&lt;p&gt;A one-cell status per person keeps the list readable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=IF(E6=0,"Needs an idea",
 IF(H6&amp;lt;0,"Over budget",
 IF(J6=E6,"All done",
 IF(K6&amp;gt;0,K6&amp;amp;" to buy","Wrap it"))))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where E is the gift count (&lt;code&gt;COUNTIFS&lt;/code&gt; on the name), J is gifts with status Wrapped or Given, and K is gifts still at Idea. Add conditional formatting on the text ("Over budget" red, "All done" green) and you can read the whole list in two seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The late-package alert
&lt;/h2&gt;

&lt;p&gt;Online orders are where gifts go missing. One conditional formatting rule on the Gifts rows turns an order red once its arrival date has passed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=AND($I6="Ordered", $K6&amp;lt;&amp;gt;"", $K6&amp;lt;TODAY())
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply it to the whole row range (&lt;code&gt;B6:L405&lt;/code&gt;). If a row is red, chase the tracking number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dashboard
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Days to go     =MAX(0, DATE(2026,12,25) - TODAY())
Spent          =SUM(Gifts!F6:F405) + SUM(Spending!E6:E305)
Still planned  =SUMIFS(Gifts!E6:E405, Gifts!F6:F405, "")
Left           =Budget - Spent - Still planned
Saved on deals =SUM(Gifts!H6:H405)
Gifts done     =(COUNTIF(Gifts!I6:I405,"Wrapped")+COUNTIF(Gifts!I6:I405,"Given"))/COUNTA(Gifts!I6:I405)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep a separate Spending tab for food, decorations, travel and wrapping paper, with a budget per category. Those are the costs that blow up a holiday budget, because nobody puts them on the gift list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two habits that make it work
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Type the price the moment you pay, from your phone if needed (the Google Sheets app handles all of the above).&lt;/li&gt;
&lt;li&gt;Look at "Left" before every purchase, not after.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Want it ready-made?
&lt;/h2&gt;

&lt;p&gt;If you would rather print it, the paper version is here: &lt;a href="https://payhip.com/b/KUdnC" rel="noopener noreferrer"&gt;Holiday Gift List &amp;amp; Budget Planner (PDF)&lt;/a&gt;. More templates and tools: &lt;a href="https://patreon.com/123black" rel="noopener noreferrer"&gt;patreon.com/123black&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>excel</category>
      <category>googlesheets</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Generating a mirror-twin line puzzle: pair-aware pruning and a separator that was secretly a cell</title>
      <dc:creator>Black</dc:creator>
      <pubDate>Sun, 25 Oct 2026 21:51:06 +0000</pubDate>
      <link>https://dev.to/blackspectre512/generating-a-mirror-twin-line-puzzle-pair-aware-pruning-and-a-separator-that-was-secretly-a-cell-4edo</link>
      <guid>https://dev.to/blackspectre512/generating-a-mirror-twin-line-puzzle-pair-aware-pruning-and-a-separator-that-was-secretly-a-cell-4edo</guid>
      <description>&lt;p&gt;I wanted a daily path puzzle with one twist that changes how you think, not just a bigger grid. The rule I landed on: &lt;strong&gt;you draw one line, and the game draws its twin rotated 180 degrees. Your line and the twin may not overlap, and together they must fill every square.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You can play it free here: &lt;a href="https://blackspectre512.itch.io/twinline" rel="noopener noreferrer"&gt;Twinline on itch.io&lt;/a&gt;. This post covers how the puzzles are generated and checked, plus two bugs that cost me most of the build time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule as data
&lt;/h2&gt;

&lt;p&gt;On an n x n board with n even, cell &lt;code&gt;c&lt;/code&gt; at &lt;code&gt;(row, col)&lt;/code&gt; has the twin &lt;code&gt;(n-1-row, n-1-col)&lt;/code&gt;. No cell is its own twin when n is even, so the board splits cleanly into &lt;code&gt;n*n/2&lt;/code&gt; twin pairs. A solution is a path of exactly &lt;code&gt;n*n/2&lt;/code&gt; cells that uses one cell from every pair.&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;twin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the player takes a cell, its twin is blocked too. One &lt;code&gt;used&lt;/code&gt; array covers both:&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;used&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;used&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;twin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1: a random solution path
&lt;/h2&gt;

&lt;p&gt;I grow a path with depth-first search. At each step it tries the neighbours with the fewest free exits first (Warnsdorff's rule), plus a little random noise so boards don't all look the same:&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;opts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nb&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;c&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;x&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;used&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&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;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;rnd&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&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="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;b&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plain DFS on 8x8 sometimes wandered for seconds. The pruning that fixed it is a connectivity check: from the head of the line, flood fill through the free cells and count how many &lt;strong&gt;twin pairs&lt;/strong&gt; are still reachable. If that is fewer than the pairs still needed, this branch can never finish.&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;connected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;used&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;block&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;seen&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;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;used&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="nx"&gt;st&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;c&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;pairs&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;seen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;c&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="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;st&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;const&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;nb&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;u&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;used&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;block&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="k"&gt;continue&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;seen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt; &lt;span class="nx"&gt;pairs&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// count a pair once, whichever half we reach first&lt;/span&gt;
      &lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="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;st&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;v&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;pairs&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;left&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 also reject solutions that stay almost entirely in one half of the board. They are technically valid but play like half a puzzle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: clues until the answer is unique
&lt;/h2&gt;

&lt;p&gt;Numbers are placed along the solution path: the first and last cell, then roughly every fourth cell. Then a solver counts solutions up to 2. If it finds a second solution, I look at where that solution first disagrees with mine and put a number there. That kills the other solution with the smallest possible change. Repeat until exactly one solution is left.&lt;/p&gt;

&lt;p&gt;Then every clue gets removed in turn, in random order, and kept out if the puzzle stays unique. Easy and Medium get a few clues back afterwards so they feel friendlier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 1: the solver ignored what the numbers imply
&lt;/h2&gt;

&lt;p&gt;The first solver was correct but slow. On some 8x8 seeds it hit the node cap on every check, so the generator added numbers forever, because "gave up" counted as "not unique".&lt;/p&gt;

&lt;p&gt;The missing deduction: &lt;strong&gt;a numbered cell is always on your line, so its twin is never on it.&lt;/strong&gt; Without that rule, the solver spent ages exploring paths through cells that could never be used. Blocking every clue's twin before the search starts made checks about 40 times faster and the hang went away:&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;block&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;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;clues&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;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;c&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;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is now also a hint in the game itself: the twin of each number is shown as a faint ghost number, so players learn this deduction by looking at it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 2: my separator was a valid cell
&lt;/h2&gt;

&lt;p&gt;To keep phones from running the solver at all, I generate 300 puzzles per level offline and ship them as one compact string each: one character per cell, &lt;code&gt;String.fromCharCode(48 + cell)&lt;/code&gt;, solution and clue positions joined by &lt;code&gt;|&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That worked on 6x6 and 8x8. On 10x10 the generator crashed while checking the bank. &lt;code&gt;48 + 76&lt;/code&gt; is 124, which is &lt;code&gt;|&lt;/code&gt;. Any puzzle whose path used cell 76 was split in the wrong place. The fix was to use a space (char 32 sits below the encoding range) and to re-split the existing bank by length, since the solution length &lt;code&gt;n*n/2&lt;/code&gt; is known.&lt;/p&gt;

&lt;p&gt;The lesson I took from it: when you pack numbers into characters, choose the separator from outside the range you encode, not from what looks unused in a small test.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;900 puzzles (Easy 6x6, Medium 8x8, Hard 10x10), all checked offline for exactly one solution&lt;/li&gt;
&lt;li&gt;The page loads one 45 KB data file; no solver runs on the phone&lt;/li&gt;
&lt;li&gt;One HTML file plus that data file, canvas drawing, pointer events, works with touch and mouse&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The daily puzzle is the same for everyone, so you can compare times. If you try it, I'd like to hear whether the Hard boards feel fair.&lt;/p&gt;

&lt;p&gt;If you enjoy this kind of write-up, I post more builds on &lt;a href="https://patreon.com/123black" rel="noopener noreferrer"&gt;Patreon&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>gamedev</category>
      <category>algorithms</category>
      <category>puzzle</category>
    </item>
  </channel>
</rss>
