Estimates when a parkrunner will reach their next milestone. Assumes participation at every available parkrun (regular, junior, or volunteer) on Saturdays or Sundays. Special events are excluded from calculations.
What is a userscript?
A userscript is a small piece of JavaScript that runs in your browser and enhances specific websites. These scripts work with parkrun event pages, parkrunner profile pages, and results pages, and can be used with any userscript manager including Userscripts, Tampermonkey, Violentmonkey, or any compatible browser extension.
Installation
-
Install a userscript manager for your browser:
- Desktop: Userscripts (Safari), Tampermonkey (Chrome, Firefox, Edge, Opera), or Violentmonkey (Orion)
- iOS: Userscripts (Safari) or Violentmonkey (Orion)
- Android: Install Kiwi Browser, then install Tampermonkey or Violentmonkey from the Chrome Web Store.
- Click the install link below for this script.
- Click “Install” when prompted by your userscript manager.
Install parkrun Next Milestone Estimate
Bookmarklet version
You can also use this script as a bookmarklet (a bookmark whose URL is JavaScript), which can be useful on browsers that support bookmarks but not userscript managers.
Desktop bookmarklet
Drag this link to your bookmarks bar:
parkrun Next Milestone Estimate bookmarklet
Mobile bookmarklet
On mobile browsers that let you edit bookmark URLs:
- Copy the JavaScript code below to your clipboard.
- Create a new bookmark for any page.
- Edit the bookmark and replace its URL with the code you copied.
- Navigate to the relevant parkrun page.
- Select the bookmark to run the script.
javascript:function _createForOfIteratorHelper(e,t){var n,r,a,o,l="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(l)return a=!(r=!0),{s:function(){l=l.call(e)},n:function(){var e=l.next();return r=e.done,e},e:function(e){a=!0,n=e},f:function(){try{r||null==l.return||l.return()}finally{if(a)throw n}}};if(Array.isArray(e)||(l=_unsupportedIterableToArray(e))||t&&e&&"number"==typeof e.length)return l&&(e=l),o=0,{s:t=function(){},n:function(){return o>=e.length?{done:!0}:{done:!1,value:e[o++]}},e:function(e){throw e},f:t};throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function _unsupportedIterableToArray(e,t){var n;if(e)return"string"==typeof e?_arrayLikeToArray(e,t):"Map"===(n="Object"===(n={}.toString.call(e).slice(8,-1))&&e.constructor?e.constructor.name:n)||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?_arrayLikeToArray(e,t):void 0}function _arrayLikeToArray(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=Array(t);n<t;n++)r[n]=e[n];return r}(()=>{var a={10:{restricted_age:"J"},25:{},50:{},100:{},250:{},500:{},1e3:{}},u={11:{restricted_age:"J",name:"Half marathon"},21:{restricted_age:"J",name:"Marathon"},50:{restricted_age:"J",name:"Ultra marathon"},100:{restricted_age:"J",name:"junior parkrun 100"},250:{restricted_age:"J",name:"junior parkrun 250"}},d={10:{restricted_age:"J"},25:{},50:{},100:{},250:{},500:{},1e3:{}};function s(){var e,t=_createForOfIteratorHelper((0<arguments.length&&void 0!==arguments[0]?arguments[0]:document).querySelectorAll("h3"));try{for(t.s();!(e=t.n()).done;){var n,r,a=e.value,o=(null!=(n=null==(r=a.textContent)?void 0:r.trim())?n:"").match(/([\d,]+)\s+parkruns?\b/i);if(o)return{heading:a,total:parseInt(o[1].replace(/,/g,""),10)}}}catch(e){t.e(e)}finally{t.f()}return null}function c(){var e,t=_createForOfIteratorHelper((0<arguments.length&&void 0!==arguments[0]?arguments[0]:document).querySelectorAll("h3"));try{for(t.s();!(e=t.n()).done;){var n,r,a=e.value,o=(null!=(n=null==(r=a.textContent)?void 0:r.trim())?n:"").match(/([\d,]+)\s+junior\s+parkruns?\s+total/i);if(o)return{heading:a,total:parseInt(o[1].replace(/,/g,""),10)}}}catch(e){t.e(e)}finally{t.f()}return null}function g(){var e,t=_createForOfIteratorHelper((0<arguments.length&&void 0!==arguments[0]?arguments[0]:document).querySelectorAll("p"));try{for(t.s();!(e=t.n()).done;){var n,r=(null!=(n=e.value.textContent)?n:"").match(/Most recent age category was\s+([A-Z]+\d+[-\d]*)/i);if(r)return r[1]}}catch(e){t.e(e)}finally{t.f()}return null}function m(){var e,t=(0<arguments.length&&void 0!==arguments[0]?arguments[0]:document).querySelector("#volunteer-summary");return!t||!(e=(e=t.nextElementSibling)&&"TABLE"===e.tagName?e:null==(t=t.parentElement)?void 0:t.querySelector("table"))||!(t=e.querySelector("tfoot tr"))||(e=t.querySelectorAll("td")).length<2||!(t=e[1].querySelector("strong"))||(e=null!=(t=null==(e=t.textContent)?void 0:e.trim())?t:"",t=parseInt(e.replace(/,/g,""),10),Number.isNaN(t))?null:t}function p(t){var e,n=1<arguments.length&&void 0!==arguments[1]?arguments[1]:null,r=2<arguments.length&&void 0!==arguments[2]?arguments[2]:a;return null!=(e=Object.keys(r).map(Number).sort(function(e,t){return e-t}).find(function(e){return!(e<=t)&&((e=r[e]).restricted_age&&n?n.startsWith(e.restricted_age):!e.restricted_age)}))?e:null}function f(e,t,n){e=p(e,t,n);return e?{value:e,definition:n[e]}:null}function y(){var e,t,n=(0<arguments.length&&void 0!==arguments[0]?arguments[0]:document).querySelector("table#results tbody");return!n||!(n=n.querySelector("tr"))||(n=n.querySelectorAll("td")).length<2||!(n=n[1].querySelector("a"))||!(n=null==(n=n.textContent)?void 0:n.trim())||3!==(n=n.split("/")).length?null:(e=parseInt(n[0],10),t=parseInt(n[1],10)-1,n=parseInt(n[2],10),new Date(n,t,e))}function r(){var e,t,n,r,a=0<arguments.length&&void 0!==arguments[0]?arguments[0]:new Date,o=1<arguments.length&&void 0!==arguments[1]?arguments[1]:null,l=new Date(a),i=l.getDay();return 6===i?(o&&(r=o.getFullYear(),e=o.getMonth(),o=o.getDate(),t=a.getFullYear(),n=a.getMonth(),a=a.getDate(),r===t)&&e===n&&o===a&&l.setDate(l.getDate()+7),l.setHours(0,0,0,0)):(r=(6-i+7)%7,l.setDate(l.getDate()+r),l.setHours(0,0,0,0)),l}function o(){var e,t,n,r,a=0<arguments.length&&void 0!==arguments[0]?arguments[0]:new Date,o=1<arguments.length&&void 0!==arguments[1]?arguments[1]:null,l=new Date(a),i=l.getDay();return 0===i?(o&&(r=o.getFullYear(),e=o.getMonth(),o=o.getDate(),t=a.getFullYear(),n=a.getMonth(),a=a.getDate(),r===t)&&e===n&&o===a&&l.setDate(l.getDate()+7),l.setHours(0,0,0,0)):(r=(0-i+7)%7,l.setDate(l.getDate()+r),l.setHours(0,0,0,0)),l}function h(e,t){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:new Date;return!t||t<=e?null:(t=t-e,e=r(n,3<arguments.length&&void 0!==arguments[3]?arguments[3]:null),(n=new Date(e)).setDate(e.getDate()+7*(t-1)),n)}function v(e,t){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:new Date;return!t||t<=e?null:(t=t-e,e=o(n,3<arguments.length&&void 0!==arguments[3]?arguments[3]:null),(n=new Date(e)).setDate(e.getDate()+7*(t-1)),n)}function D(e){return e.toLocaleDateString(void 0,{weekday:"long",year:"numeric",month:"long",day:"numeric"})}function l(e){var t=1<arguments.length&&void 0!==arguments[1]?arguments[1]:new Date,t=new Date(t),n=(t.setHours(0,0,0,0),new Date(t)),e=(n.setDate(n.getDate()+7),new Date(e));return e.setHours(0,0,0,0),t<=e&&e<n}function x(e,t){var n,r,a=2<arguments.length&&void 0!==arguments[2]?arguments[2]:new Date;t&&e&&l(t,a)&&(n=D(t),r=[],function e(t){if(t.nodeType===Node.TEXT_NODE)r.push(t);else for(var n=0;n<t.childNodes.length;n++)e(t.childNodes[n])}(e),r.forEach(function(e){var t;e.textContent.includes(n)&&((t=document.createElement("span")).style.backgroundColor="#ffeb3b",t.style.padding="0 2px",t.style.borderRadius="2px",t.textContent=e.textContent,e.parentNode.replaceChild(t,e))}))}var i="parkrun-volunteer-credits",S="parkrun-volunteer-days";function C(){var e=0<arguments.length&&void 0!==arguments[0]?arguments[0]:i;if("undefined"!=typeof localStorage){var t=localStorage.getItem(e);if(t)try{return JSON.parse(t)}catch(e){return{saturday:!0,sunday:!0}}t=localStorage.getItem(S);if(t)try{var n=JSON.parse(t);return localStorage.setItem(e,JSON.stringify(n)),localStorage.removeItem(S),n}catch(e){}}return{saturday:!0,sunday:!0}}function b(e){"undefined"!=typeof localStorage&&localStorage.setItem(1<arguments.length&&void 0!==arguments[1]?arguments[1]:i,JSON.stringify(e))}function E(e,t){var n,r,a,o,l,i=2<arguments.length&&void 0!==arguments[2]?arguments[2]:new Date;return t&&!(t<=e)&&(t=t-e,(e=(3<arguments.length&&void 0!==arguments[3]?arguments[3]:null)||C()).saturday||e.sunday)?e.saturday&&e.sunday?(a=(6-(l=new Date(i)).getDay()+7)%7,l.setDate(l.getDate()+a),r=(0-(a=new Date(i)).getDay()+7)%7,a.setDate(a.getDate()+r),r=l<a?l:a,l=l<a,a=Math.floor((t-1)/2),o=(t-1)%2,(n=new Date(r)).setDate(r.getDate()+7*a),1==o&&n.setDate(n.getDate()+(l?1:6)),n.setHours(0,0,0,0),n):e.saturday?(a=(6-(r=new Date(i)).getDay()+7)%7,r.setDate(r.getDate()+a),r.setDate(r.getDate()+7*(t-1)),r.setHours(0,0,0,0),r):e.sunday?(l=(0-(o=new Date(i)).getDay()+7)%7,o.setDate(o.getDate()+l),o.setDate(o.getDate()+7*(t-1)),o.setHours(0,0,0,0),o):null:null}function N(e,t,n){var r,a,o,l=3<arguments.length&&void 0!==arguments[3]?arguments[3]:null;e&&"true"!==e.dataset.milestoneEstimateApplied&&(t=document.createTextNode(" (expected to reach ".concat(t," around ").concat(n,")")),(n=Array.from(e.childNodes).filter(function(e){return e.nodeType===Node.TEXT_NODE}).find(function(e){return null==(e=e.textContent)?void 0:e.includes("&")}))?0<=(a=(r=null!=(r=n.textContent)?r:"").indexOf("&"))?(o=r.slice(0,a).trimEnd(),r=r.slice(a).trimStart(),a=document.createTextNode(o),o=document.createTextNode(" ".concat(r)),e.insertBefore(a,n),e.insertBefore(t,n),e.insertBefore(o,n),e.removeChild(n)):e.insertBefore(t,n):e.appendChild(t),e.dataset.milestoneEstimateApplied="true",l)&&x(e,l)}function A(e,t){var n,r,a,o,l,i=2<arguments.length&&void 0!==arguments[2]?arguments[2]:null,u=3<arguments.length&&void 0!==arguments[3]?arguments[3]:null;e&&"true"!==e.dataset.volunteerCreditsApplied&&((n=document.createElement("p")).id="volunteer-credits-summary",t="".concat(t," volunteer credits total"),i&&u&&(t+=" (expected to reach ".concat(i," around ").concat(u,")")),n.textContent=t,(i=document.createElement("div")).id="volunteer-credits-preferences",i.style.marginTop="0.5em",i.style.fontSize="0.9em",t=C(),(r=document.createElement("label")).style.marginRight="1em",r.style.cursor="pointer",(a=document.createElement("input")).type="checkbox",a.checked=t.saturday,a.id="volunteer-saturday",a.addEventListener("change",function(){var e=C();e.saturday=a.checked,b(e),window.location.reload()}),r.appendChild(a),r.appendChild(document.createTextNode(" Volunteer on Saturdays")),(o=document.createElement("label")).style.cursor="pointer",(l=document.createElement("input")).type="checkbox",l.checked=t.sunday,l.id="volunteer-sunday",l.addEventListener("change",function(){var e=C();e.sunday=l.checked,b(e),window.location.reload()}),o.appendChild(l),o.appendChild(document.createTextNode(" Volunteer on Sundays")),i.appendChild(r),i.appendChild(o),e.insertAdjacentElement("afterend",n),n.insertAdjacentElement("afterend",i),e.dataset.volunteerCreditsApplied="true",u)&&(t=new Date(u),Number.isNaN(t.getTime())||x(n,t))}function w(e,t,n){var r=3<arguments.length&&void 0!==arguments[3]?arguments[3]:null;e&&"true"!==e.dataset.juniorMilestoneEstimateApplied&&(e.appendChild(document.createTextNode(" (expected to reach ".concat(t," around ").concat(n,")"))),e.dataset.juniorMilestoneEstimateApplied="true",r)&&x(e,r)}function k(e){var t,n,r,a,o;e&&"true"!==e.dataset.assumptionsInfoApplied&&((t=document.createElement("div")).id="milestone-assumptions-info",t.style.marginTop="1em",t.style.padding="0.75em",t.style.backgroundColor="#f5f5f5",t.style.border="1px solid #ddd",t.style.borderRadius="4px",t.style.fontSize="0.85em",t.style.color="#555",(n=document.createElement("strong")).textContent="ℹ️ Assumptions behind expected dates:",n.style.display="block",n.style.marginBottom="0.5em",(r=document.createElement("ul")).style.margin="0.5em 0 0 0",r.style.padding="0 0 0 1.25em",r.style.listStyle="none",(a=document.createElement("li")).textContent="You participate at every available parkrun day",a.style.margin="0.25em 0",(o=document.createElement("li")).textContent="Special events are excluded from the calculations",o.style.margin="0.25em 0",r.appendChild(a),r.appendChild(o),t.appendChild(n),t.appendChild(r),((null==(a=e.parentElement)?void 0:a.querySelector("#volunteer-credits-preferences"))||e).insertAdjacentElement("afterend",t),e.dataset.assumptionsInfoApplied="true")}function e(){var e,t,n,r,a,o=0<arguments.length&&void 0!==arguments[0]?arguments[0]:document,l=1<arguments.length&&void 0!==arguments[1]?arguments[1]:new Date,i=s(o);i?(e=g(o),(r=p(i.total,e))?(a=y(o),(a=h(i.total,r,l,a))?(N(i.heading,r,D(a),a),null!==(r=m(o))&&((n=p(r,a=null,d))&&(t=E(r,n,l))&&(a=D(t)),A(i.heading,r,n,a)),(t=c(o))&&null!=e&&e.startsWith("J")&&null!=(r=f(t.total,e,u))&&null!=(n=r.definition)&&n.name&&(a=v(t.total,r.value,l))&&w(t.heading,r.definition.name,D(a),a),k(i.heading)):console.log("Unable to calculate milestone date")):console.log("No upcoming milestone found")):console.log("Parkrun total heading not found")}"undefined"!=typeof module&&module.exports?module.exports={milestones:a,juniorMilestones:u,volunteerMilestones:d,findParkrunTotalHeading:s,findJuniorParkrunTotalHeading:c,findAgeCategory:g,findVolunteerCreditsTotal:m,findMostRecentFinishDate:y,getNextMilestone:p,getNextMilestoneDefinition:f,getNextSaturday:r,getNextSunday:o,calculateMilestoneDate:h,calculateJuniorMilestoneDate:v,formatDate:D,isDateInNextWeek:l,highlightDateIfNeeded:x,getVolunteerCreditPreferences:C,setVolunteerCreditPreferences:b,getNextVolunteerMilestoneDate:E,appendMilestoneEstimate:N,appendJuniorMilestoneEstimate:w,appendVolunteerCreditsSummary:A,appendAssumptionsInfo:k,applyMilestoneEstimate:e}:e()})();