<?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: Rai Ansar </title>
    <description>The latest articles on DEV Community by Rai Ansar  (@raiansar).</description>
    <link>https://dev.to/raiansar</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%2F167688%2Fee34c5c3-08db-429e-ac83-c3df4dc23ce0.jpg</url>
      <title>DEV Community: Rai Ansar </title>
      <link>https://dev.to/raiansar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/raiansar"/>
    <language>en</language>
    <item>
      <title>Rounding race splits so they still add up to the goal time</title>
      <dc:creator>Rai Ansar </dc:creator>
      <pubDate>Fri, 06 Nov 2026 06:44:39 +0000</pubDate>
      <link>https://dev.to/raiansar/rounding-race-splits-so-they-still-add-up-to-the-goal-time-1bl1</link>
      <guid>https://dev.to/raiansar/rounding-race-splits-so-they-still-add-up-to-the-goal-time-1bl1</guid>
      <description>&lt;p&gt;A split table for a race looks like plain division. Take the goal time, divide it by the distance, and print one row per checkpoint. Runners read those rows off a paper pace band or a watch in the middle of a race, so every row has to be a whole number of seconds. Rounding is where the plain version goes wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rounding each segment
&lt;/h2&gt;

&lt;p&gt;Take a 4:00:00 marathon. The distance is 42.195 km, so with a checkpoint every kilometre you get 42 full kilometres and a last segment of 0.195 km. The obvious code works out each segment's time and rounds it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 14,400 s&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;42.195&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// km&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;points&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nc"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&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;_&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="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="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;distance&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;previous&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;segments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;points&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;point&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seconds&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;total&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;point&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;point&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;seconds&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="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="c1"&gt;// 14,389&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A full kilometre at that pace takes 341.27 seconds, which rounds down to 5:41. Lose 0.27 seconds 42 times and the table for a 4:00:00 race ends at 3:59:49, 11 seconds short.&lt;/p&gt;

&lt;p&gt;The error can go either way. At a 3:30:00 goal the same code rounds every kilometre up and the table ends at 3:30:16. Fewer checkpoints means less drift, yet the 4-hour table still ends at 3:59:54 with mile checkpoints and at 3:59:57 with one every 5 km.&lt;/p&gt;

&lt;p&gt;The elapsed column is worse than the total. It is the number a runner compares with the race clock, and with kilometre splits it drifts up to about 11.5 seconds away from the exact plan before the last segment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Round the elapsed time, then subtract
&lt;/h2&gt;

&lt;p&gt;The fix is to round the running total instead of each piece. Work out the exact elapsed time at each checkpoint, round that, and make each segment the difference between two rounded elapsed times.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;allocate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;weights&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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="kr"&gt;number&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;sum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;weights&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;cumulative&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;previous&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;weights&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;weight&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="nx"&gt;cumulative&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;weight&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;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;weights&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;weights&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;cumulative&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// keep every segment at least 1 s long&lt;/span&gt;
    &lt;span class="nx"&gt;elapsed&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="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;elapsed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;previous&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;total&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;elapsed&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;segments&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 weights here are segment lengths in kilometres. Every elapsed time lands within half a second of the exact plan. The last one is set to the goal itself, so the table always finishes on the goal time.&lt;/p&gt;

&lt;p&gt;The segments now alternate. For the 4:00:00 marathon with kilometre splits, each kilometre is 5:41 or 5:42. With a checkpoint every 5 km, the segments are 0:28:26 or 0:28:27, the final 2.195 km takes 0:12:29, and the 9 rows add up to exactly 4:00:00.&lt;/p&gt;

&lt;p&gt;The clamp line covers a corner case. A goal of a few seconds over many checkpoints could round two elapsed times to the same second, which would print a 0-second segment. The clamp gives every segment at least one second, and the calculator rejects a goal shorter than the number of checkpoints before it gets here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the last partial segment
&lt;/h2&gt;

&lt;p&gt;A marathon is 26.2188 miles, and some calculators round it to 26.2. At 4:00:00 that's 9:09.6 per mile, which shows as 9:10. The true figure is 9:09.2, which shows as 9:09. When I checked the top 5 US and UK results for "marathon split calculator" on 24 September 2026, two of them left out the final 0.2188 miles altogether and one used 26.2 miles.&lt;/p&gt;

&lt;p&gt;On an even 4:00:00 plan, mile 26 comes at 3:58:00, so the final 0.2188 miles takes two minutes. The checkpoint list has to end on the real distance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MILE_KM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.609344&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// exact, by definition&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkpoints&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;distanceKm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stepKm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;points&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;stepKm&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;distanceKm&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;points&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="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;stepKm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;points&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;distanceKm&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;points&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 loop stops before the finish and then adds the exact distance, so the last segment is whatever is left over: 0.195 km, 2.195 km or 0.2188 miles, depending on the spacing. The small tolerance stops float noise from adding a second checkpoint on top of the finish.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pacing shapes reuse the same rounding
&lt;/h2&gt;

&lt;p&gt;Because &lt;code&gt;allocate&lt;/code&gt; takes weights rather than distances, pacing shapes need very little extra code. For a negative split, the pace multiplier runs in a straight line from 1.02 at the start to 0.98 at the finish, and each segment's weight is its length times the multiplier at its midpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SLOPE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.02&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;multiplier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fraction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;SLOPE&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;fraction&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;previous&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;weights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;points&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;point&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;midpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;previous&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;point&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;weight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;point&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;multiplier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;midpoint&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;point&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;weight&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 value of a straight line at the midpoint of a segment times its length is the exact integral over that segment, so the shape doesn't depend on where the checkpoints fall. Split a 4:00:00 marathon into halves and the negative plan gives 2:01:12 and 1:58:48, a 50.5% and 49.5% split. A positive split swaps the two. Every plan still finishes on 4:00:00, because it is the same allocation underneath.&lt;/p&gt;

&lt;p&gt;The printed pace band on the site takes its rows from the same plan. If you want to see the output, the marathon split calculator on &lt;a href="https://racepaceplanner.com/marathon-split-calculator" rel="noopener noreferrer"&gt;Race Pace Planner&lt;/a&gt; shows the even, negative and positive tables side by side for any goal time, with a Segment column and an Elapsed column built this way.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>algorithms</category>
    </item>
    <item>
      <title>Payroll math has dates in it: modelling Ireland's October PRSI change</title>
      <dc:creator>Rai Ansar </dc:creator>
      <pubDate>Wed, 28 Oct 2026 08:34:23 +0000</pubDate>
      <link>https://dev.to/raiansar/payroll-math-has-dates-in-it-modelling-irelands-october-prsi-change-560c</link>
      <guid>https://dev.to/raiansar/payroll-math-has-dates-in-it-modelling-irelands-october-prsi-change-560c</guid>
      <description>&lt;p&gt;Most take-home pay calculators I've read share one shortcut: they turn every deduction into an annual rate and multiply. For Irish pay in 2026 that shortcut gives the wrong answer, because one of the three deductions changes rate partway through the year.&lt;/p&gt;

&lt;p&gt;I hit this while building &lt;a href="https://irishtakehome.com/" rel="noopener noreferrer"&gt;Irishtakehome&lt;/a&gt;, a take-home calculator for Irish salaries. This post walks through the model with one worked example: a single employee on €45,000, paid weekly, with no pension. All rates below are the 2026 rules as of 15 October 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three charges, three bases
&lt;/h2&gt;

&lt;p&gt;Irish payroll takes three separate charges out of a salary, and each one is worked out on a different base.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Income tax (PAYE)&lt;/strong&gt; is banded, then reduced by credits. A single person pays 20% on the first €44,000 of taxable pay and 40% on the rest. The personal tax credit (€2,000) and the employee tax credit (€2,000) come off the tax itself, not off the pay.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;20% of €44,000      = €8,800
40% of €1,000       =   €400
less credits        = −€4,000
PAYE                = €5,200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;USC&lt;/strong&gt; (Universal Social Charge) has its own bands:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Slice of income&lt;/th&gt;
&lt;th&gt;Rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First €12,012&lt;/td&gt;
&lt;td&gt;0.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Next €16,688 (up to €28,700)&lt;/td&gt;
&lt;td&gt;2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Next €41,344 (up to €70,044)&lt;/td&gt;
&lt;td&gt;3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Balance over €70,044&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On €45,000 that's €60.06 + €333.76 + €489.00 = €882.82.&lt;/p&gt;

&lt;p&gt;USC also has a threshold that behaves differently from a band. Income of €13,000 or less pays no USC at all, but once you go over it, USC applies to all of your income from the first euro. If you write this as a marginal band, people just above €13,000 get the wrong figure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PRSI&lt;/strong&gt; (social insurance, Class A employee) is charged per pay period, on the whole of that period's pay. Weekly pay from €38 to €352 pays nothing. Above €352, the rate is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;4.2% of the whole week's pay for pay dates up to 30 September 2026&lt;/li&gt;
&lt;li&gt;4.35% of the whole week's pay for pay dates from 1 October 2026&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Between €352.01 and €424 a week a credit of up to €12 reduces the charge. I've left that band out of the code below to keep it short; our example pays €865.38 a week, well above it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why an annual rate is wrong here
&lt;/h2&gt;

&lt;p&gt;If you treat PRSI as one rate for the year you get one of these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flat 4.2%  on €45,000 = €1,890.00   (too low)
flat 4.35% on €45,000 = €1,957.50   (too high)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real figure depends on how many pay dates fall on each side of 1 October. PRSI is worked out on each pay period, so the rate that applies is the one in force on the pay date. Here is the model I ended up with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ROUND_HALF_UP&lt;/span&gt;

&lt;span class="n"&gt;CENT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;RATE_CHANGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&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;def&lt;/span&gt; &lt;span class="nf"&gt;weekly_prsi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weekly_pay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pay_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;weekly_pay&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;352&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# nil band (the credit band from €352.01 to €424 is left out here)
&lt;/span&gt;    &lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.042&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pay_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;RATE_CHANGE&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0435&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weekly_pay&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;quantize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CENT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ROUND_HALF_UP&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;salary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;45000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;weekly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;salary&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;52&lt;/span&gt;
&lt;span class="n"&gt;first_pay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&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="c1"&gt;# 52 Friday pay dates in 2026
&lt;/span&gt;&lt;span class="n"&gt;pays&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;first_pay&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weeks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;52&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;weekly_prsi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weekly&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pays&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pays&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;RATE_CHANGE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pays at 4.2%: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, at 4.35%: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;52&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PRSI for the year: €&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pays at 4.2%: 39, at 4.35%: 13
PRSI for the year: €1906.97
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thirty-nine Friday pays fall before 1 October and thirteen after. Each week's charge is rounded to the cent on its own, as it is on a payslip, so the year comes to €1,906.97, between the two flat figures. The last thirteen payslips of the year each take a little more PRSI than the first thirty-nine, and a model with one annual rate can't show that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Salary              €45,000.00
PAYE                −€5,200.00
USC                   −€882.82
PRSI                −€1,906.97
Take-home pay       €37,010.21   (82% kept)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The pension trap
&lt;/h2&gt;

&lt;p&gt;The other place a single rate lets you down is pension contributions. A contribution taken from pay by an employer's approved scheme lowers the pay PAYE is worked out on. It doesn't lower the pay USC and PRSI are charged on. So if your model subtracts the pension from gross pay once and then runs all three charges on the result, it overstates the saving.&lt;/p&gt;

