Skip to content

Instantly share code, notes, and snippets.

@mattsson
Last active November 25, 2020 14:02
Show Gist options
  • Save mattsson/3e076854244e32c9665b17c909435b60 to your computer and use it in GitHub Desktop.
Save mattsson/3e076854244e32c9665b17c909435b60 to your computer and use it in GitHub Desktop.
Tour de France Standings Scriptable Widget for iOS 14

https://imgur.com/a/q8R5iNf

To use:

  1. Update iOS 14
  2. Install Scriptable app: https://scriptable.app/
  3. Add a new script and paste in the script code
  4. Add a medium-sized widget to your iPhone home screen by long-holding, tapping the Plus button in the top left and selecting Scriptable from the list of widget sources
  5. Configure the widget to use the Tour de France Standings script by long-holding on the widget and tapping Edit Widget

NOTE: I have not yet added support for all flags, see the flagForCountry() function.

// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: yellow; icon-glyph: bicycle;
const textSize = 11
const fontName = "Menlo-Regular"
const preferredTextWidth = 45
const ridersToShow = 7
const widget = new ListWidget()
widget.spacing = 4
const heading = widget.addText("Tour de France Standings")
heading.textSize = textSize + 1
heading.fontName = "Menlo-Bold"
heading.centerAlignText()
await addTourStandings()
widget.presentLarge()
Script.setWidget(widget)
Script.complete()
async function addTourStandings() {
const tourStartDate = new Date("2020-08-29")
const now = new Date()
var dayOfTourYesterday = daysBetween(tourStartDate, now)
console.log(dayOfTourYesterday + " days between " + tourStartDate + " and " + now)
// This allows you to add standings for more than the latest stage,
// but it may exceed the allowed memory consumption of widgets and thus not display at all
// await addRankingsForDay(dayOfTour - 1)
// widget.addSpacer(10)
await addRankingsForDay(dayOfTourYesterday + 1)
}
async function addRankingsForDay(today) {
var selectedDay = today
var selectedPreviousDay = null
var latestStandings = null
var previousStandings = null
var dayText = ""
// If today is not rest day, try to fetch
if (stageForDay(today) != null) {
const stage = stageForDay(today)
latestStandings = await topStandingsForStage(stage)
}
// Check if today was rest day or data not ready yet
if (latestStandings == null) {
// Check if yesterday was not rest day
if (stageForDay(today - 1) != null) {
selectedDay = today - 1
dayText = " (Yesterday)"
} else {
// Yesterday was rest day, so 2 days ago must be fine
selectedDay = today - 2
dayText = " (2 days ago)"
}
const stage = stageForDay(selectedDay)
latestStandings = await topStandingsForStage(stage)
} else {
dayText = " (Today)"
}
// If day before selected day is rest day, then go back another day
if (stageForDay(selectedDay - 1) != null) {
selectedPreviousDay = selectedDay - 1
} else {
selectedPreviousDay = selectedDay - 2
}
const prevStage = stageForDay(selectedPreviousDay)
previousStandings = await topStandingsForStage(prevStage)
// Only show top 5
latestStandings = latestStandings.slice(0, ridersToShow)
const heading = widget.addText("Stage " + stageForDay(selectedDay) + dayText)
heading.font = new Font(fontName, textSize)
for (standing of latestStandings) {
const position = standing.position
const rider = standing.rider
const name = rider.shortname
const country = rider.nationality
var rankChangeIcon = "🆕"
var rankChangeText = "(--)"
for (oldStanding of previousStandings) {
if (name == oldStanding.rider.shortname) {
oldPosition = oldStanding.position
if (oldPosition < position) {
rankChangeIcon = "⬇️"
} else if (oldPosition > position) {
rankChangeIcon = "⬆️"
} else {
rankChangeIcon = "🔁"
}
const rankChange = oldPosition - position
if (rankChange > 0) {
rankChangeText = "(+" + rankChange + ")"
} else if (rankChange < 0) {
rankChangeText = "(" + rankChange +
")"
}
break
}
}
const time = standing.time
const gap = standing.gap
var timeText
if (position == 1) {
timeText = formatTime(time)
} else {
timeText = "+" + formatTime(gap)
}
const textBeforeTime = position + ". " + rankChangeText + " " + rankChangeIcon + " " + flagForCountry(country) +
" " + name
var missingSpaces = preferredTextWidth - textBeforeTime.length - timeText.length
var spacesString = ""
while (missingSpaces > spacesString.length) {
spacesString = spacesString + " "
}
const finalText = textBeforeTime + spacesString + timeText
// console.log(name)
// console.log(textBeforeTime.length)
// console.log(spacesString.length)
// console.log(timeText.length)
// console.log("total " + finalText.length)
const widgetText = widget.addText(finalText)
widgetText.font = new Font(fontName, textSize)
}
}
async function topStandingsForStage(stage) {
var stageForUrl = new String(stage)
if (stage < 10) {
stageForUrl = "0" + stageForUrl
}
const url = "https://prod-tdf-api.netcosports.com/rankings/" + stageForUrl + "00/ITG"
console.log(url)
const request = new Request(url)
const string = await request.loadString()
const json = JSON.parse(string)
// If response is array, then we have results from this stage in the tour
if (Array.isArray(json)) {
const allRanks = json[0].ranks
return allRanks
} else {
return null
}
}
function flagForCountry(country) {
switch (country) {
case "FRA":
return "🇫🇷"
case "GBR":
return "🇬🇧"
case "SLO":
return "🇸🇮"
case "SUI":
return "🇨🇭"
case "ITA":
return "🇮🇹"
case "COL":
return "🇨🇴"
case "NED":
return "🇳🇱"
case "DAN":
return "🇩🇰"
case "AUS":
return "🇦🇺"
case "ESP":
return "🇪🇸"
default:
console.log("missing country: " + country)
return "🇺🇳"
}
}
function stageForDay(day) {
// Return null for rest days or if the tour is done
if (day == 10 || day == 17 || day > 23) {
return null
}
const dayToStageMap = {
1:1,
2:2,
3:3,
4:4,
5:5,
6:6,
7:7,
8:8,
9:9,
// Day 10 is rest day
11:10,
12:11,
13:12,
14:13,
15:14,
16:15,
// Day 17 is rest day
18:16,
19:17,
20:18,
21:19,
22:20,
23:21
}
return dayToStageMap[day]
}
function daysBetween(date1, date2) {
const firstDateUTC = Date.UTC(date1.getFullYear(), date1.getMonth(), date1.getDate())
const secondDateUTC = Date.UTC(date2.getFullYear(), date2.getMonth(), date2.getDate())
// console.log(new Date(firstDateUTC))
// console.log(new Date(secondDateUTC))
return Math.floor((secondDateUTC - firstDateUTC) / (1000 * 60 * 60 * 24))
}
function formatTime(ms) {
const seconds = ms / 1000
const minutes = Math.floor(seconds / 60)
const hours = Math.floor(minutes / 60)
const modSecs = seconds % 60
const formattedSecs = (modSecs < 10 ? "0" : "") + modSecs + "\""
const modMins = minutes % 60
const formattedMins = (modMins < 10 ? "0" : "") + modMins + "' "
var formattedHours = ""
if (hours >= 1) {
formattedHours = hours + "h "
}
return formattedHours + formattedMins + formattedSecs
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment