<?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: Dor</title>
    <description>The latest articles on DEV Community by Dor (@keepknow).</description>
    <link>https://dev.to/keepknow</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3962551%2F9829c47b-18b0-468c-9865-8a58b8135258.png</url>
      <title>DEV Community: Dor</title>
      <link>https://dev.to/keepknow</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/keepknow"/>
    <language>en</language>
    <item>
      <title>I built a tool that races you against the marathon world record</title>
      <dc:creator>Dor</dc:creator>
      <pubDate>Mon, 01 Jun 2026 11:53:50 +0000</pubDate>
      <link>https://dev.to/keepknow/i-built-a-tool-that-races-you-against-the-marathon-world-record-4n0k</link>
      <guid>https://dev.to/keepknow/i-built-a-tool-that-races-you-against-the-marathon-world-record-4n0k</guid>
      <description>&lt;p&gt;A quick fun one. I took the running app I work on, pulled out the Daniels-Gilbert VDOT model, and wired it to a question every runner secretly wonders: if you and the world-record holder started together, where would you be when they finish?&lt;/p&gt;

&lt;p&gt;You put in one race result. It predicts your equivalent time at 5K, 10K, half, and marathon, then animates a little race for each: your runner and the pro both set off down the track, the pro reaches the finish line, and a green bar fills up to wherever you are when they cross it. The marathon one is brutal. For a 22:00 5K runner, Sabastian Sawe finishes his 1:59:30 marathon while you are still about 18 km from the line.&lt;/p&gt;

&lt;p&gt;Try it: &lt;a href="https://runnerapp.pro/tools/vs-the-pros" rel="noopener noreferrer"&gt;https://runnerapp.pro/tools/vs-the-pros&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A few build notes for anyone doing similar:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The math is tiny.&lt;/strong&gt; VDOT is two equations (an oxygen-cost curve and a percent-of-max-you-can-hold curve) plus a bisection to invert it. I open-sourced that part: &lt;a href="https://github.com/dortaldt/vdot-runner" rel="noopener noreferrer"&gt;https://github.com/dortaldt/vdot-runner&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The animation is just CSS custom properties + transitions.&lt;/strong&gt; Each lane sets &lt;code&gt;--up&lt;/code&gt; (your position) and &lt;code&gt;--p&lt;/code&gt; (the pro at 100). The runner markers transition &lt;code&gt;left&lt;/code&gt;, and a fill div transitions &lt;code&gt;width&lt;/code&gt;, both driven by the same variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.track-fill&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--up&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1%&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="m"&gt;1.9s&lt;/span&gt; &lt;span class="n"&gt;cubic-bezier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;.22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;.61&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;.18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.runner.you&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--up&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1%&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100%&lt;/span&gt; &lt;span class="n"&gt;-&lt;/span&gt; &lt;span class="m"&gt;26px&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;left&lt;/span&gt; &lt;span class="m"&gt;1.9s&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;Set the variable to 0 in markup, then to the target in a double &lt;code&gt;requestAnimationFrame&lt;/code&gt; so the transition actually fires:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;requestAnimationFrame&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;fill&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--up&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;you&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--up&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;pro&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--p&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;No signup, no tracking, all client-side.&lt;/strong&gt; The whole thing is one HTML file. Runner figures are inline SVG (SF Symbols are licensed to Apple platforms only, so they are not legal on the web, which trips people up).&lt;/p&gt;

&lt;p&gt;It is a toy, but it is the kind of toy runners screenshot and argue about. Curious what percentage you land at.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>showdev</category>
      <category>webdev</category>
      <category>css</category>
    </item>
    <item>
      <title>How race time predictors actually work: Daniels VDOT vs Riegel, with the JavaScript</title>
      <dc:creator>Dor</dc:creator>
      <pubDate>Mon, 01 Jun 2026 10:52:20 +0000</pubDate>
      <link>https://dev.to/keepknow/how-race-time-predictors-actually-work-daniels-vdot-vs-riegel-with-the-javascript-55k1</link>
      <guid>https://dev.to/keepknow/how-race-time-predictors-actually-work-daniels-vdot-vs-riegel-with-the-javascript-55k1</guid>
      <description>&lt;p&gt;Every race time predictor on the internet is doing one of two things. It is either fitting a power law to your result (Pete Riegel, 1981) or running you through a physiology model (Jack Daniels and Jimmy Gilbert, 1979). I implemented both in plain JavaScript for a running app I work on, so here is how they actually work, with the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 1: Riegel's power law
&lt;/h2&gt;

&lt;p&gt;Riegel was a mechanical engineer and a competitive runner. In 1981 he fit a simple relationship to world-record performances: finish time scales with distance raised to a small power above 1.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// T2 = T1 * (D2 / D1) ^ 1.06&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;riegel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d2&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;t1&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d2&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;d1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.06&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// 20:00 5K -&amp;gt; predicted 10K&lt;/span&gt;
&lt;span class="nf"&gt;riegel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ~2497s = 41:37&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exponent 1.06 captures the fact that you cannot hold a pace forever. Double the distance and time slightly more than doubles. It needs no physiology and almost no input. The weakness is that 1.06 was fit to world records, so it flatters amateur marathon predictions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method 2: Daniels and Gilbert's VDOT model
&lt;/h2&gt;

&lt;p&gt;Two years earlier, Daniels and Gilbert went through physiology instead of around it. Two equations. The first is the oxygen cost of running at a velocity &lt;code&gt;v&lt;/code&gt; in metres per minute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;vo2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;4.60&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.182258&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.000104&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;v&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 second is the fraction of VO2max you can sustain for a race lasting &lt;code&gt;t&lt;/code&gt; minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;pctMax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.1894393&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;exp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.012778&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.2989558&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;exp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.1932605&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From a race you derive a fitness score, VDOT, then solve for the time at a new distance where oxygen demand matches what you can sustain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;vdotFromRace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;           &lt;span class="c1"&gt;// d metres, t seconds&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tMin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;tMin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;vo2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;pctMax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tMin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;timeForDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vdot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;     &lt;span class="c1"&gt;// bisection on duration&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;hi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;implied&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;vo2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;mid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;pctMax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mid&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;implied&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;vdot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nx"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;hi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  They mostly agree, until the marathon
&lt;/h2&gt;

&lt;p&gt;For trained runners predicting between nearby distances, the two methods land within a percent or two of each other. The marathon is where both break down. Vickers and Vertosick (2016) studied recreational runners and found real marathon times ran about 4 to 5 percent slower than the prediction, with much wider scatter.&lt;/p&gt;

&lt;p&gt;The reason is not the math. A 5K tests your aerobic ceiling. A marathon tests fat oxidation, glycogen, fluid replacement, heat tolerance, and pacing discipline, none of which a 5K loads. A predictor tells you what is possible if you do the training. It does not tell you what you will run on an untrained marathon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Showing both is the honest move
&lt;/h2&gt;

&lt;p&gt;The practical takeaway I landed on: run both methods and show the spread. When they agree, trust it. When they diverge by more than a couple percent, that gap is itself information about how far you are extrapolating.&lt;/p&gt;

&lt;p&gt;I put both behind a &lt;a href="https://runnerapp.pro/tools/race-time-predictor" rel="noopener noreferrer"&gt;race time predictor&lt;/a&gt; and a set of &lt;a href="https://runnerapp.pro/pace/" rel="noopener noreferrer"&gt;goal-time pace charts&lt;/a&gt; if you want to see the outputs without wiring up the code. References for anyone going deeper: Riegel (1981), American Scientist; Daniels and Gilbert (1979), Oxygen Power; Vickers and Vertosick (2016), BMC Sports Science.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>math</category>
      <category>webdev</category>
      <category>running</category>
    </item>
    <item>
      <title>I built an on-device marathon coach for iPhone and Apple Watch (no account, no server)</title>
      <dc:creator>Dor</dc:creator>
      <pubDate>Mon, 01 Jun 2026 10:51:27 +0000</pubDate>
      <link>https://dev.to/keepknow/i-built-an-on-device-marathon-coach-for-iphone-and-apple-watch-no-account-no-server-1lp</link>
      <guid>https://dev.to/keepknow/i-built-an-on-device-marathon-coach-for-iphone-and-apple-watch-no-account-no-server-1lp</guid>
      <description>&lt;p&gt;For a while now I have been building a running app called Smart Runner, and the whole thing is a bet against how the category usually works. No subscription. No account. No server. Everything lives on the phone. Here is why, and the parts that were actually hard.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bet
&lt;/h2&gt;

&lt;p&gt;Most serious running apps are subscriptions, and your training history sits on their servers. If you stop paying, you lose access to years of your own runs. If the company gets acquired or shuts a feature, your data goes with it.&lt;/p&gt;

&lt;p&gt;I wanted the opposite. Pay once, and the data never leaves your device. The app reads from Apple Health and writes to local storage. There is no Smart Runner backend to leak, sell, or sunset.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SwiftUI + SwiftData&lt;/strong&gt; for everything local. No backend at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HealthKit&lt;/strong&gt; for reading workouts, heart rate samples, and writing structured workouts back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WatchConnectivity&lt;/strong&gt; to push the planned workout to the Apple Watch and pull the result back.&lt;/li&gt;
&lt;li&gt;A native &lt;strong&gt;watchOS&lt;/strong&gt; app that plays the workout back with live pace and HR zones.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The part that bit me
&lt;/h2&gt;

&lt;p&gt;WatchConnectivity plus SwiftData is where I lost the most time. I was handing SwiftData model objects to the off-thread send, and the model context would get touched off its own thread and corrupt state. Intermittent, ugly, hard to reproduce.&lt;/p&gt;

&lt;p&gt;The fix was to snapshot the planned workout into plain value types (a dictionary of &lt;code&gt;[String: Any]&lt;/code&gt;) on the main context &lt;em&gt;before&lt;/em&gt; the background send, so nothing model-bound ever crosses a thread boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Wrong: passing the SwiftData model into the off-thread send&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transferUserInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plannedWorkout&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;// Right: snapshot to value types on-context first, then send&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;snapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;plannedWorkout&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asTransferDictionary&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;// [String: Any], no model refs&lt;/span&gt;
&lt;span class="kt"&gt;DispatchQueue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;global&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transferUserInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snapshot&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;Obvious in hindsight. Most concurrency bugs are.&lt;/p&gt;

&lt;h2&gt;
  
  
  The training engine
&lt;/h2&gt;

&lt;p&gt;The coaching side is built on published running science rather than a black box:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;VDOT&lt;/strong&gt; for pace zones, from Daniels and Gilbert's 1979 oxygen-cost model. One race result gives you Easy, Marathon, Threshold, Interval, and Repetition paces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ATL / CTL / TSB&lt;/strong&gt; training-load tracking, the same acute-versus-chronic load model endurance coaches use, so the plan knows when you are fresh, fit, or cooked.&lt;/li&gt;
&lt;li&gt;The plan recalculates after every run instead of being a fixed 16 week PDF.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want the math, there is a free &lt;a href="https://runnerapp.pro/tools/vdot-calculator" rel="noopener noreferrer"&gt;VDOT calculator&lt;/a&gt; that exposes the numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why on-device at all
&lt;/h2&gt;

&lt;p&gt;Three reasons that turned out to matter more than I expected:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your training history should outlive any subscription. Pay once, keep it forever.&lt;/li&gt;
&lt;li&gt;There is no account step, so onboarding is just opening the app.&lt;/li&gt;
&lt;li&gt;No server means no server bills, which is part of how the pay-once model even works.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The trade-off is real: no web dashboard, no cross-platform sync beyond Apple Health, iPhone and Apple Watch only. For the runner who wants a private, owned training tool, that trade is the point.&lt;/p&gt;

&lt;p&gt;If you want to see it, it is here: &lt;a href="https://runnerapp.pro" rel="noopener noreferrer"&gt;Smart Runner&lt;/a&gt;. Happy to get into the watch sync or the training-load math in the comments.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>ios</category>
      <category>swift</category>
      <category>watchos</category>
    </item>
  </channel>
</rss>