&lt;p&gt;In code, that means keeping two bases instead of one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;paye_base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gross&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;pension_contribution&lt;/span&gt;
&lt;span class="n"&gt;usc_base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gross&lt;/span&gt;
&lt;span class="n"&gt;prsi_base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gross&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tax relief on the pension also has an age-related cap (15% to 40% of earnings, with earnings counted up to €115,000), so the PAYE base can't drop without limit either.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell anyone writing payroll code
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Store rates with an effective date, and look them up by pay date.&lt;/li&gt;
&lt;li&gt;Round per pay period, the way a payslip does, then add up.&lt;/li&gt;
&lt;li&gt;Keep a separate base for each charge. They stop matching as soon as a pension contribution comes in.&lt;/li&gt;
&lt;li&gt;Treat cliff thresholds like USC's €13,000 as their own rule, not as a band.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to check your own numbers against these rules, the worked example above is the calculator's default case, and every figure in this post comes from it. Has anyone here modelled a mid-year rate change in another country's payroll? I'd like to hear how you handled the pay-date rule.&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>python</category>
      <category>fintech</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Test your redirect map with bash and curl before you move a site</title>
      <dc:creator>Rai Ansar </dc:creator>
      <pubDate>Sun, 25 Oct 2026 06:43:23 +0000</pubDate>
      <link>https://dev.to/raiansar/test-your-redirect-map-with-bash-and-curl-before-you-move-a-site-8p0</link>
      <guid>https://dev.to/raiansar/test-your-redirect-map-with-bash-and-curl-before-you-move-a-site-8p0</guid>
      <description>&lt;p&gt;The DNS change is usually the calm part of a site move. The trouble shows up later: someone rebuilds the server config and half the redirect rules don't come back, or a rule left over from the old CMS stacks on top of a new one and every old URL now takes two hops to land.&lt;/p&gt;

&lt;p&gt;I've done more than 700 site migrations, and the habit that saved me most often is a dull one. The redirect map lives in the repo as a CSV, and a small script tests it against the live site, the same way a test suite tests code. If the map can be re-run, a broken rule shows up in seconds instead of in Search Console three weeks later.&lt;/p&gt;

&lt;h2&gt;
  
  
  The map is one row per old URL
&lt;/h2&gt;

&lt;p&gt;These are the columns I use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;old_url,new_url,status,match_type,clicks_12m,referring_domains,notes
https://old.example.com/about-us,https://www.example.com/about/,301,exact,380,12,Path renamed
https://old.example.com/blog/tag/widgets,https://www.example.com/blog/,301,merge,12,0,Thin tag page folded into the blog index
https://old.example.com/old-promo-2017,,410,removed,0,0,Expired offer. No equivalent page; do not send to home
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;status&lt;/code&gt; is 301 or 308 for pages that moved or were merged, and 410 or 404 for pages that are gone. &lt;code&gt;match_type&lt;/code&gt; says whether the row is an exact match, a pattern rule, a merge or a removal. Pattern rows get expanded to real URLs before testing, because you can't curl a regex.&lt;/p&gt;

&lt;p&gt;The two traffic columns come from Search Console and your backlink tool, and they decide which rows you check by hand first. A broken redirect on a page with 4,000 clicks a year hurts more than one on a dead tag page.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the test asserts
&lt;/h2&gt;

&lt;p&gt;For every 301 or 308 row, three things have to be true:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The first response is a permanent redirect. Google treats 301 and 308 as permanent and says they don't lose PageRank. A 302 or 307 tells it to keep showing the old URL in results.&lt;/li&gt;
&lt;li&gt;The chain ends on &lt;code&gt;new_url&lt;/code&gt;, and that page answers 200.&lt;/li&gt;
&lt;li&gt;It takes one hop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For 410 and 404 rows, the old URL has to answer with that code and must not redirect anywhere.&lt;/p&gt;

&lt;p&gt;The one-hop rule is stricter than Google needs. Googlebot follows up to 10 redirects, but Google's own advice is to send people straight to the final URL. Two hops usually means an old rule and a new rule are both firing, and that is cheap to fix while you're still on staging.&lt;/p&gt;

&lt;h2&gt;
  
  
  The script
&lt;/h2&gt;

&lt;p&gt;It needs only bash and curl.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# check-redirects.sh: test a redirect map against a live site.&lt;/span&gt;
&lt;span class="c"&gt;# Usage: ./check-redirects.sh redirect-map.csv&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;span class="nv"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;:?usage:&lt;span class="p"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt; redirect-map.csv&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;pass&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;fail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;, &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; old new status _rest&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$old&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"old_url"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$old&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;continue
  &lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="p"&gt;//&lt;/span&gt;&lt;span class="s1"&gt;$'&lt;/span&gt;&lt;span class="se"&gt;\r&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;new&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;new&lt;/span&gt;&lt;span class="p"&gt;//&lt;/span&gt;&lt;span class="s1"&gt;$'&lt;/span&gt;&lt;span class="se"&gt;\r&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nv"&gt;first&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;--max-time&lt;/span&gt; 20 &lt;span class="nt"&gt;--proto&lt;/span&gt; &lt;span class="s1"&gt;'=http,https'&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code}'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$old&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;problems&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in
    &lt;/span&gt;301|308&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; last hops final &amp;lt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="nt"&gt;--max-redirs&lt;/span&gt; 10 &lt;span class="nt"&gt;--max-time&lt;/span&gt; 30 &lt;span class="nt"&gt;--proto&lt;/span&gt; &lt;span class="s1"&gt;'=http,https'&lt;/span&gt; &lt;span class="nt"&gt;--proto-redir&lt;/span&gt; &lt;span class="s1"&gt;'=http,https'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code} %{num_redirects} %{url_effective}\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$old&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$first&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in &lt;/span&gt;301|308&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;problems&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$problems&lt;/span&gt;&lt;span class="s2"&gt; first-response=&lt;/span&gt;&lt;span class="nv"&gt;$first&lt;/span&gt;&lt;span class="s2"&gt;(want &lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="s2"&gt;);"&lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt; &lt;span class="k"&gt;esac&lt;/span&gt;
      &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$last&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"200"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;problems&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$problems&lt;/span&gt;&lt;span class="s2"&gt; final-status=&lt;/span&gt;&lt;span class="nv"&gt;$last&lt;/span&gt;&lt;span class="s2"&gt;;"&lt;/span&gt;
      &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$final&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$new&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;problems&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$problems&lt;/span&gt;&lt;span class="s2"&gt; lands-on=&lt;/span&gt;&lt;span class="nv"&gt;$final&lt;/span&gt;&lt;span class="s2"&gt;(want &lt;/span&gt;&lt;span class="nv"&gt;$new&lt;/span&gt;&lt;span class="s2"&gt;);"&lt;/span&gt;
      &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;hops&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;0&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-le&lt;/span&gt; 1 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;problems&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$problems&lt;/span&gt;&lt;span class="s2"&gt; hops=&lt;/span&gt;&lt;span class="nv"&gt;$hops&lt;/span&gt;&lt;span class="s2"&gt;;"&lt;/span&gt;
      &lt;span class="p"&gt;;;&lt;/span&gt;
    410|404&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$first&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;problems&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$problems&lt;/span&gt;&lt;span class="s2"&gt; status=&lt;/span&gt;&lt;span class="nv"&gt;$first&lt;/span&gt;&lt;span class="s2"&gt;(want &lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="s2"&gt;);"&lt;/span&gt;
      &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;problems&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"unknown status column: &lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="s2"&gt;;"&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="k"&gt;esac&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$problems&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nv"&gt;pass&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;pass+1&lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"PASS &lt;/span&gt;&lt;span class="nv"&gt;$old&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;else &lt;/span&gt;&lt;span class="nv"&gt;fail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;fail+1&lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL &lt;/span&gt;&lt;span class="nv"&gt;$old&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$problems&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi
done&lt;/span&gt; &amp;lt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"---"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"passed &lt;/span&gt;&lt;span class="nv"&gt;$pass&lt;/span&gt;&lt;span class="s2"&gt;, failed &lt;/span&gt;&lt;span class="nv"&gt;$fail&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$fail&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two curl calls per row. The first one doesn't follow redirects, so it sees exactly what the server answers. The second follows the chain with &lt;code&gt;-L&lt;/code&gt; and reports the final status, the number of hops and the URL it ended on. A 308 passes a 301 row and the other way round, since both are permanent. Because the script exits 1 when any row fails, you can drop it into a CI job as it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  A run against a test set
&lt;/h2&gt;

&lt;p&gt;To show each failure, I built three rows on httpbin.org's redirect endpoint. The first is a correct 301, the second answers 302 where the map wants a 301, and the third is a 301 that lands on another 301 before it gets to the destination.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;old_url,new_url,status,match_type
https://httpbin.org/redirect-to?url=https%3A%2F%2Fhttpbin.org%2Fget&amp;amp;status_code=301,https://httpbin.org/get,301,exact
https://httpbin.org/redirect-to?url=https%3A%2F%2Fhttpbin.org%2Fget&amp;amp;status_code=302,https://httpbin.org/get,301,exact
https://httpbin.org/redirect-to?url=https%3A%2F%2Fhttpbin.org%2Fredirect-to%3Furl%3Dhttps%253A%252F%252Fhttpbin.org%252Fget%26status_code%3D301&amp;amp;status_code=301,https://httpbin.org/get,301,exact
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The run on 25 October 2026:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PASS https://httpbin.org/redirect-to?url=https%3A%2F%2Fhttpbin.org%2Fget&amp;amp;status_code=301
FAIL https://httpbin.org/redirect-to?url=https%3A%2F%2Fhttpbin.org%2Fget&amp;amp;status_code=302  first-response=302(want 301);
FAIL https://httpbin.org/redirect-to?url=https%3A%2F%2Fhttpbin.org%2Fredirect-to%3Furl%3Dhttps%253A%252F%252Fhttpbin.org%252Fget%26status_code%3D301&amp;amp;status_code=301  hops=2;
---
passed 1, failed 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It took about 7 seconds and exited with code 1.&lt;/p&gt;

&lt;p&gt;On a real move, the failures map to fixes like these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;first-response=302&lt;/code&gt; means a temporary redirect somewhere in the config. Framework helpers often default to it. Express's &lt;code&gt;res.redirect()&lt;/code&gt; sends a 302 unless you pass a status.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hops=2&lt;/code&gt; usually means a legacy rule and a new rule are chained. If &lt;code&gt;/a&lt;/code&gt; already went to &lt;code&gt;/b&lt;/code&gt; and the new map sends &lt;code&gt;/b&lt;/code&gt; to &lt;code&gt;/c&lt;/code&gt;, map &lt;code&gt;/a&lt;/code&gt; straight to &lt;code&gt;/c&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lands-on=&lt;/code&gt; means the rule points somewhere other than the map says. Often it's the home page, which is the worst catch-all: Google can treat many unrelated URLs redirected to one page as soft 404s.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Keep it next to the config
&lt;/h2&gt;

&lt;p&gt;Commit the CSV and the script beside the server or CDN config that holds the rules. Run the script once against staging before launch, again right after the DNS change, and again whenever someone touches the redirect config. The next person who rebuilds that config will find a test that tells them what they broke.&lt;/p&gt;

&lt;p&gt;The rest of the migration process, all 40 checks from the first crawl to switching off the old server, with both files to download, is on &lt;a href="https://migrateweb.com/website-migration-checklist" rel="noopener noreferrer"&gt;MigrateWeb&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>devops</category>
      <category>seo</category>
      <category>bash</category>
    </item>
    <item>
      <title>Padding a JPEG signature up to a 10 KB minimum without changing a pixel</title>
      <dc:creator>Rai Ansar </dc:creator>
      <pubDate>Thu, 22 Oct 2026 06:41:31 +0000</pubDate>
      <link>https://dev.to/raiansar/padding-a-jpeg-signature-up-to-a-10-kb-minimum-without-changing-a-pixel-1mn7</link>
      <guid>https://dev.to/raiansar/padding-a-jpeg-signature-up-to-a-10-kb-minimum-without-changing-a-pixel-1mn7</guid>
      <description>&lt;p&gt;I build a browser tool that resizes photos and signatures for Indian exam and job application forms. Most of the work is the obvious kind: a form says "under 20 KB", so you shrink the file until it fits. The case that caught me out was the opposite one, where a signature comes out too small for the form to accept.&lt;/p&gt;

&lt;p&gt;Several notices set the signature as a window. IBPS RRB XV (Officers and Office Assistants), RCF Assistant Officer (Secretarial) and India Exim Bank SRD 2026 all ask for 140 × 60 px and 10 to 20 KB. TNPSC One Time Registration asks for 230 × 75 px, also 10 to 20 KB. So there is a floor as well as a ceiling, and the pixel size is fixed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quality 100 is not enough
&lt;/h2&gt;

&lt;p&gt;My first assumption was that a file under the minimum just needs a higher JPEG quality. That works until quality is already at 100.&lt;/p&gt;

&lt;p&gt;A signature is dark ink on white paper. At 140 × 60 there are only 8,400 pixels, and most of the 8 × 8 blocks the encoder works on are flat white. Those compress to almost nothing. On our sample signature, the IBPS size came to 6.2 KB at quality 100, and the TNPSC size to 8.4 KB. The encoder has nothing left to spend bytes on, so the minimum is out of reach from the quality setting alone.&lt;/p&gt;

&lt;p&gt;The pixel count is also fixed by the notice, so scaling up is not an option. Adding noise or a border would get the bytes up, but it changes what the person checking the form sees. I wanted the file bigger without touching a single pixel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Padding with comment segments
&lt;/h2&gt;

&lt;p&gt;A JPEG is a sequence of marker segments. &lt;code&gt;FF FE&lt;/code&gt; is a comment (COM) segment: two marker bytes, a two-byte length, then whatever you like. Decoders skip it. So the fix is to add comment segments until the file reaches the minimum, and leave the image data alone.&lt;/p&gt;

&lt;p&gt;The padding goes just before the &lt;code&gt;FF D9&lt;/code&gt; end-of-image marker, so the file still ends the way a JPEG should. A segment's length field is 16 bits, so a large gap needs more than one segment. This is close to what ships:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;padJpeg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetBytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Uint8Array&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;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;targetBytes&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;bytes&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;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at&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="o"&gt;!==&lt;/span&gt; &lt;span class="mh"&gt;0xff&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mh"&gt;0xd9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid JPEG output&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bytes&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;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;targetBytes&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remaining&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;segmentBytes&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;remaining&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="nx"&gt;_537&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;segmentBytes&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// A segment needs 4 bytes of header; overshoot slightly instead.&lt;/span&gt;
      &lt;span class="nx"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;4&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="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;segment&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;segmentBytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;segment&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="mh"&gt;0xff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;segment&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="mh"&gt;0xfe&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;declaredLength&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;segmentBytes&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="c1"&gt;// the length counts itself, not the marker&lt;/span&gt;
    &lt;span class="nx"&gt;segment&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="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;declaredLength&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;declaredLength&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xff&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;combined&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;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;segment&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;combined&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subarray&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nx"&gt;combined&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;combined&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;segment&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;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;combined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="nx"&gt;segmentBytes&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;output&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tests check that the output reaches the target size and still ends with &lt;code&gt;FF D9&lt;/code&gt;, and that a file without &lt;code&gt;FF D9&lt;/code&gt; at the end is refused rather than padded into something broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which KB?
&lt;/h2&gt;

&lt;p&gt;The other half of the problem is that "10 to 20 KB" does not say how many bytes a KB is. Some upload screens count 1,024 bytes and some count 1,000. A file of 20,300 bytes is under 20 KB on one reading and over it on the other.&lt;/p&gt;

&lt;p&gt;So the tool sets the maximum under the smaller reading and the minimum under the larger one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A maximum of `kb` KB, met under both the 1000- and the 1024-byte reading.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;kbCeilingBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kb&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// A minimum of `kb` KB, met under both readings.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;kbFloorBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kb&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For 10 to 20 KB that gives a target of 10,240 to 20,000 bytes. A file in that range passes whichever way the portal counts. When a window is too narrow to satisfy both readings (exactly 50 KB, say), it falls back to 1,024 bytes per KB and the code records that it did.&lt;/p&gt;

&lt;h2&gt;
  
  
  The search before the padding
&lt;/h2&gt;

&lt;p&gt;Padding is the last step. First the tool encodes the image at the exact pixel size and binary-searches the quality for the largest file that still fits under the maximum. It uses &lt;code&gt;canvas.toBlob&lt;/code&gt; with JPEG output, which takes quality on a 0.01 grid, so about seven halvings settle it. If the best fit is under the minimum, it pads. If padding would push it over the maximum, it throws an error rather than hand back a file the form will reject.&lt;/p&gt;

&lt;h2&gt;
  
  
  What came out
&lt;/h2&gt;

&lt;p&gt;I ran the presets in Chrome 153 on 25 September 2026 on a generated test image of a signature photographed on a phone, 3024 × 1512 px and 892 KB:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Preset&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;Quality&lt;/th&gt;
&lt;th&gt;Padding added&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;IBPS, 140 × 60 px&lt;/td&gt;
&lt;td&gt;10.0 KB&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;3.8 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TNPSC, 230 × 75 px&lt;/td&gt;
&lt;td&gt;10.0 KB&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;1.6 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSC, 472 × 157 px&lt;/td&gt;
&lt;td&gt;18.9 KB&lt;/td&gt;
&lt;td&gt;99&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The SSC preset needs no padding because it has about nine times the pixels of the IBPS one. SSC CHSL and CPO 2026 give the signature as roughly 6.0 × 2.0 cm with no DPI, so the preset converts that at an assumed 200 DPI. That assumption is the weakest part of the setup, so the SSC page on the site spells it out.&lt;/p&gt;

&lt;p&gt;Everything runs in the browser and nothing is uploaded, which matters when the file is someone's signature. If you want to try the floor case yourself, the tool is at &lt;a href="https://formassets.com/in/tools/resize-image-to-20kb" rel="noopener noreferrer"&gt;FormAssets&lt;/a&gt;. Pick the IBPS preset, give it a clean signature, and it will tell you how many bytes it had to add.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>typescript</category>
      <category>frontend</category>
    </item>
    <item>
      <title>A 200 response and a broken page: what screenshot monitoring taught me</title>
      <dc:creator>Rai Ansar </dc:creator>
      <pubDate>Sat, 17 Oct 2026 23:27:52 +0000</pubDate>
      <link>https://dev.to/raiansar/a-200-response-and-a-broken-page-what-screenshot-monitoring-taught-me-1jbi</link>
      <guid>https://dev.to/raiansar/a-200-response-and-a-broken-page-what-screenshot-monitoring-taught-me-1jbi</guid>
      <description>&lt;p&gt;An uptime check answers one question: did the server respond? Visitors ask a different one: can I use this page? After a few years of looking after client sites, the gap between those two answers was the thing I hit most often. A CSS deploy left a blank white screen. A WordPress plugin update broke checkout. An SSL certificate expired while the uptime dashboard still showed green.&lt;/p&gt;

&lt;p&gt;I am Rai Ansar, a developer and DevOps engineer in Islamabad. I have worked on websites and servers since May 2019, and I built Visual Sentinel to close that gap. Here is what the visual part taught me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The diff is the easy part
&lt;/h2&gt;

&lt;p&gt;The core of a visual monitor fits in a few lines. This is a minimal sketch with Playwright, not our production code:&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;playwright&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browser&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;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&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;page&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;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;viewport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1366&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;900&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;networkidle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;current.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;fullPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.ad-slot&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;time&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare &lt;code&gt;current.png&lt;/code&gt; with a stored baseline pixel by pixel and you have a change detector. Left like this it will also alert on pages that change on every load, which leads to the lessons below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 1: the baseline is the last confirmed good capture
&lt;/h2&gt;

&lt;p&gt;If you compare against the previous screenshot, a broken page becomes the new normal after one run. Compare each capture against the most recent confirmed baseline instead. Keeping the screenshot history then tells you when the break was introduced.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 2: the threshold decides whether anyone trusts the alert
&lt;/h2&gt;

&lt;p&gt;Ads, timestamps and rotating banners change on every load. Each monitor has a mismatch threshold, 5% by default, calculated over the whole rendered page. A tightly designed landing page or a checkout flow can go lower. A news homepage needs a higher number. For elements that always move, exclude them by CSS selector rather than raising the threshold for the whole page, because a high threshold hides real breakage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 3: a change is evidence, not a verdict
&lt;/h2&gt;

&lt;p&gt;On bdifferent.ie, a real customer site (recorded July 2026, shared with permission), 62.23% of the homepage changed while the uptime check still reported that the page loads. That is an easy case. Many diffs are a new hero image or a seasonal banner. The alert should carry a before and after capture so a person can decide in a few seconds. We treat a visual change as evidence to review and never as an automatic outage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 4: pixels and text answer different questions
&lt;/h2&gt;

&lt;p&gt;A screenshot tells you the page looks different. It does not tell you the price changed from one number to another. For that we run a separate content check: it extracts text from CSS selectors you define in the fetched source HTML and alerts when it changes. It does not run page JavaScript, so a price rendered client-side needs the visual check instead. Knowing which check covers which case saved me from promising more than either one does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 5: capture frequency is a cost decision
&lt;/h2&gt;

&lt;p&gt;Rendering a full page in Chromium costs more than a ping. On our Solo plan visual captures run every 30 minutes, and on Starter and above every 15 minutes, while uptime checks are much cheaper and run more often. A deploy regression is visible on the next capture, so a short interval matters most on sites that ship often.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson 6: group the alerts
&lt;/h2&gt;

&lt;p&gt;When three sites on the same server go down together, three separate 3 a.m. alerts help nobody. Visual Sentinel links sites to a server and sends one grouped incident when they fail together, with the likely shared cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would build first if I started again
&lt;/h2&gt;

&lt;p&gt;Start with the screenshot, the baseline and the threshold. Add the content check once someone asks about a specific price or sentence. Add server grouping when you run more than one site per machine.&lt;/p&gt;

&lt;p&gt;If you want to see the visual side on a live page, &lt;a href="https://visualsentinel.com/visual-monitoring" rel="noopener noreferrer"&gt;Visual Sentinel's visual monitoring&lt;/a&gt; shows a captured diff with the last good version beside the current one. Your first check is free when you add a site.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>monitoring</category>
      <category>webdev</category>
      <category>testing</category>
    </item>
    <item>
      <title>ChatGPT vs Claude vs Gemini (March 2026): The Definitive AI Comparison</title>
      <dc:creator>Rai Ansar </dc:creator>
      <pubDate>Tue, 10 Mar 2026 03:39:48 +0000</pubDate>
      <link>https://dev.to/raiansar/chatgpt-vs-claude-vs-gemini-march-2026-the-definitive-ai-comparison-46o6</link>
      <guid>https://dev.to/raiansar/chatgpt-vs-claude-vs-gemini-march-2026-the-definitive-ai-comparison-46o6</guid>
      <description>&lt;p&gt;Three platforms. Three radically different philosophies. And in March 2026, the gap between ChatGPT, Claude, and Gemini has never been more interesting — or more confusing for anyone trying to pick one.&lt;/p&gt;

&lt;p&gt;OpenAI just shipped GPT-5.4 with native computer use and a 1M-token context window. Anthropic's Claude Opus 4.6 sits at #1 on the LMSYS Chatbot Arena. Google's Gemini 3.1 Pro quietly posted a 94.3% on GPQA Diamond, the highest score any model has achieved on PhD-level science questions. Meanwhile, the real battleground has shifted to coding agents: Claude Code, GPT Codex, and Gemini CLI are fighting for every developer's terminal.&lt;/p&gt;

&lt;p&gt;I've spent the past two weeks stress-testing all three across coding projects, research tasks, creative writing, and daily workflows. Here's what actually matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Last Updated: March 2026&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Verdict: Best AI for Each Use Case
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;th&gt;Winner&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Coding &amp;amp; Development&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Claude (Opus 4.6 + Claude Code)&lt;/td&gt;
&lt;td&gt;#1 on SWE-bench (80.8%), Claude Code CLI dominates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Research &amp;amp; Analysis&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Gemini 3.1 Pro&lt;/td&gt;
&lt;td&gt;1M native context, 94.3% GPQA Diamond&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Creative Writing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Claude Opus 4.6&lt;/td&gt;
&lt;td&gt;Most natural prose, best voice consistency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Agentic Workflows&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ChatGPT (GPT-5.4)&lt;/td&gt;
&lt;td&gt;Native computer use, multi-step automation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best Value&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Gemini&lt;/td&gt;
&lt;td&gt;Free tier with Flash, $19.99/mo for Pro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Enterprise/Teams&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ChatGPT&lt;/td&gt;
&lt;td&gt;Most mature ecosystem, Codex for async work&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The Latest Models: March 2026
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ChatGPT: GPT-5.4 Changes the Game
&lt;/h3&gt;

&lt;p&gt;GPT-5.4, released March 5, 2026, brings &lt;strong&gt;native computer use&lt;/strong&gt; — it can interpret screenshots, operate browsers, and issue keyboard/mouse commands. Key upgrades:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1M token context window&lt;/strong&gt; (API) — up from 272K&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Computer use built-in&lt;/strong&gt; — first mainline model with native screen interaction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPT-5.3-Codex capabilities merged&lt;/strong&gt; — industry-leading code gen baked in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GDPval score of 83%&lt;/strong&gt; — matches or exceeds professionals across 44 occupations&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Claude: Opus 4.6 Takes the Crown
&lt;/h3&gt;

&lt;p&gt;Claude Opus 4.6 holds &lt;strong&gt;#1 on LMSYS Chatbot Arena&lt;/strong&gt; with 1504 Elo — real users preferring Claude over every other model in blind tests.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;80.8% on SWE-bench Verified&lt;/strong&gt; — top-tier for real-world software engineering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;200K context window&lt;/strong&gt; (1M beta) with 128K max output tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adaptive thinking&lt;/strong&gt; — dynamically decides reasoning depth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compaction&lt;/strong&gt; — automatic context summarization for infinite conversations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sleeper hit is &lt;strong&gt;Claude Sonnet 4.6&lt;/strong&gt; at 79.6% SWE-bench — one-fifth the cost of Opus and preferred over the previous Opus 4.5 in 59% of comparisons.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gemini: 3.1 Pro Is a Quiet Beast
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;94.3% on GPQA Diamond&lt;/strong&gt; — highest PhD-level science score ever&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;80.6% on SWE-bench Verified&lt;/strong&gt; — tied with Claude Opus 4.6&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;77.1% on ARC-AGI-2&lt;/strong&gt; — more than double Gemini 3 Pro's 31.1%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native 1M token context&lt;/strong&gt; — no beta flag, no waitlist&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multimodal&lt;/strong&gt; — text, images, 8.4 hrs audio, 1 hr video, 900-page PDFs&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Head-to-Head Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;ChatGPT (GPT-5.4)&lt;/th&gt;
&lt;th&gt;Claude (Opus 4.6)&lt;/th&gt;
&lt;th&gt;Gemini (3.1 Pro)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Context Window&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1M (API) / 272K (Chat)&lt;/td&gt;
&lt;td&gt;200K (1M beta)&lt;/td&gt;
&lt;td&gt;1M native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Max Output&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~32K tokens&lt;/td&gt;
&lt;td&gt;128K tokens&lt;/td&gt;
&lt;td&gt;65K tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LMSYS Rank&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Top 10&lt;/td&gt;
&lt;td&gt;#1 (1504 Elo)&lt;/td&gt;
&lt;td&gt;#2 (1500 Elo)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SWE-bench&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;77.2%&lt;/td&gt;
&lt;td&gt;80.8%&lt;/td&gt;
&lt;td&gt;80.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GPQA Diamond&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;92.8%&lt;/td&gt;
&lt;td&gt;91.3%&lt;/td&gt;
&lt;td&gt;94.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ARC-AGI-2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;73.3%&lt;/td&gt;
&lt;td&gt;75.2%&lt;/td&gt;
&lt;td&gt;77.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Image Gen&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;DALL-E 4&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Nano Banana 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Computer Use&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;td&gt;Via API&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Coding Agent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;GPT Codex&lt;/td&gt;
&lt;td&gt;Claude Code CLI&lt;/td&gt;
&lt;td&gt;Gemini CLI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Coding Showdown: Claude Code vs GPT Codex vs Gemini CLI
&lt;/h2&gt;

&lt;p&gt;The real competition is in the terminal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Claude Code: The Developer's First Choice
&lt;/h3&gt;

&lt;p&gt;Claude Code hit &lt;strong&gt;$2.5 billion ARR&lt;/strong&gt; — over half of Anthropic's enterprise revenue (&lt;a href="https://aitoolranked.com/blog/best-ai-for-coding-2026-developer-guide" rel="noopener noreferrer"&gt;more on AI coding tools&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;It runs in your terminal, reads your entire project, writes code, runs tests, handles git, and debugs failures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parallel subagents&lt;/strong&gt; — up to 7 simultaneous operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP integration&lt;/strong&gt; — Google Drive, Jira, Slack, custom tooling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full terminal access&lt;/strong&gt; — builds, tests, git, any CLI operation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VS Code and JetBrains extensions&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  GPT Codex: Async Powerhouse
&lt;/h3&gt;

&lt;p&gt;Codex is a &lt;strong&gt;senior engineer you delegate to&lt;/strong&gt;. It works autonomously in cloud sandboxes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Runs 1-30 minutes&lt;/strong&gt; on complex tasks with real-time progress&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud sandboxes&lt;/strong&gt; with test harnesses, linters, type checkers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactive mode with GPT-5.4&lt;/strong&gt; — steer mid-task&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parallel worktrees&lt;/strong&gt; — multiple agents on different project parts&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Power Move: Use Both Together
&lt;/h3&gt;

&lt;p&gt;The workflow gaining traction:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code generates&lt;/strong&gt; — faster real-time coding, deep local context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPT Codex reviews&lt;/strong&gt; — autonomous code review in cloud sandbox&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code iterates&lt;/strong&gt; — rapid fixes from Codex feedback&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Teams report &lt;strong&gt;30-40% more issues caught&lt;/strong&gt; than either tool alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gemini CLI: Present but Not Ready
&lt;/h3&gt;

&lt;p&gt;Free tier with 1,000 requests/day is generous, but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sequential execution only — no parallel tasks&lt;/li&gt;
&lt;li&gt;Frequent 429 rate limit errors&lt;/li&gt;
&lt;li&gt;Less refined agentic behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For professional work, Claude Code and GPT Codex are in a different league.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;ChatGPT&lt;/th&gt;
&lt;th&gt;Claude&lt;/th&gt;
&lt;th&gt;Gemini&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Free&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;GPT-4o&lt;/td&gt;
&lt;td&gt;Sonnet 4.6&lt;/td&gt;
&lt;td&gt;Flash, 1K req/day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Standard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$20/mo&lt;/td&gt;
&lt;td&gt;$20/mo&lt;/td&gt;
&lt;td&gt;$19.99/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Power&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$200/mo&lt;/td&gt;
&lt;td&gt;$100-200/mo&lt;/td&gt;
&lt;td&gt;$249.99/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  API (per million tokens)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GPT-5.4&lt;/td&gt;
&lt;td&gt;~$2.50&lt;/td&gt;
&lt;td&gt;~$10.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus 4.6&lt;/td&gt;
&lt;td&gt;$5.00&lt;/td&gt;
&lt;td&gt;$25.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 4.6&lt;/td&gt;
&lt;td&gt;$3.00&lt;/td&gt;
&lt;td&gt;$15.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 3.1 Pro&lt;/td&gt;
&lt;td&gt;$2.00&lt;/td&gt;
&lt;td&gt;$12.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Which Should You Choose?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;ChatGPT&lt;/strong&gt; → agentic automation, async coding delegation, enterprise teams&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude&lt;/strong&gt; → daily coding (Claude Code is unmatched), best writing quality, complex nuanced tasks&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gemini&lt;/strong&gt; → massive documents (1M context), best free tier, PhD-level reasoning&lt;/p&gt;

&lt;h3&gt;
  
  
  My Daily Setup
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code&lt;/strong&gt; (Pro $20/mo) — primary coding tool&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ChatGPT Pro&lt;/strong&gt; ($200/mo) — Codex for async delegation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemini AI Pro&lt;/strong&gt; ($19.99/mo) — research, Google integration&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Pick just one? &lt;strong&gt;Claude Pro at $20/mo&lt;/strong&gt;. Best value per dollar.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is ChatGPT still the best AI in 2026?&lt;/strong&gt;&lt;br&gt;
Most popular, but Claude holds #1 on LMSYS Arena and Gemini leads reasoning benchmarks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Claude better than ChatGPT for coding?&lt;/strong&gt;&lt;br&gt;
Yes — 80.8% vs 77.2% SWE-bench, and Claude Code CLI has $2.5B ARR.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use Claude Code and GPT Codex together?&lt;/strong&gt;&lt;br&gt;
Absolutely. Implementation + review. 30-40% more issues caught.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which has the largest context window?&lt;/strong&gt;&lt;br&gt;
GPT-5.4 and Gemini: 1M tokens. Gemini's is natively available everywhere.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://aitoolranked.com/blog/chatgpt-vs-claude-vs-gemini" rel="noopener noreferrer"&gt;AIToolRanked&lt;/a&gt;. More comparisons: &lt;a href="https://aitoolranked.com/blog/elevenlabs-review-2026-complete-analysis" rel="noopener noreferrer"&gt;ElevenLabs review&lt;/a&gt; | &lt;a href="https://aitoolranked.com/blog/best-ai-for-coding-2026-developer-guide" rel="noopener noreferrer"&gt;Best AI for coding&lt;/a&gt; | &lt;a href="https://aitoolranked.com/blog/grok-vs-chatgpt-2026-performance-comparison" rel="noopener noreferrer"&gt;Grok vs ChatGPT&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>chatgpt</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
