<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Patrick Van der Spiegel's Notes</title><link>http://patrick.vanderspie.gl/notes/</link><description>Recent content in Notes on Patrick Van der Spiegel</description><generator>Hugo</generator><language>en-GB</language><lastBuildDate>Sun, 03 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="http://patrick.vanderspie.gl/notes/index.xml" rel="self" type="application/rss+xml"/><item><title>Maasmarathon</title><link>http://patrick.vanderspie.gl/notes/2026-05-03/</link><pubDate>Sun, 03 May 2026 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/notes/2026-05-03/</guid><description>&lt;div class="fitroute">
 &lt;figure class="light">
 &lt;img src="http://patrick.vanderspie.gl/notes/2026-05-03/route.png" alt="Maasmarathon route" loading="lazy" />
 &lt;/figure>
 &lt;figure class="dark">
 &lt;img src="http://patrick.vanderspie.gl/notes/2026-05-03/route-dark.png" alt="Maasmarathon route" loading="lazy" />
 &lt;/figure>&lt;div class="fitroute-chips">
 &lt;div class="fitroute-chip">
 &lt;span class="fitroute-label">Distance&lt;/span>
 &lt;span class="fitroute-value">42.99 km&lt;/span>
 &lt;/div>
 &lt;div class="fitroute-chip">
 &lt;span class="fitroute-label">Moving time&lt;/span>
 &lt;span class="fitroute-value">3:59:09&lt;/span>
 &lt;/div>
 &lt;div class="fitroute-chip">
 &lt;span class="fitroute-label">Elevation&lt;/span>
 &lt;span class="fitroute-value">273 m&lt;/span>
 &lt;/div>
 &lt;/div>&lt;hr class="hr-subtle" />
&lt;/div>

&lt;style>
.hr-subtle {
 background: rgba(0, 0, 0, 0.08);
 margin: 1em 0 1.5em;
}
html.dark .hr-subtle {
 background: rgba(255, 255, 255, 0.08);
}
@media (prefers-color-scheme: dark) {
 html:not(.dark):not(.light) .hr-subtle {
 background: rgba(255, 255, 255, 0.08);
 }
}
.fitroute figure {
 margin: 0;
}
.fitroute-chips {
 display: flex;
 gap: 0.5em;
 margin-top: 0.5em;
}
.fitroute-chip {
 flex: 1;
 display: flex;
 flex-direction: column;
 align-items: center;
 justify-content: center;
 padding: 0.6em 0.5em;
 border: 1px solid #d0d0d0;
 border-radius: 4px;
 background-color: #f3f4f6;
 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.fitroute-label {
 font-size: 0.75em;
 color: #888;
 text-transform: uppercase;
 letter-spacing: 0.05em;
}
.fitroute-value {
 font-size: 1.1em;
 font-weight: 600;
 color: #242424;
}
@media (prefers-color-scheme: dark) {
 html:not(.dark):not(.light) .fitroute-chip {
 background-color: #1a1a1a;
 border-color: #333;
 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
 }
 html:not(.dark):not(.light) .fitroute-label { color: #777; }
 html:not(.dark):not(.light) .fitroute-value { color: #dadada; }
}
html.dark .fitroute-chip {
 background-color: #1a1a1a;
 border-color: #333;
 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
html.dark .fitroute-label { color: #777; }
html.dark .fitroute-value { color: #dadada; }
&lt;/style>

&lt;p>Spent the weekend at &lt;a href="https://en.wikipedia.org/wiki/Blegny">Blegny&lt;/a> for the 27th Maasmarathon in &lt;a href="https://en.wikipedia.org/wiki/Vis%C3%A9">Visé&lt;/a>. Took the opportunity to walk over the recently opened &lt;em>&lt;a href="https://nl.wikipedia.org/wiki/Passerelle_van_Caestert">passerelle de Caster&lt;/a>&lt;/em> near &lt;a href="https://en.wikipedia.org/wiki/Kanne">Kanne&lt;/a>, a suspension bridge at 55 metres height over the Albert Canal.&lt;/p>
&lt;p>Took the 15-minute trip from Visé to Maastricht in the Netherlands with the &lt;a href="https://nl.wikipedia.org/wiki/Drielandentrein">Drielandentrein&lt;/a>. Visited a &lt;a href="https://www.visitmaastricht.com/en/locaties/30386516/boekhandel-dominicanen">bookshop in a church&lt;/a>, went to the &lt;em>&amp;ldquo;Porous Grounds, Sacred Codes&amp;rdquo;&lt;/em> exhibition at &lt;a href="https://www.visitmaastricht.com/en/locations/4083627566/marres-1">Marres, House for Contemporary Culture&lt;/a>, and stood at the &lt;a href="https://nl.wikipedia.org/wiki/Drielandenpunt_(Vaals)">Drielandenpunt&lt;/a> in Vaals where the borders of Belgium, the Netherlands, and Germany intersect.&lt;/p>
&lt;p>For the marathon itself, I finally managed to run one under four hours. Couldn&amp;rsquo;t have done it without the pacer! My previous chip times were:&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Race&lt;/th>
 &lt;th>Year&lt;/th>
 &lt;th>Chip time&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>Stockholm Marathon&lt;/strong>&lt;/td>
 &lt;td>2024&lt;/td>
 &lt;td>05:12:03&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>In Flanders Fields Marathon&lt;/strong>&lt;/td>
 &lt;td>2024&lt;/td>
 &lt;td>04:11:00&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>European Running Championships&lt;/strong>&lt;/td>
 &lt;td>2025&lt;/td>
 &lt;td>04:12:47&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>I&amp;rsquo;ll limit myself to one marathon per year. My next marathon would be the &lt;a href="https://www.erch2027.com/">European Running Championships 2027&lt;/a> in Belgrade.&lt;/p></description></item><item><title>Anagram hikes</title><link>http://patrick.vanderspie.gl/notes/2026-03-21/</link><pubDate>Sat, 21 Mar 2026 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/notes/2026-03-21/</guid><description>&lt;p>&lt;figure class="light">&lt;img src="http://patrick.vanderspie.gl/notes/20260321-anagram-hikes.gif"
 alt="Anagram Hikes">
&lt;/figure>

&lt;figure class="dark">&lt;img src="http://patrick.vanderspie.gl/notes/20260321-anagram-hikes-dark.gif"
 alt="Anagram Hikes">
&lt;/figure>
&lt;/p>
&lt;hr>
&lt;p>&lt;em>bpost&lt;/em> has a &lt;a href="https://www.bpost.be/nl/postcodevalidatie-tool">postal code validation tool&lt;/a>, providing a spreadsheet of postal codes with names of Belgian communes and their &lt;a href="https://en.wikipedia.org/wiki/Deelgemeente">&lt;em>deelgemeenten&lt;/em>&lt;/a>. We can convert the spreadsheet to a &lt;code>CSV&lt;/code> file using &lt;code>ssconvert&lt;/code>, and filter out all special postal codes&lt;label for="sidenote-2" class="sidenote-number">&lt;/label>&lt;input type="checkbox" id="sidenote-2" class="sidenote-toggle">&lt;span class="sidenote">&lt;a href="https://en.wikipedia.org/wiki/Sinterklaas">Sinterklaas&lt;/a> has his own postal code: 612, for 6th of December.&lt;/span> (which are not linked to a commune, but to large public institutions and broadcasters that get their own postal code&lt;label for="sidenote-3" class="sidenote-number">&lt;/label>&lt;input type="checkbox" id="sidenote-3" class="sidenote-toggle">&lt;span class="sidenote">&lt;a href="https://en.wikipedia.org/wiki/Postal_codes_in_Belgium">&lt;strong>Postal codes in Belgium&lt;/strong> &amp;ndash; &lt;em>Wikipedia&lt;/em>&lt;/a>&lt;/span>) with &lt;code>xan&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>ssconvert zipcodes_num_nl_2025.xls postal.csv
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>xan head postal.csv | xan view
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre tabindex="0">&lt;code>Displaying 5 cols from 10 rows of &amp;lt;stdin&amp;gt;
┌───┬──────────┬────────────────┬──────────────┬────────────────┬───────────┐
│ - │ Postcode │ Plaatsnaam │ Deelgemeente │ Hoofdgemeente │ Provincie │
├───┼──────────┼────────────────┼──────────────┼────────────────┼───────────┤
│ 0 │ 612 │ Sinterklaas │ &amp;lt;empty&amp;gt; │ Sinterklaas │ &amp;lt;empty&amp;gt; │
│ 1 │ 1000 │ Brussel │ Neen │ BRUSSEL │ BRUSSEL │
│ 2 │ 1005 │ Verenigde Ver… │ &amp;lt;empty&amp;gt; │ Verenigde Ver… │ &amp;lt;empty&amp;gt; │
│ 3 │ 1006 │ Raad van de V… │ &amp;lt;empty&amp;gt; │ Raad van de V… │ &amp;lt;empty&amp;gt; │
│ 4 │ 1007 │ Assemblée de … │ &amp;lt;empty&amp;gt; │ Assemblée de … │ &amp;lt;empty&amp;gt; │
│ 5 │ 1008 │ Kamer van Vol… │ &amp;lt;empty&amp;gt; │ Kamer van Vol… │ &amp;lt;empty&amp;gt; │
│ 6 │ 1009 │ Belgische Sen… │ &amp;lt;empty&amp;gt; │ Belgische Sen… │ &amp;lt;empty&amp;gt; │
│ 7 │ 1011 │ Vlaams Parlem… │ &amp;lt;empty&amp;gt; │ Vlaams Parlem… │ &amp;lt;empty&amp;gt; │
│ 8 │ 1012 │ Parlement de … │ &amp;lt;empty&amp;gt; │ Parlement de … │ &amp;lt;empty&amp;gt; │
│ 9 │ 1020 │ Laken │ Ja │ BRUSSEL │ BRUSSEL │
└───┴──────────┴────────────────┴──────────────┴────────────────┴───────────┘
&lt;/code>&lt;/pre>&lt;p>Filtering out these special cases:&lt;/p>
&lt;pre tabindex="0">&lt;code>Displaying 5 cols from 10 rows of &amp;lt;stdin&amp;gt;
┌───┬──────────┬─────────────────────┬───────────┬─────────────────────┬───────────┐
│ - │ Postcode │ Plaatsnaam │ Deelgeme… │ Hoofdgemeente │ Provincie │
├───┼──────────┼─────────────────────┼───────────┼─────────────────────┼───────────┤
│ 0 │ 1000 │ Brussel │ Neen │ BRUSSEL │ BRUSSEL │
│ 1 │ 1020 │ Laken │ Ja │ BRUSSEL │ BRUSSEL │
│ 2 │ 1030 │ Schaarbeek │ Neen │ SCHAARBEEK │ BRUSSEL │
│ 3 │ 1040 │ Etterbeek │ Neen │ ETTERBEEK │ BRUSSEL │
│ 4 │ 1050 │ Elsene │ Neen │ ELSENE │ BRUSSEL │
│ 5 │ 1060 │ Sint-Gillis │ Neen │ SINT-GILLIS │ BRUSSEL │
│ 6 │ 1070 │ Anderlecht │ Neen │ ANDERLECHT │ BRUSSEL │
│ 7 │ 1080 │ Sint-Jans-Molenbeek │ Neen │ SINT-JANS-MOLENBEEK │ BRUSSEL │
│ 8 │ 1081 │ Koekelberg │ Neen │ KOEKELBERG │ BRUSSEL │
│ 9 │ 1082 │ Sint-Agatha-Berchem │ Neen │ SINT-AGATHA-BERCHEM │ BRUSSEL │
└───┴──────────┴─────────────────────┴───────────┴─────────────────────┴───────────┘
&lt;/code>&lt;/pre>&lt;p>We are interested in both the names of &lt;em>hoofdgemeenten&lt;/em> and &lt;em>deelgemeenten&lt;/em> in lowercase. We put both of these in one column using &lt;code>xan unpivot 1,3&lt;/code>, which puts all values in a column named &lt;code>value&lt;/code>. We now have some duplicates in this column, which is why we also need &lt;code>dedup&lt;/code> before writing these to a &lt;code>towns.txt&lt;/code> file.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>xan filter &lt;span style="color:#666;font-style:italic">&amp;#34;!eq(Deelgemeente, &amp;#39;&amp;#39;)&amp;#34;&lt;/span> postal.csv &lt;span style="color:#666;font-style:italic">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">&lt;/span>	| xan transform 1,3 &lt;span style="color:#666;font-style:italic">&amp;#39;lower(_)&amp;#39;&lt;/span> &lt;span style="color:#666;font-style:italic">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">&lt;/span>	| xan unpivot 1,3 &lt;span style="color:#666;font-style:italic">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">&lt;/span>	| xan &lt;span style="font-weight:bold;text-decoration:underline">select&lt;/span> value &lt;span style="color:#666;font-style:italic">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">&lt;/span>	| xan dedup &amp;gt; towns.txt
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>We end up with a list of 2733 names:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>xan head towns.txt | xan view
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre tabindex="0">&lt;code>Displaying 1 col from 10 rows of &amp;lt;stdin&amp;gt;
┌───┬─────────────────────┐
│ - │ value │
├───┼─────────────────────┤
│ 0 │ brussel │
│ 1 │ laken │
│ 2 │ schaarbeek │
│ 3 │ etterbeek │
│ 4 │ elsene │
│ 5 │ sint-gillis │
│ 6 │ anderlecht │
│ 7 │ sint-jans-molenbeek │
│ 8 │ koekelberg │
│ 9 │ sint-agatha-berchem │
└───┴─────────────────────┘
&lt;/code>&lt;/pre>&lt;p>With the &lt;a href="http://patrick.vanderspie.gl/posts/scrambled-stations/#single-word-anagrams">alphabetic map&lt;/a> of these names, we get 18 anagram town pairs for which we plan hiking routes using the &lt;a href="https://openrouteservice.org">openrouteservice API&lt;/a>.&lt;/p>
&lt;pre tabindex="0">&lt;code>rosée ⟷ sorée
idegem ⟷ diegem
olen ⟷ olne
tiegem ⟷ itegem
seraing ⟷ ragnies
herent ⟷ herten
erpe ⟷ peer
laken ⟷ alken
meerbeke ⟷ meerbeek
mere ⟷ meer
mollem ⟷ lommel
herve ⟷ hever
virelles ⟷ serville
veulen ⟷ leuven
borsbeke ⟷ borsbeek
berloz ⟷ borlez
lembeke ⟷ lembeek
edingen ⟷ gedinne
&lt;/code>&lt;/pre>&lt;p>&lt;figure class="light">&lt;img src="http://patrick.vanderspie.gl/notes/20260321-anagram-hikes.png"
 alt="Anagram Hikes">
&lt;/figure>

&lt;figure class="dark">&lt;img src="http://patrick.vanderspie.gl/notes/20260321-anagram-hikes-dark.png"
 alt="Anagram Hikes">
&lt;/figure>
&lt;/p></description></item><item><title>&lt; 1000 kilometres in 2025</title><link>http://patrick.vanderspie.gl/notes/2026-01-07/</link><pubDate>Wed, 07 Jan 2026 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/notes/2026-01-07/</guid><description>
&lt;img src="http://patrick.vanderspie.gl/notes/24-25_distance-comparison.png" class="invert-dark" loading="lazy" />


&lt;p>See &lt;a href="http://patrick.vanderspie.gl/notes/2025-01-04">2024&lt;/a>; will post &lt;em>&amp;quot;&amp;gt; 1000 kilometres in 2026&amp;quot;&lt;/em> next year.&lt;/p></description></item><item><title>1000 kilometres in 2024</title><link>http://patrick.vanderspie.gl/notes/2025-01-04/</link><pubDate>Sat, 04 Jan 2025 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/notes/2025-01-04/</guid><description>
&lt;img src="http://patrick.vanderspie.gl/notes/2024-cumulative_distance_graph.png" class="invert-dark" loading="lazy" />


&lt;hr>
&lt;ul>
&lt;li>&lt;a href="https://intervals.icu">intervals.icu API&lt;/a> and &lt;a href="https://jqlang.github.io/jq/">&lt;code>jq&lt;/code>&lt;/a>:&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">BASE_URL&lt;/span>=&lt;span style="color:#666;font-style:italic">&amp;#34;https://intervals.icu/api/v1/athlete&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">OLDEST&lt;/span>=&lt;span style="color:#666;font-style:italic">&amp;#34;2024-01-01&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">NEWEST&lt;/span>=&lt;span style="color:#666;font-style:italic">&amp;#34;2024-12-31&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#888;font-style:italic"># Fetch activities&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">activities&lt;/span>=&lt;span style="font-weight:bold;text-decoration:underline">$(&lt;/span>curl -s -u API_KEY:&lt;span style="color:#666;font-weight:bold;font-style:italic">$API_KEY&lt;/span> &lt;span style="color:#666;font-style:italic">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$BASE_URL&lt;/span>&lt;span style="color:#666;font-style:italic">/&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$INTERVALS_UID&lt;/span>&lt;span style="color:#666;font-style:italic">/activities?oldest=&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$OLDEST&lt;/span>&lt;span style="color:#666;font-style:italic">&amp;amp;newest=&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$NEWEST&lt;/span>&lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#888;font-style:italic"># CSV header&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;font-style:italic">echo&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#34;date,cumulative_distance&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#888;font-style:italic"># Initialise cumulative distance and iterate through each date&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">cumulative_distance&lt;/span>=0
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">current_date&lt;/span>=&lt;span style="font-weight:bold;text-decoration:underline">$(&lt;/span>date -I -d &lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$OLDEST&lt;/span>&lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">end_date&lt;/span>=&lt;span style="font-weight:bold;text-decoration:underline">$(&lt;/span>date -I -d &lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$NEWEST&lt;/span>&lt;span style="color:#666;font-style:italic"> + 1 day&amp;#34;&lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">while&lt;/span> [ &lt;span style="color:#666;font-weight:bold;font-style:italic">$current_date&lt;/span> != &lt;span style="color:#666;font-weight:bold;font-style:italic">$end_date&lt;/span> ]; &lt;span style="font-weight:bold;text-decoration:underline">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#888;font-style:italic"># Calculate total distance for the current day&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#666;font-weight:bold;font-style:italic">day_distance&lt;/span>=&lt;span style="font-weight:bold;text-decoration:underline">$(&lt;/span>&lt;span style="font-weight:bold;font-style:italic">echo&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$activities&lt;/span>&lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span> | &lt;span style="color:#666;font-style:italic">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">&lt;/span> jq --arg date &lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$current_date&lt;/span>&lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic"> map(select(.type == &amp;#34;Run&amp;#34; and 
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic"> (.start_date_local | startswith($date)))) | 
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic"> map(.distance) | 
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic"> add // 0&amp;#39;&lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">)&lt;/span> &lt;span style="color:#888;font-style:italic"># return day distance (or return 0 if false or null)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#888;font-style:italic"># Add day distance to cumulative distance&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#666;font-weight:bold;font-style:italic">cumulative_distance&lt;/span>=&lt;span style="font-weight:bold;text-decoration:underline">$(&lt;/span>&lt;span style="font-weight:bold;font-style:italic">echo&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#34;[&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$cumulative_distance&lt;/span>&lt;span style="color:#666;font-style:italic">, &lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$day_distance&lt;/span>&lt;span style="color:#666;font-style:italic">]&amp;#34;&lt;/span> | jq &lt;span style="color:#666;font-style:italic">&amp;#39;add&amp;#39;&lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#888;font-style:italic"># Date and cumulative distance up until current date&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;font-style:italic">echo&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$current_date&lt;/span>&lt;span style="color:#666;font-style:italic">,&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$cumulative_distance&lt;/span>&lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#888;font-style:italic"># Move to the next day&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#666;font-weight:bold;font-style:italic">current_date&lt;/span>=&lt;span style="font-weight:bold;text-decoration:underline">$(&lt;/span>date -I -d &lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$current_date&lt;/span>&lt;span style="color:#666;font-style:italic"> + 1 day&amp;#34;&lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">done&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csv" data-lang="csv">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">date&lt;/span>,&lt;span style="color:#666;font-style:italic">cumulative_distance&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">2024-01-01&lt;/span>,&lt;span style="color:#666;font-style:italic">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">2024-01-02&lt;/span>,&lt;span style="color:#666;font-style:italic">6919&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">2024-01-03&lt;/span>,&lt;span style="color:#666;font-style:italic">6919&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">2024-01-04&lt;/span>,&lt;span style="color:#666;font-style:italic">10034&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">2024-01-05&lt;/span>,&lt;span style="color:#666;font-style:italic">10034&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">2024-12-27&lt;/span>,&lt;span style="color:#666;font-style:italic">958647.54&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">2024-12-28&lt;/span>,&lt;span style="color:#666;font-style:italic">973656.54&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">2024-12-29&lt;/span>,&lt;span style="color:#666;font-style:italic">973656.54&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">2024-12-30&lt;/span>,&lt;span style="color:#666;font-style:italic">980075.54&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">2024-12-31&lt;/span>,&lt;span style="color:#666;font-style:italic">1000088.54&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;ul>
&lt;li>&lt;a href="http://www.gnuplot.info/">gnuplot&lt;/a>:&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gnuplot" data-lang="gnuplot">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">set&lt;/span> &lt;span style="font-weight:bold;font-style:italic">terminal&lt;/span> pngcairo size 1024,768 font &lt;span style="color:#666;font-style:italic">&amp;#34;iA Writer Quattro V&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">set&lt;/span> &lt;span style="font-weight:bold;font-style:italic">output&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#39;cumulative_distance_graph.png&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="">&lt;/span>&lt;span style="color:#888;font-style:italic"># Set data file separator and time-related settings&lt;/span>&lt;span style="">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="">&lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">set&lt;/span> &lt;span style="font-weight:bold;font-style:italic">datafile&lt;/span> separator &lt;span style="color:#666;font-style:italic">&amp;#34;,&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">set&lt;/span> &lt;span style="font-weight:bold;font-style:italic">xdata&lt;/span> time
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">set&lt;/span> &lt;span style="font-weight:bold;font-style:italic">timefmt&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#34;%Y-%m-%d&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">set&lt;/span> &lt;span style="font-weight:bold;font-style:italic">format&lt;/span> x &lt;span style="color:#666;font-style:italic">&amp;#34;%B&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">set&lt;/span> &lt;span style="font-weight:bold;font-style:italic">ylabel&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#34;Cumulative Running Distance in 2024&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">set&lt;/span> &lt;span style="font-weight:bold;font-style:italic">grid&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="">&lt;/span>&lt;span style="color:#888;font-style:italic"># Customize axis and range&lt;/span>&lt;span style="">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="">&lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">set&lt;/span> &lt;span style="font-weight:bold;font-style:italic">xtics&lt;/span> rotate by -45
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">set&lt;/span> &lt;span style="font-weight:bold;font-style:italic">yrange&lt;/span> [0:1150]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">set&lt;/span> &lt;span style="font-weight:bold;font-style:italic">xrange&lt;/span> [&lt;span style="color:#666;font-style:italic">&amp;#39;2024-01-01&amp;#39;&lt;/span>:*]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="">&lt;/span>&lt;span style="color:#888;font-style:italic"># Set margins&lt;/span>&lt;span style="">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="">&lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">set&lt;/span> &lt;span style="font-weight:bold;font-style:italic">lmargin&lt;/span> 16
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">set&lt;/span> &lt;span style="font-weight:bold;font-style:italic">rmargin&lt;/span> 10
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">set&lt;/span> &lt;span style="font-weight:bold;font-style:italic">tmargin&lt;/span> 5
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">set&lt;/span> &lt;span style="font-weight:bold;font-style:italic">bmargin&lt;/span> 6
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="">&lt;/span>&lt;span style="color:#888;font-style:italic"># Mark specific races with arrows and labels&lt;/span>&lt;span style="">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="">&lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">set&lt;/span> &lt;span style="font-weight:bold;font-style:italic">arrow&lt;/span> from &lt;span style="color:#666;font-style:italic">&amp;#39;2024-06-01&amp;#39;&lt;/span>,0 to &lt;span style="color:#666;font-style:italic">&amp;#39;2024-06-01&amp;#39;&lt;/span>,1150 nohead lw 1 dashtype 3 linecolor &lt;span style="color:#666;font-style:italic">&amp;#34;gray30&amp;#34;&lt;/span> &lt;span style="color:#888;font-style:italic"># Stockholm marathon&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">set&lt;/span> &lt;span style="font-weight:bold;font-style:italic">label&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#34;🇸🇪&amp;#34;&lt;/span> at &lt;span style="color:#666;font-style:italic">&amp;#39;2024-06-03&amp;#39;&lt;/span>,950
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="">&lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">set&lt;/span> &lt;span style="font-weight:bold;font-style:italic">arrow&lt;/span> from &lt;span style="color:#666;font-style:italic">&amp;#39;2024-09-15&amp;#39;&lt;/span>,0 to &lt;span style="color:#666;font-style:italic">&amp;#39;2024-09-15&amp;#39;&lt;/span>,1150 nohead lw 1 dashtype 3 linecolor &lt;span style="color:#666;font-style:italic">&amp;#34;gray30&amp;#34;&lt;/span> &lt;span style="color:#888;font-style:italic"># In Flanders Fields marathon&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">set&lt;/span> &lt;span style="font-weight:bold;font-style:italic">label&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#34;🇧🇪&amp;#34;&lt;/span> at &lt;span style="color:#666;font-style:italic">&amp;#39;2024-09-17&amp;#39;&lt;/span>,950
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="">&lt;/span>&lt;span style="color:#888;font-style:italic"># Plot cumulative distance and 1000 km goal with markers and styles&lt;/span>&lt;span style="">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="">&lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">plot&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#34;cumulative_distance.csv&amp;#34;&lt;/span> &lt;span style="font-weight:bold;font-style:italic">using&lt;/span> 1:(&lt;span style="">$&lt;/span>2/1000) &lt;span style="font-weight:bold;font-style:italic">with&lt;/span> lines &lt;span style="font-weight:bold;font-style:italic">title&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#34;Cumulative Distance (km)&amp;#34;&lt;/span> lw 2 linecolor rgb &lt;span style="color:#666;font-style:italic">&amp;#34;orange-red&amp;#34;&lt;/span>, \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> 0 &lt;span style="font-weight:bold;font-style:italic">with&lt;/span> lines &lt;span style="font-weight:bold;font-style:italic">title&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#34;Marathons&amp;#34;&lt;/span> lw 1 dashtype 3 linecolor &lt;span style="color:#666;font-style:italic">&amp;#34;black&amp;#34;&lt;/span>, \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> 1000 &lt;span style="font-weight:bold;font-style:italic">with&lt;/span> lines &lt;span style="font-weight:bold;font-style:italic">notitle&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#34;1000 km&amp;#34;&lt;/span> lw 1 dashtype 2 linecolor &lt;span style="color:#666;font-style:italic">&amp;#34;black&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></description></item><item><title>Common Favourite Films on Letterboxd</title><link>http://patrick.vanderspie.gl/notes/2024-12-26/</link><pubDate>Thu, 26 Dec 2024 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/notes/2024-12-26/</guid><description>&lt;blockquote>
&lt;p>&lt;a href="https://letterboxd.com/pvdsp/">Letterboxd&lt;/a> allows users to list up to four favourite films, but there&amp;rsquo;s no built-in way to find people with similar tastes. To solve this, I developed a script to scrape &amp;lsquo;fans&amp;rsquo; of specific films and identify overlapping favourites.&lt;/p>&lt;/blockquote>
&lt;p>










&lt;figure class="">

 &lt;div>
 &lt;img loading="lazy" alt="My favourite films on Letterboxd" src=" /notes/20241225-favourites.png">
 &lt;/div>

 
 
 &lt;div class="caption-container">
 &lt;figcaption> My favourite films on Letterboxd &lt;/figcaption>
 &lt;/div>
 
 
&lt;/figure>&lt;/p>
&lt;hr>
&lt;h2 id="scraping-fans">Scraping Fans&lt;/h2>
&lt;p>As there is no public Letterboxd API&lt;label for="sidenote-0" class="sidenote-number">&lt;/label>&lt;input type="checkbox" id="sidenote-0" class="sidenote-toggle">&lt;span class="sidenote">I requested access to the Letterboxd API beta in 2016, but never received a response.&lt;/span> but I still want to find people with similar movie tastes,
I wrote a script that takes a Letterboxd movie identifier as an argument and scrapes the list of users who have listed this movie as one of their four favourites (also called &amp;ldquo;fans&amp;rdquo; on Letterboxd).
This script &lt;code>curl&lt;/code>s every fan page of the given movie, extracting user identifiers through a regex with &lt;code>grep&lt;/code>, and appending them to a &lt;code>CSV&lt;/code> file.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#888;font-weight:bold">#!/bin/sh
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#888;font-weight:bold">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">movie&lt;/span>=&lt;span style="color:#666;font-weight:bold;font-style:italic">$1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">page&lt;/span>=1
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">while&lt;/span> :; &lt;span style="font-weight:bold;text-decoration:underline">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#666;font-weight:bold;font-style:italic">fans&lt;/span>=&lt;span style="font-weight:bold;text-decoration:underline">$(&lt;/span>curl --silent &lt;span style="color:#666;font-style:italic">&amp;#34;https://letterboxd.com/film/&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$movie&lt;/span>&lt;span style="color:#666;font-style:italic">/fans/page/&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$page&lt;/span>&lt;span style="color:#666;font-style:italic">/&amp;#34;&lt;/span> | &lt;span style="color:#666;font-style:italic">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">&lt;/span> grep -Po &lt;span style="color:#666;font-style:italic">&amp;#39;(?&amp;lt;=a href=&amp;#34;/)[^&amp;#34;]+(?=/\&amp;#34; class=\&amp;#34;name\&amp;#34;)&amp;#39;&lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [ -z &lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$fans&lt;/span>&lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span> ] &amp;amp;&amp;amp; &lt;span style="font-weight:bold;font-style:italic">break&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;font-style:italic">echo&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$fans&lt;/span>&lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span> &amp;gt;&amp;gt; &lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="color:#666;font-style:italic">${&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">movie&lt;/span>&lt;span style="color:#666;font-style:italic">}&lt;/span>&lt;span style="color:#666;font-style:italic">_fans.csv&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#666;font-weight:bold;font-style:italic">page&lt;/span>=&lt;span style="font-weight:bold;text-decoration:underline">$((&lt;/span>page + 1&lt;span style="font-weight:bold;text-decoration:underline">))&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">done&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>As a test, we can look at the &lt;a href="https://letterboxd.com/film/the-innocents-2021/">&amp;ldquo;The Innocents&amp;rdquo;&lt;/a>, which currently has 88 fans.
&lt;code>&amp;quot;./letterboxdFans.sh the-innocents-2021&amp;quot;&lt;/code> produces a &lt;code>CSV&lt;/code> named &lt;code>&amp;quot;the-innocents-2021_fans.csv&amp;quot;&lt;/code> in the same folder.
Using the &lt;code>fish&lt;/code> shell, we check the number of entries by counting newlines in the &lt;code>CSV&lt;/code> with &lt;code>cat&lt;/code> and &lt;code>count&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ cat the-innocents-2021_fans.csv | count
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>88
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Looking at some popular films (&lt;em>like &lt;a href="https://letterboxd.com/film/parasite-2019/">Parasite&lt;/a>, for example&lt;/em>) shows that we can see at most 6.4k fans even though there are 102k of them in this specific case, however.
As there are 25 fans displayed per page, and we can only scrape 6.4k of them, this means that the fan pages are capped at page 256.
Manually confirming this indeed shows that there are at most &lt;a href="https://letterboxd.com/film/parasite-2019/fans/page/256/">256 pages&lt;/a> of alphabetically ordered fans per film.&lt;/p>
&lt;hr>
&lt;h2 id="finding-similar-profiles">Finding Similar Profiles&lt;/h2>
&lt;p>After scraping &lt;del>all&lt;/del> at most 6.4k fans of each of my four favourite films and putting the resulting &lt;code>CSV&lt;/code>s in a separate folder, we can find profiles that have at least two favourite films in common by looking for duplicate entries:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ ls letterboxd_fans/
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>good-time_fans.csv 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>paterson_fans.csv 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>perfect-days-2023_fans.csv 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>uncut-gems_fans.csv
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ cat letterboxd_fans/* | sort &amp;gt; merged.csv
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>$ uniq -c merged.csv | sort -nr | head -n 5
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> 2 mandypixel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> 2 malcolmconger
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> 2 louistoth
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> 2 loshmy
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> 2 ln42
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="automatically-following-profiles">Automatically Following Profiles&lt;/h2>
&lt;p>I attempted to automatically follow profiles that share at least two favourite films using &lt;a href="https://www.selenium.dev/">Selenium&lt;/a>, but only got as far as automatically visiting every profile with shared favourite films.&lt;/p>
&lt;p>Current issues include not being able to use an existing Firefox profile that is logged into my Letterboxd account to Selenium, and not being able to automatically click on the Follow button.&lt;/p>
&lt;p>As this is just a quick one-time project, I manually logged in using the browser that pops up after starting my script before automatically cycling through profiles from the &lt;code>merged.csv&lt;/code> list and manually clicking the follow button before the next profile page loads. This allowed me to quickly follow a couple of profiles with common favourite movies.&lt;/p></description></item><item><title>llm + Espanso: Clipboard Summaries</title><link>http://patrick.vanderspie.gl/notes/2024-09-09/</link><pubDate>Mon, 09 Sep 2024 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/notes/2024-09-09/</guid><description>&lt;p>I &lt;a href="http://patrick.vanderspie.gl/posts/obsidian-espanso-llm/">recently wrote an Espanso trigger&lt;/a> named &lt;code>:llm(⋯)&lt;/code> that pipes the contents of my clipboard to &lt;a href="https://github.com/simonw/llm">Simon Willison&amp;rsquo;s &lt;code>llm&lt;/code> CLI tool&lt;/a> and expands the trigger to &lt;code>llm&lt;/code>&amp;rsquo;s output in a custom Obsidian callout. This regex trigger expects a keyword (like &lt;code>summarise&lt;/code>, &lt;code>simplify&lt;/code>, or &lt;code>translate&lt;/code>) between its parenthesis, and transforms this keyword to a prompt that summarises, simplifies, or translates whatever text I current have within my clipboard.&lt;/p>
&lt;p>After frequent use of this trigger, I noticed that I mostly use &lt;code>:llm(summarise)&lt;/code>. To simplify my workflow, I decided to transfer summarisation to a dedicated &lt;code>:summary&lt;/code> trigger so that I don&amp;rsquo;t have to rely on providing and capturing keywords using regex:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>- trigger: &lt;span style="color:#666;font-style:italic">&amp;#34;:summary&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> replace: &lt;span style="color:#666;font-style:italic">&amp;#34;&amp;gt; [!llm] _\&amp;#34;{{title}}\&amp;#34;_\n&amp;gt; {{summary}}&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> vars:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - name: clipboard
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> type: clipboard
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - name: summary
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> type: shell
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> params:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cmd: &lt;span style="color:#666;font-style:italic">&amp;#34;echo $ESPANSO_CLIPBOARD | llm -m &amp;#39;gpt-4o&amp;#39; -s
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic"> \ &amp;#39;Summarise the provided text into a concise UK English paragraph. 
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic"> \ Ensure the summary captures essential information.&amp;#39;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - name: title
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> type: shell
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> params:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cmd: &lt;span style="color:#666;font-style:italic">&amp;#34;echo $ESPANSO_SUMMARY | llm -m &amp;#39;gpt-4o-mini&amp;#39; -s
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic"> \ &amp;#39;Generate a short one-sentence title that accurately reflects
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic"> \ the main idea of the summary. Use UK English.&amp;#39;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>










&lt;figure class="">

 &lt;div>
 &lt;img loading="lazy" alt="" src=" /notes/20240909-llm-espanso.gif">
 &lt;/div>

 
 
 
&lt;/figure>&lt;/p></description></item><item><title>Can GPT Boggle?</title><link>http://patrick.vanderspie.gl/notes/2024-08-27/</link><pubDate>Tue, 27 Aug 2024 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/notes/2024-08-27/</guid><description>&lt;p>During the programming and writing of &lt;a href="http://patrick.vanderspie.gl/posts/automating-boggle">&amp;ldquo;Automating Boggle&amp;rdquo;&lt;/a>, I remember being surprised that popular AI models (like GPT-4) — capable of many remarkable tasks — couldn&amp;rsquo;t do something so &amp;ldquo;trivial&amp;rdquo; as reading rotated dice. I regularly retry using the OpenAI API to recognise the labels on these dice, but the models always fail to do so (and they often fail spectacularly). The prompt I use is the following:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>&amp;#34;Given an image of a 4x4 grid of Boggle dice, extract and return the 16 letters from the grid, in a continuous string from top left to bottom right, left-to-right across each row. Account for any rotations of the letters. The output should be the correct sequence of letters as a continuous string, without any additional text, newlines, and formatting.&amp;#34;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>It often happens that I get more than 16 letters in return, most of them not even present on the board. If we look at the OpenAI API documentation, under the section of &lt;a href="https://platform.openai.com/docs/guides/vision/limitations">&lt;em>Limitations&lt;/em>&lt;/a>, we see:&lt;/p>
&lt;blockquote>
&lt;p>&lt;em>&lt;strong>Rotation:&lt;/strong> The model may misinterpret rotated / upside-down text or images.&lt;/em>&lt;/p>&lt;/blockquote>
&lt;p>Instead of manually calling the API once in a while to see if an updated version of a vision model can complete the task, I plan to write a GitHub Actions workflow that regularly calls the API and compares the output to the expected solution, printing the results on this website. Guess this will be my AI model benchmark: how long before these models can read rotated dice from an image and the instructions from the prompt above?&lt;/p></description></item><item><title>git mailmap</title><link>http://patrick.vanderspie.gl/notes/2024-08-26/</link><pubDate>Mon, 26 Aug 2024 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/notes/2024-08-26/</guid><description>&lt;p>I work on my Git repositories from different machines, which separates my contributions over different Git identities. Looking at &lt;a href="https://stackoverflow.com/a/9597462">all developers that contributed to one of my project&amp;rsquo;s repository&lt;/a>, we get the following:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>(base) pvdsp@laptop:~/git/⋯$ git shortlog -sne
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> 71 Patrick Van der Spiegel &amp;lt;pvdsp@⋯.be&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> 58 Patrick Van der Spiegel &amp;lt;patrick@⋯.com&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ⋯
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Adding a &lt;a href="https://git-scm.com/docs/gitmailmap">&lt;code>.mailmap&lt;/code> file&lt;/a> to the root directory of the repository — following the &lt;code>Proper Name &amp;lt;proper@email.xx&amp;gt; &amp;lt;commit@email.xx&amp;gt;&lt;/code> structure — fixes this issue:&lt;/p>
&lt;pre tabindex="0">&lt;code>Patrick Van der Spiegel &amp;lt;pvdsp@⋯.be&amp;gt; &amp;lt;patrick@⋯.com&amp;gt;
&lt;/code>&lt;/pre>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>(base) pvdsp@laptop:~/git/⋯$ git shortlog -sne
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> 129 Patrick Van der Spiegel &amp;lt;pvdsp@⋯.be&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></description></item><item><title>Joining Mastodon</title><link>http://patrick.vanderspie.gl/notes/2024-08-25/</link><pubDate>Sun, 25 Aug 2024 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/notes/2024-08-25/</guid><description>&lt;p>After years of lurking on Twitter — barely contributing to conversations on the platform, and only using it to read tweets from creators I follow — I am trying out Mastodon for a while. I planned to do this a while ago: most people I follow on Twitter also use or cross-post to Mastodon, and it makes sense to use non-commercial alternatives where possible.&lt;/p>
&lt;p>Reading blog posts through my RSS reader is excellent for keeping up with news related to my interests, but I sometimes miss some interaction and discussion on these posts. I hope to find this interaction through Mastodon, as not every blog supports Webmentions (and I do not yet have it fully implemented either).&lt;/p>
&lt;p>For now, I have:&lt;/p>
&lt;ul>
&lt;li>Manually searched and followed people of which I read their blogs&lt;/li>
&lt;li>Added a link to &lt;a href="https://c.im/@pvdsp">my Mastodon profile&lt;/a> on my main page (&lt;a href="https://symbol.fediverse.info/">including ⁂&lt;/a>)&lt;/li>
&lt;li>Verified my domain by adding &lt;code>&amp;lt;link href=&amp;quot;https://c.im/@pvdsp&amp;quot; rel=&amp;quot;me&amp;quot;&amp;gt;&lt;/code> to the &lt;code>&amp;lt;head&amp;gt;&lt;/code> of my pages&lt;/li>
&lt;/ul>
&lt;p>I looked into using &lt;a href="https://til.simonwillison.net/mastodon/custom-domain-mastodon">my custom domain for Mastodon&lt;/a> so that I could use &lt;code>@patrick@vanderspie.gl&lt;/code> as my username, but I will postpone this as it involves setting up your own Mastodon server or using a &lt;a href="https://masto.host/">managed host&lt;/a> (and I feel like using a custom domain is not worth yet another subscription).&lt;/p>
&lt;p>I&amp;rsquo;ve heard good things about Mastodon so far, so I look forward to seeing if it is a better Twitter alternative!&lt;/p></description></item><item><title>CSS filter posts by category</title><link>http://patrick.vanderspie.gl/notes/2024-08-24/</link><pubDate>Sat, 24 Aug 2024 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/notes/2024-08-24/</guid><description>&lt;p>As I also plan to upload non-technical posts and notes on my website, I added the ability to filter content based on categories. Trying to avoid another Javascript solution, I remembered the CSS &lt;code>:has&lt;/code> selector (which I read about in &lt;a href="https://ryanmulligan.dev/blog/we-can-has-it-all/">a blog post from Ryan Mulligan&amp;rsquo;s website&lt;/a>).&lt;/p>
&lt;p>I based my solution on his examples, tailoring it to the classes available in my Hugo theme. First, I added a partial with a &lt;code>fieldset&lt;/code> and included it in the &lt;code>HTML&lt;/code> template used for lists. For now, the categories are hard-coded, as I don&amp;rsquo;t plan on adding more in the future:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&amp;lt;fieldset&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;legend&amp;gt;Filter by category:&amp;lt;/legend&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;input type=&lt;span style="color:#666;font-style:italic">&amp;#34;radio&amp;#34;&lt;/span> id=&lt;span style="color:#666;font-style:italic">&amp;#34;all&amp;#34;&lt;/span> name=&lt;span style="color:#666;font-style:italic">&amp;#34;filter&amp;#34;&lt;/span> value=&lt;span style="color:#666;font-style:italic">&amp;#34;all&amp;#34;&lt;/span> checked /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label for=&lt;span style="color:#666;font-style:italic">&amp;#34;all&amp;#34;&lt;/span>&amp;gt;Show all&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;input type=&lt;span style="color:#666;font-style:italic">&amp;#34;radio&amp;#34;&lt;/span> id=&lt;span style="color:#666;font-style:italic">&amp;#34;tech&amp;#34;&lt;/span> name=&lt;span style="color:#666;font-style:italic">&amp;#34;filter&amp;#34;&lt;/span> value=&lt;span style="color:#666;font-style:italic">&amp;#34;tech&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label for=&lt;span style="color:#666;font-style:italic">&amp;#34;tech&amp;#34;&lt;/span>&amp;gt;Only technical&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;input type=&lt;span style="color:#666;font-style:italic">&amp;#34;radio&amp;#34;&lt;/span> id=&lt;span style="color:#666;font-style:italic">&amp;#34;nontech&amp;#34;&lt;/span> name=&lt;span style="color:#666;font-style:italic">&amp;#34;filter&amp;#34;&lt;/span> value=&lt;span style="color:#666;font-style:italic">&amp;#34;nontech&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label for=&lt;span style="color:#666;font-style:italic">&amp;#34;nontech&amp;#34;&lt;/span>&amp;gt;Only non-technical&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/fieldset&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Next, I added the following to my &lt;code>css&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-css" data-lang="css">&lt;span style="display:flex;">&lt;span>fieldset {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;text-decoration:underline">opacity&lt;/span>: 75&lt;span style="font-weight:bold;text-decoration:underline">%&lt;/span>; 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;text-decoration:underline">font-size&lt;/span>: 0.75&lt;span style="font-weight:bold;text-decoration:underline">em&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;text-decoration:underline">border&lt;/span>: &lt;span style="font-weight:bold;text-decoration:underline">dotted&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;text-decoration:underline">border-width&lt;/span>: 1&lt;span style="font-weight:bold;text-decoration:underline">px&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>body:has([name=&lt;span style="color:#666;font-style:italic">&amp;#34;filter&amp;#34;&lt;/span>][value=&lt;span style="color:#666;font-style:italic">&amp;#34;tech&amp;#34;&lt;/span>]:checked)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .&lt;span style="color:#666;font-weight:bold;font-style:italic">post-line&lt;/span>:not([category=&lt;span style="color:#666;font-style:italic">&amp;#34;tech&amp;#34;&lt;/span>]) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;text-decoration:underline">display&lt;/span>: &lt;span style="font-weight:bold;text-decoration:underline">none&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>body:has([name=&lt;span style="color:#666;font-style:italic">&amp;#34;filter&amp;#34;&lt;/span>][value=&lt;span style="color:#666;font-style:italic">&amp;#34;nontech&amp;#34;&lt;/span>]:checked)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .&lt;span style="color:#666;font-weight:bold;font-style:italic">post-line&lt;/span>:not([category=&lt;span style="color:#666;font-style:italic">&amp;#34;nontech&amp;#34;&lt;/span>]) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;text-decoration:underline">display&lt;/span>: &lt;span style="font-weight:bold;text-decoration:underline">none&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Finally, to make the filters work, I added a category to every post or note in Hugo:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-md" data-lang="md">&lt;span style="display:flex;">&lt;span>---
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>title: &amp;#34;CSS filter posts by category&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>date: &amp;#34;2024-08-24&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>⋯
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>category: &amp;#34;tech&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>---
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>and included this category in the partial for post entries:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&amp;lt;div class=&lt;span style="color:#666;font-style:italic">&amp;#34;post-line&amp;#34;&lt;/span> category=&lt;span style="color:#666;font-style:italic">&amp;#34;{{ .Params.category }}&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>	⋯
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></description></item><item><title>Python packaging in Rust</title><link>http://patrick.vanderspie.gl/notes/2024-08-23/</link><pubDate>Fri, 23 Aug 2024 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/notes/2024-08-23/</guid><description>&lt;p>Thanks to &lt;a href="https://simonwillison.net/2024/Aug/20/uv-unified-python-packaging/">a blog post by Simon Willison&lt;/a>, I learned about the open-source Python tooling by &lt;a href="https://astral.sh/">Astral&lt;/a>. Coincidentally, I started looking into the Rust programming language &lt;a href="http://patrick.vanderspie.gl/notes/2024-08-21/">a few days ago&lt;/a> and was pleasantly surprised by Cargo.&lt;/p>
&lt;p>&lt;code>uv&lt;/code> — which is a Python package and project manager written in Rust — is a kind of &lt;a href="https://astral.sh/blog/uv-unified-python-packaging">&amp;ldquo;Cargo, for Python&amp;rdquo;&lt;/a>. Looking at the &lt;a href="https://github.com/astral-sh/uv?tab=readme-ov-file#highlights">highlights&lt;/a> in &lt;code>uv&lt;/code>&amp;rsquo;s README.md file, the tool sounds promising, and I fully intend to try it out soon. The demonstrations on &lt;a href="https://astral.sh/blog/uv-unified-python-packaging">their blog post&lt;/a> are convincing, especially &lt;code>uvx&lt;/code> and &lt;code>uv run&lt;/code>:&lt;/p>
&lt;div class="quote-block">
 &lt;blockquote class="quote-content">With &lt;code>uv run&lt;/code>, you don&amp;rsquo;t have to think about activating virtual environments, managing dependencies, or keeping your project up-to-date. It just works.&lt;/blockquote>
&lt;/div>&lt;div class="quote-source-link">
 &lt;a href="https://astral.sh/blog/uv-unified-python-packaging" target="_blank" rel="noopener">Astral - uv: Unified Python packaging&lt;/a>
&lt;/div>
&lt;p>and, more importantly:&lt;/p>
&lt;div class="quote-block">
 &lt;blockquote class="quote-content">(⋯) it also adds features that are essential to local development but not covered by the standards, like relative paths and editable dependencies.&lt;/blockquote>
&lt;/div>&lt;div class="quote-source-link">
 &lt;a href="https://astral.sh/blog/uv-unified-python-packaging" target="_blank" rel="noopener">Astral - uv: Unified Python packaging&lt;/a>
&lt;/div>
&lt;p>Astral also provides a Python linter and code formatter written in Rust, named &lt;code>ruff&lt;/code>. I&amp;rsquo;m currently not using a dedicated linter; I&amp;rsquo;m just relying on my IDE and its built-in checks, but I might try adding &lt;code>ruff&lt;/code> to my workflow.&lt;/p></description></item><item><title>Tracking tasks in Obsidian</title><link>http://patrick.vanderspie.gl/notes/2024-08-22/</link><pubDate>Thu, 22 Aug 2024 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/notes/2024-08-22/</guid><description>&lt;p>A couple of days ago, I made a simple addition to my &lt;a href="http://patrick.vanderspie.gl/notes/2024-08-14/">Pomodoro timer&lt;/a>. To better keep track of work done in a given day, I edited my daily notes template of my Obsidian journal to include a table with a row for each pomodoro interval, explaining what I did during that interval:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-md" data-lang="md">&lt;span style="display:flex;">&lt;span>---
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>type: research journal
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>pomodori: 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>---
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span># Tasks of the day
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>| Hours | Task |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>| ----- | ----- |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span># Topics
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span># Ideas and Insights
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- 
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I now keep track of when a new interval starts, and write a new row after each interval ends. This row includes:&lt;/p>
&lt;ul>
&lt;li>The start and end time of the interval&lt;/li>
&lt;li>Space for a one-sentence summary of what I did during that interval&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">START_TIME&lt;/span>=&lt;span style="font-weight:bold;text-decoration:underline">$(&lt;/span>date +%H:%M&lt;span style="font-weight:bold;text-decoration:underline">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">ENTRY&lt;/span>=&lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$JOURNAL&lt;/span>&lt;span style="color:#666;font-style:italic">/&lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">$(&lt;/span>date +%Y-%m-%d&lt;span style="font-weight:bold;text-decoration:underline">)&lt;/span>&lt;span style="color:#666;font-style:italic"> Research Journal.md&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>⋯
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">NEW_ROW&lt;/span>=&lt;span style="color:#666;font-style:italic">&amp;#34;| &lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$START_TIME&lt;/span>&lt;span style="color:#666;font-style:italic"> — &lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">$(&lt;/span>date +%H:%M&lt;span style="font-weight:bold;text-decoration:underline">)&lt;/span>&lt;span style="color:#666;font-style:italic"> | ⋯ |&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sed -i &lt;span style="color:#666;font-style:italic">&amp;#34;s/# Topics/&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$NEW_ROW&lt;/span>&lt;span style="color:#666;font-style:italic">\n# Topics/&amp;#34;&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$ENTRY&lt;/span>&lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>A short and quick addition, but I&amp;rsquo;ve noticed that it helps me keep focused on one specific task during each 25-minute interval, for now. As an example:&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Hours&lt;/th>
 &lt;th>Task&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>09:02 — 09:27&lt;/td>
 &lt;td>Literature review on &lt;code>[[Complex Systems|DES]]&lt;/code> &lt;code>#phd&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>09:34 — 09:48&lt;/td>
 &lt;td>Reading &lt;code>[[🔴 (Zimmermann, 2008)]]&lt;/code> &lt;code>#phd&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>10:48 — 11:16&lt;/td>
 &lt;td>Reading paper draft &lt;code>#paper&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>⋯&lt;/td>
 &lt;td>⋯&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description></item><item><title>Learning Rust</title><link>http://patrick.vanderspie.gl/notes/2024-08-21/</link><pubDate>Wed, 21 Aug 2024 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/notes/2024-08-21/</guid><description>&lt;p>I started experimenting with the Rust programming language by reading &lt;a href="https://doc.rust-lang.org/book/">&amp;ldquo;the book&amp;rdquo;&lt;/a>, trying some basic things and loosely following along with the examples. Although I&amp;rsquo;ve only gone through the first three chapters — stopping after the common programming concepts and leaving the idea of ownership for later — I&amp;rsquo;m still planning to write down what I found interesting about the language compared to the ones I use more often (mainly Python and Julia).&lt;/p>
&lt;p>Nothing interesting will be written here for anyone familiar with Rust, but I&amp;rsquo;m growing fond of the idea behind &lt;a href="https://www.jvt.me/posts/2017/06/25/blogumentation/">&amp;ldquo;blogumentation&amp;rdquo;&lt;/a> for self-documentation. In this case, writing things down that I found interesting will help me remember them:&lt;/p>
&lt;h3 id="terminating-statements-with">&lt;strong>Terminating statements with &lt;code>;&lt;/code>&lt;/strong>&lt;/h3>
&lt;p>Like multiple other languages, Rust uses the semicolon after statements. I wondered why this is so often the convention, and found &lt;a href="https://ntietz.com/blog/researching-why-we-use-semicolons-as-statement-terminators/">a blog post by Nicole Tietz&lt;/a> speculating why that is the case.&lt;/p>
&lt;h3 id="cargo-and-initialising-git-repositories">Cargo and initialising Git repositories&lt;/h3>
&lt;p>Using Cargo — which also initialises a new Git repository by default and creates the project structure when you begin a new project — seems like a great workflow.&lt;/p>
&lt;div class="quote-block">
 &lt;blockquote class="quote-content">Cargo expects your source files to live inside the &lt;code>src&lt;/code> directory. The top-level project directory is just for README files, license information, configuration files, and anything else not related to your code. Using Cargo helps you organise your projects. There&amp;rsquo;s a place for everything, and everything is in its place.&lt;/blockquote>
&lt;/div>&lt;div class="quote-source-link">
 &lt;a href="https://doc.rust-lang.org/book/ch01-03-hello-cargo.html" target="_blank" rel="noopener">The Rust Programming Language&lt;/a>
&lt;/div>
&lt;p>In general, I think it&amp;rsquo;s helpful that it initialises and enforces this organisation. I tend to ignore conventions when quickly whipping something up in Python, which gets messy quickly.&lt;/p>
&lt;h3 id="shadowing-of-immutable-variables">Shadowing of immutable variables&lt;/h3>
&lt;p>Variables are immutable by default, but I was initially confused as to why you would allow shadowing for immutable variables. I did not immediately see any scenario where it would make sense to shadow an immutable variable. This will probably make more sense once I see some examples in practice. Another thing about shadowing that I found a bit weird is the example below:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-rust" data-lang="rust">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;font-style:italic;text-decoration:underline">let&lt;/span> spaces = &lt;span style="color:#666;font-style:italic">&amp;#34; &amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;font-style:italic;text-decoration:underline">let&lt;/span> spaces = spaces.len();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="quote-block">
 &lt;blockquote class="quote-content">The first spaces variable is a string type and the second spaces variable is a number type. Shadowing thus spares us from having to come up with different names, such as &lt;code>spaces_str&lt;/code> and &lt;code>spaces_num&lt;/code>; instead, we can reuse the simpler &lt;code>spaces&lt;/code> name.&lt;/blockquote>
&lt;/div>&lt;div class="quote-source-link">
 &lt;a href="https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html" target="_blank" rel="noopener">The Rust Programming Language&lt;/a>
&lt;/div>
&lt;p>This feels like a fast way for things to get messy, shadowing the same variable in different scopes but changing the type (and thus the meaning) of the variable, but keeping the name. &lt;code>spaces_str&lt;/code> and &lt;code>spaces_num&lt;/code> don&amp;rsquo;t sound so bad to me, as the names at least convey meaning.&lt;/p>
&lt;h3 id="visual-separator-for-integer-literals">Visual separator for integer literals&lt;/h3>
&lt;div class="quote-block">
 &lt;blockquote class="quote-content">Number literals can also use _ as a visual separator to make the number easier to read, such as 1_000, which will have the same value as if you had specified 1000.&lt;/blockquote>
&lt;/div>&lt;div class="quote-source-link">
 &lt;a href="https://doc.rust-lang.org/book/ch03-02-data-types.html" target="_blank" rel="noopener">The Rust Programming Language&lt;/a>
&lt;/div>
&lt;p>Through looking up other languages that provide a visual separator for integer literals, I stumbled upon &lt;a href="https://github.com/ziglang/zig/issues/504">this GitHub issue&lt;/a>, which contains some interesting discussion on separators.&lt;/p>
&lt;h3 id="other-things-that-i-like-so-far-at-first-glance">Other things that I like so far, at first glance&lt;/h3>
&lt;p>A lot of these might seem very standard, but as Python is my daily driver, it&amp;rsquo;s nice to try a language that has:&lt;/p>
&lt;ul>
&lt;li>Integer division truncating toward zero to the nearest integer:&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-rust" data-lang="rust">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;font-style:italic;text-decoration:underline">let&lt;/span> truncated = -5 / 3; &lt;span style="color:#888;font-style:italic">// Results in -1
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>The terminology &amp;ldquo;destructuring&amp;rdquo; for breaking up a single tuple into parts (&lt;em>which Python also has, but first time that I encounter this terminology&lt;/em>):&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-rust" data-lang="rust">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;font-style:italic;text-decoration:underline">let&lt;/span> tup = (500, 6.4, 1);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;font-style:italic;text-decoration:underline">let&lt;/span> (x, y, z) = tup;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>The distinction between arrays and vectors, and the initialisation of an array using this notation:&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-rust" data-lang="rust">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;font-style:italic;text-decoration:underline">let&lt;/span> a = [3; 5]; &lt;span style="color:#888;font-style:italic">// same as writing let a = [3, 3, 3, 3, 3];
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>The fact that you must declare the type of each parameter in function signatures:&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-rust" data-lang="rust">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">fn&lt;/span> &lt;span style="color:#666;font-weight:bold;font-style:italic">main&lt;/span>() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> print_labeled_measurement(5, &lt;span style="color:#666;font-style:italic">&amp;#39;h&amp;#39;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">fn&lt;/span> &lt;span style="color:#666;font-weight:bold;font-style:italic">print_labeled_measurement&lt;/span>(value: &lt;span style="font-weight:bold;text-decoration:underline">i32&lt;/span>, unit_label: &lt;span style="font-weight:bold;text-decoration:underline">char&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#666;font-weight:bold;font-style:italic">println!&lt;/span>(&lt;span style="color:#666;font-style:italic">&amp;#34;The measurement is: &lt;/span>&lt;span style="color:#666;font-style:italic">{value}{unit_label}&lt;/span>&lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>The distinction between &lt;em>acting&lt;/em> and &lt;em>returning&lt;/em> through statements and expressions (and that expressions don&amp;rsquo;t have ending semicolons). I already see myself making mistakes by turning expressions into statements — not letting it return a value — by accidentally adding a semicolon. Luckily, the thrown errors are very readable, so far:&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-rust" data-lang="rust">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#888;font-weight:bold">$&lt;/span> cargo run
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Compiling functions v0.1.0 (file:&lt;span style="color:#666;font-style:italic">///projects/functions)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">&lt;/span>error: &lt;span style="color:#666;font-weight:bold;font-style:italic">expected&lt;/span> expression, found &lt;span style="">`&lt;/span>&lt;span style="font-weight:bold;font-style:italic;text-decoration:underline">let&lt;/span>&lt;span style="">`&lt;/span> statement
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> --&amp;gt; &lt;span style="color:#666;font-weight:bold;font-style:italic">src&lt;/span>/main.rs:2:14
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>2 | &lt;span style="font-weight:bold;font-style:italic;text-decoration:underline">let&lt;/span> x = (&lt;span style="font-weight:bold;font-style:italic;text-decoration:underline">let&lt;/span> y = 6);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | ^^^
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> = note: &lt;span style="color:#666;font-weight:bold;font-style:italic">only&lt;/span> supported directly &lt;span style="font-weight:bold;text-decoration:underline">in&lt;/span> conditions of &lt;span style="">`&lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">if&lt;/span>&lt;span style="">`&lt;/span> and &lt;span style="">`&lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">while&lt;/span>&lt;span style="">`&lt;/span> expressions
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>The fact that conditions must explicitly be booleans:&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-rust" data-lang="rust">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#888;font-weight:bold">$&lt;/span> cargo run
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Compiling branches v0.1.0 (file:&lt;span style="color:#666;font-style:italic">///projects/branches)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">&lt;/span>error[E0308]: &lt;span style="color:#666;font-weight:bold;font-style:italic">mismatched&lt;/span> types
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> --&amp;gt; &lt;span style="color:#666;font-weight:bold;font-style:italic">src&lt;/span>/main.rs:4:8
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> |
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>4 | &lt;span style="font-weight:bold;text-decoration:underline">if&lt;/span> number {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> | ^^^^^^ expected &lt;span style="">`&lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">bool&lt;/span>&lt;span style="">`&lt;/span>, found integer
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>For more information about this error, &lt;span style="font-weight:bold;text-decoration:underline">try&lt;/span> &lt;span style="">`&lt;/span>rustc --explain E0308&lt;span style="">`&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>error: &lt;span style="color:#666;font-weight:bold;font-style:italic">could&lt;/span> not compile &lt;span style="">`&lt;/span>branches&lt;span style="">`&lt;/span> (bin &lt;span style="color:#666;font-style:italic">&amp;#34;branches&amp;#34;&lt;/span>) due to 1 previous error
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>One-line assignment using &lt;code>if&lt;/code>, but enforcing that the result of each arm must be of the same type:&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-rust" data-lang="rust">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;font-style:italic;text-decoration:underline">let&lt;/span> number = &lt;span style="font-weight:bold;text-decoration:underline">if&lt;/span> condition { 5 } &lt;span style="font-weight:bold;text-decoration:underline">else&lt;/span> { 6 };
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>The &lt;code>loop&lt;/code> keyword and loop labels (probably used sparingly as there are &lt;code>while&lt;/code> and &lt;code>for&lt;/code> keywords as well):&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-rust" data-lang="rust">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">fn&lt;/span> &lt;span style="color:#666;font-weight:bold;font-style:italic">main&lt;/span>() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;text-decoration:underline">loop&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#666;font-weight:bold;font-style:italic">println!&lt;/span>(&lt;span style="color:#666;font-style:italic">&amp;#34;again!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Looking forward to learning more about Rust!&lt;/p></description></item><item><title>Visualising Belgian roads</title><link>http://patrick.vanderspie.gl/notes/2024-08-20/</link><pubDate>Tue, 20 Aug 2024 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/notes/2024-08-20/</guid><description>&lt;p>&lt;figure class="dark">&lt;img src="http://patrick.vanderspie.gl/notes/belgium_roads_dark.png"
 alt="Visualisation of all roads in Belgium using city-roads">
&lt;/figure>

&lt;figure class="light">&lt;img src="http://patrick.vanderspie.gl/notes/belgium_roads_light.png"
 alt="Visualisation of all roads in Belgium using city-roads">
&lt;/figure>
&lt;/p>
&lt;p>When exploring &lt;a href="http://patrick.vanderspie.gl/notes/2024-08-10/">Overture data&lt;/a> earlier this month, I stumbled upon &lt;a href="https://benfry.com/allstreets/">&lt;em>&amp;ldquo;All Streets&amp;rdquo;&lt;/em> by Ben Fry&lt;/a>. I loved the visualisation and wondered how I could reproduce it for other countries. Looking at Ben Fry&amp;rsquo;s writing archive, he briefly discussed his work in a &lt;a href="https://benfry.com/writing/archives/54/">blog post&lt;/a> from 2008:&lt;/p>
&lt;div class="quote-block">
 &lt;blockquote class="quote-content">Nothing particularly genius about this piece—it&amp;rsquo;s mostly just a matter of collecting the data and creating the image. But it&amp;rsquo;s one of those cases where even in a (relatively) raw format, the data itself is quite striking.&lt;/blockquote>
&lt;/div>&lt;div class="quote-source-link">
 &lt;a href="https://benfry.com/writing/archives/54/" target="_blank" rel="noopener">Ben Fry&lt;/a>
&lt;/div>
&lt;p>&amp;ldquo;Collecting the data and creating the image&amp;rdquo;: as easy as &lt;a href="https://knowyourmeme.com/memes/how-to-draw-an-owl">drawing an owl&lt;/a>. Luckily, the rest of the blog post reveals a bit more of the process — but sadly doesn&amp;rsquo;t really help me in reproducing it for other countries.&lt;/p>
&lt;p>After a bit of Googling, I found great work by &lt;a href="https://github.com/anvaka">Andrei Kashcha&lt;/a>: &lt;a href="https://anvaka.github.io/city-roads/">&lt;code>city-roads&lt;/code>&lt;/a>, which allows you to render roads in any area using data from OpenStreetMap, which I used for the image at the top of this note. I plan to dive into &lt;a href="https://github.com/anvaka/city-roads">the open-sourced code&lt;/a> of &lt;code>city-roads&lt;/code>, and I might try to make something similar from scratch using Overture (just to learn how it works, as inspired by &lt;a href="https://lobste.rs/s/wimfh5/you_should_make_new_programming_language#c_zhzowj">a comment on lobste.rs&lt;/a>):&lt;/p>
&lt;div class="quote-block">
 &lt;blockquote class="quote-content">You should make a toy version of every major tool you use, if you can find the time and energy to do so. It&amp;rsquo;s a great way to understand the constraints that go into the tools you use and the limits of their capabilities.&lt;/blockquote>
&lt;/div>&lt;div class="quote-source-link">
 &lt;a href="https://lobste.rs/s/wimfh5/you_should_make_new_programming_language#c_zhzowj" target="_blank" rel="noopener">lobste.rs comment&lt;/a>
&lt;/div>
&lt;p>I want to do something similar as &lt;code>city-roads&lt;/code>, but expand it by combining it with &lt;a href="https://docs.overturemaps.org/guides/places/">Overture Places data&lt;/a>. Possibilities for visualisation include heatmaps for distance from road segments to specific types of places (like the nearest train station or bus stop, for example), or create a &lt;a href="https://www.chronotrains.com">Chronotrains&lt;/a>-style alternative on a Belgian map, using open data from NMBS, &lt;a href="https://data.delijn.be/">De Lijn&lt;/a>, and &lt;a href="https://www.letec.be/">TEC&lt;/a>.&lt;/p></description></item><item><title>Displaying tags in Hugo</title><link>http://patrick.vanderspie.gl/notes/2024-08-19/</link><pubDate>Mon, 19 Aug 2024 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/notes/2024-08-19/</guid><description>&lt;p>As I plan to write about a variety of different topics on this website, I decided to add tags to the properties of all my posts and notes so far:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-md" data-lang="md">&lt;span style="display:flex;">&lt;span>---
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>title: &amp;#34;Displaying tags in Hugo&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>date: &amp;#34;2024-08-19&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>tags: [&amp;#34;hugo&amp;#34;]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>⋯
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>---
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Tags were already available in the &lt;a href="https://github.com/tomfran/typo">Hugo theme&lt;/a> that I am currently using, but I did not like the way they were displayed. I edited &lt;code>partials/post-entry.html&lt;/code> so there&amp;rsquo;s a loop over every tag of a post, displaying them as a comma-delimited list next to the post or note&amp;rsquo;s title:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&amp;lt;div class=&lt;span style="color:#666;font-style:italic">&amp;#34;post-line&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {{ $dateFormat := &amp;#34;2 Jan 2006&amp;#34;}}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {{ with .Site.Params.listDateFormat }}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {{ $dateFormat = .}}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {{ end }}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p class=&lt;span style="color:#666;font-style:italic">&amp;#34;line-date&amp;#34;&lt;/span>&amp;gt;{{ .Date.Format $dateFormat }} &amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;span&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;a href=&lt;span style="color:#666;font-style:italic">&amp;#34;{{ .RelPermalink }}&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {{- .Title -}}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/a&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {{ if .Site.Params.listSummaries }}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;span class=&lt;span style="color:#666;font-style:italic">&amp;#34;post-tags&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {{- range $index, $tag := .Params.tags -}}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {{ if $index }}, {{ end }}&amp;lt;a href=&lt;span style="color:#666;font-style:italic">&amp;#34;{{ printf &amp;#34;&lt;/span>&lt;span style="">/&lt;/span>tags&lt;span style="">/%&lt;/span>s&lt;span style="">/&amp;#34;&lt;/span> &lt;span style="">$&lt;/span>tag &lt;span style="">|&lt;/span> urlize &lt;span style="">}}&amp;#34;&lt;/span>&amp;gt;#{{ $tag }}&amp;lt;/a&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {{- end -}}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/span&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#666;font-style:italic">&amp;#34;line-summary&amp;#34;&lt;/span>&amp;gt; {{ .Description | markdownify }} &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {{ end }}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/span&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-css" data-lang="css">&lt;span style="display:flex;">&lt;span>.&lt;span style="color:#666;font-weight:bold;font-style:italic">post-tags&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;text-decoration:underline">font-size&lt;/span>: 0.7&lt;span style="font-weight:bold;text-decoration:underline">em&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;text-decoration:underline">opacity&lt;/span>: 50&lt;span style="font-weight:bold;text-decoration:underline">%&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;text-decoration:underline">vertical-align&lt;/span>: &lt;span style="font-weight:bold;text-decoration:underline">baseline&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Clicking on the link of the tag brings you to a list of all posts or note&amp;rsquo;s with the same tag.&lt;/p></description></item><item><title>Using data sources in Hugo</title><link>http://patrick.vanderspie.gl/notes/2024-08-18/</link><pubDate>Sun, 18 Aug 2024 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/notes/2024-08-18/</guid><description>&lt;p>Until now, I have been storing different variables as site parameters in my Hugo website&amp;rsquo;s &lt;code>hugo.toml&lt;/code> file. A regularly triggered GitHub Actions workflow updates these variables by using various APIs to check for any changes. If changes are detected, the workflow updates the &lt;code>hugo.toml&lt;/code> file with the new values (replacing the old ones using &lt;code>sed&lt;/code>) and pushes the changes to my website’s GitHub repository. There, another GitHub Workflow builds my Hugo website and deploys it to GitHub Pages.&lt;/p>
&lt;p>As I start tracking more variables than originally planned, it makes sense to store them the proper way: using &lt;a href="https://gohugo.io/content-management/data-sources/">data sources&lt;/a> in Hugo. My plan is to keep a &lt;code>yaml&lt;/code> file in my Hugo website&amp;rsquo;s data directory, which will store all variables that I want to display on my website. The rest of the workflow should remain unchanged, but this approach should separate these variables from actual site parameters.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>running_distance: 720851.2999999999
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>running_cp: 3.5694862545565007
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>writing_streak: 12
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>best_writing_streak: 12
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>best_writing_streak_date: &lt;span style="color:#666;font-style:italic">&amp;#34;2024-08-19&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>tr_value: 6150.468645723648
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>tr_rank: b
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I&amp;rsquo;m planning to write a decent and complete &lt;a href="http://patrick.vanderspie.gl/posts/">post&lt;/a> explaining which APIs I use, the variables that I store (and plan to store in the future), and how these GitHub Workflows work. I currently keep track of the following variables:&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Variable&lt;/th>
 &lt;th>Value&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Longest writing streak&lt;/td>
 &lt;td>




20 days | Achieved on Aug 27, 2024&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Current writing streak&lt;/td>
 &lt;td>



0 days | Updated on May 10, 2026&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Running Critical Power&lt;/td>
 &lt;td>3.490 W/kg&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Distance ran this year&lt;/td>
 &lt;td>418.2 km&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>TETR.IO&amp;rsquo;s TETRA LEAGUE&lt;/td>
 &lt;td>&lt;a href="https://ch.tetr.io/u/geuze/league">&lt;span>11126.88 TR (&lt;img class="tr_rank" src="https://tetr.io/res/league-ranks/a.png"/>)&lt;/span>&lt;/a>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description></item><item><title>Displaying TETR.IO TR on website</title><link>http://patrick.vanderspie.gl/notes/2024-08-17/</link><pubDate>Sat, 17 Aug 2024 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/notes/2024-08-17/</guid><description>&lt;p>TETR.IO&amp;rsquo;s started its new season yesterday. &lt;a href="http://patrick.vanderspie.gl/notes/2024-08-13/">A couple of days ago, I planned to scrape my TETR.IO profile to display the change in my TR over time&lt;/a>. The game&amp;rsquo;s &lt;a href="https://tetr.io/about/patchnotes/#chlog_BETA_1_2_0">patch notes&lt;/a> include the feature that I was missing in the game&amp;rsquo;s previous season, however:&lt;/p>
&lt;div class="quote-block">
 &lt;blockquote class="quote-content">You can now see your TR change over time on the TETRA CHANNEL site.&lt;/blockquote>
&lt;/div>&lt;div class="quote-source-link">
 &lt;a href="https://tetr.io/about/patchnotes/#chlog_BETA_1_2_0" target="_blank" rel="noopener">TETR.IO Patch Notes&lt;/a>
&lt;/div>
&lt;p>Additionally, if I still want to include my TR over time on my website, scraping isn&amp;rsquo;t necessary at all: the patch notes mention that the game &lt;a href="https://tetr.io/about/api/">provides an API&lt;/a>. Simply using &lt;code>curl&lt;/code> and &lt;code>jq&lt;/code> gives me all the data I need:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>curl https://ch.tetr.io/api/users/geuze/summaries/league | jq &lt;span style="color:#666;font-style:italic">&amp;#39;.data | .tr&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I store this data as a site parameter in my &lt;code>hugo.toml&lt;/code> file:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>TETRIO_TR = 3090.2731291544947
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>TETRIO_RANK = &amp;#34;c+&amp;#34;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now, I can use it in a Hugo shortcode:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&amp;lt;span&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>	{{ printf &amp;#34;%.2f&amp;#34; .Site.Params.TETRIO_TR }} TR
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>	(&amp;lt;img class=&lt;span style="color:#666;font-style:italic">&amp;#34;tr_rank&amp;#34;&lt;/span> 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>		 src=&lt;span style="color:#666;font-style:italic">&amp;#34;https://tetr.io/res/league-ranks/{{ .Site.Params.TETRIO_RANK }}.png&amp;#34;&lt;/span> /&amp;gt;)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/span&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Which looks like this: &lt;strong>&lt;a href="https://ch.tetr.io/u/geuze/league">&lt;span>11126.88 TR (&lt;img class="tr_rank" src="https://tetr.io/res/league-ranks/a.png"/>)&lt;/span>&lt;/a>&lt;/strong>.
To keep this value up-to-date, I add it to a GitHub Actions workflow that uses the game&amp;rsquo;s API to retrieve my TR and rank, and replaces the current value in the &lt;code>hugo.toml&lt;/code> file using &lt;code>sed&lt;/code>.&lt;/p></description></item><item><title>Visualising multi-sport events</title><link>http://patrick.vanderspie.gl/notes/2024-08-16/</link><pubDate>Fri, 16 Aug 2024 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/notes/2024-08-16/</guid><description>&lt;p>Today, I participated in my first duathlon — 5 kilometres of running, followed by 32 kilometres of cycling, and finishing with another 5 kilometres of running:&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>&lt;/th>
 &lt;th>&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>Run #1&lt;/strong>&lt;/td>
 &lt;td>&lt;strong>00:24:54&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>⋮ &lt;em>Transition #1&lt;/em>&lt;/td>
 &lt;td>&lt;em>00:01:06&lt;/em>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Bike&lt;/strong>&lt;/td>
 &lt;td>&lt;strong>01:21:49&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>⋮ &lt;em>Transition #2&lt;/em>&lt;/td>
 &lt;td>&lt;em>00:01:53&lt;/em>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Run #2&lt;/strong>&lt;/td>
 &lt;td>&lt;strong>00:23:44&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>⋮ &lt;em>Total&lt;/em>&lt;/td>
 &lt;td>&lt;em>02:13:28&lt;/em>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>As I do with my separate running or cycling sessions, I record my performance (&lt;em>HR, power output, pace&lt;/em>) and upload it to Strava and &lt;a href="https://intervals.icu">intervals.icu&lt;/a>. &lt;strong>It surprises me, however, that there is no clear way of combining these separate activities into one multi-sport event.&lt;/strong> I assume there&amp;rsquo;s much to gain when looking at these activities in their context, rather than separately.&lt;/p>
&lt;p>As a small side project, I could look into the best way of combining the different &lt;code>.fit&lt;/code>-files into one well-visualised multi-sport event. It would surprise me if no one else has done this before, but this idea gives me the best opportunity to dive into how these files are structured (&lt;em>as I am totally unfamiliar with how they work&lt;/em>) and how I could combine them in one multi-sport visualisation.&lt;/p>
&lt;p>I could use the data from different &lt;code>.fit&lt;/code>-files to display some maps of the course and compare the first and last running segments of a duathlon, for example. I could show a well-visualised race timeline with interesting annotations like weather conditions and elevation profiles, and the location of the highest achieved heart rate and power output. For friend groups participating in the same event, I could show different participants&amp;rsquo; data on the same visualisation, to easily let them compare their performance.&lt;/p></description></item><item><title>Target power to predicted race time</title><link>http://patrick.vanderspie.gl/notes/2024-08-15/</link><pubDate>Thu, 15 Aug 2024 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/notes/2024-08-15/</guid><description>&lt;p>For both training and racing, I use a &lt;a href="https://www.stryd.com/eu/en">Stryd pod&lt;/a>: a running power meter that allows me to base my running on my power output, instead of looking at my pace or heart rate (which are both influenced by elevation). The Stryd running pod calculates my &lt;a href="https://help.stryd.com/en/articles/6879345-critical-power-definition">Critical Power (CP)&lt;/a>, which is similar to Functional Threshold Power (FTP). My CP is currently &lt;span class="tooltip">3.490 W/kg&lt;span class="tooltiptext">Data from intervals.icu&lt;/span>&lt;/span> (and was 3.75 W/kg at the time of writing) — something that I want to keep improving as I&amp;rsquo;m planning to run my second marathon mid-September.&lt;/p>
&lt;h3 id="predicting-finish-time-with-stryd">Predicting finish time with Stryd&lt;/h3>
&lt;p>The Stryd app has two handy features: one predicts race calculations for a set of fixed distances (&lt;em>1 mile, 5k, 10k, HM, and full marathon&lt;/em>) and the other returns a target power + predicted race time based on CP for a given &lt;code>GPX&lt;/code>. I was wondering how they calculate these predicted race times, and stumbled upon &lt;a href="https://blog.stryd.com/2020/01/10/how-to-calculate-your-race-time-from-your-target-power/">a blog post&lt;/a> by Stryd themselves.&lt;/p>
&lt;p>Their simplified formula — specifically meant for a flat course with no air resistance — is rather straightforward: $T = \frac{1.04 * d}{TP / m}$, where $T$ is the predicted race time in seconds, $TP$ is the target power for the race in watts, $m$ is the runner&amp;rsquo;s body weight in kilograms, and $d$ is the distance in meters:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">from&lt;/span> &lt;span style="color:#666;font-weight:bold;font-style:italic">datetime&lt;/span> &lt;span style="font-weight:bold;text-decoration:underline">import&lt;/span> timedelta
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">def&lt;/span> &lt;span style="color:#666;font-weight:bold;font-style:italic">predicted_time&lt;/span>(d, TP, m):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>	seconds = 1.04 * d / (TP / m)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>	&lt;span style="font-weight:bold;text-decoration:underline">return&lt;/span> &lt;span style="font-weight:bold;font-style:italic">str&lt;/span>(timedelta(seconds=seconds))
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="in-flanders-fields-marathon-prediction">&amp;ldquo;In Flanders Fields&amp;rdquo; marathon prediction&lt;/h3>
&lt;p>For &lt;a href="https://marathons.be/over-iff/">my next marathon&lt;/a>, I am trying to go for a time under 4 hours. As my target power is ~250 W, I would finish my next marathon in &lt;strong>3 hours and 51 minutes&lt;/strong>, in the &lt;span class="tooltip">best case scenario&lt;span class="tooltiptext">Flat course, but weather can have a big impact&lt;/span>&lt;/span>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>d = 42195 &lt;span style="color:#888;font-style:italic"># marathon distance in m&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>TP = 250 &lt;span style="color:#888;font-style:italic"># target power in W&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>m = 79 &lt;span style="color:#888;font-style:italic"># weight in kg&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;gt;&amp;gt;&amp;gt; predicted_time(d, TP, m)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">&amp;#39;3:51:06.964800&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This is pretty similar to the Stryd app&amp;rsquo;s estimated time based on the provided &lt;code>GPX&lt;/code> of the marathon course, which predicts I will run the marathon in around &lt;strong>3 hours and 50 minutes&lt;/strong> — if I can maintain an average power output of ~252 W. Using the simplified formula directly, however, gives me a bit more flexibility to see what my predicted time would be if I increase my target power or lose weight, but retain my CP:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>weight = &lt;span style="font-weight:bold;font-style:italic">range&lt;/span>(80, 74, -1)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>time = [predicted_time(d, TP, w) &lt;span style="font-weight:bold;text-decoration:underline">for&lt;/span> w &lt;span style="font-weight:bold">in&lt;/span> weight]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;gt;&amp;gt;&amp;gt; &lt;span style="font-weight:bold;text-decoration:underline">for&lt;/span> w, t &lt;span style="font-weight:bold">in&lt;/span> &lt;span style="font-weight:bold;font-style:italic">zip&lt;/span>(weight, time):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>... &lt;span style="font-weight:bold;font-style:italic">print&lt;/span>(&lt;span style="color:#666;font-style:italic">f&lt;/span>&lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="color:#666;font-style:italic">{&lt;/span>w&lt;span style="color:#666;font-style:italic">}&lt;/span>&lt;span style="color:#666;font-style:italic"> kg — &lt;/span>&lt;span style="color:#666;font-style:italic">{&lt;/span>t&lt;span style="color:#666;font-style:italic">}&lt;/span>&lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">&amp;#39;80 kg — 3:54:02.496000&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">&amp;#39;79 kg — 3:51:06.964800&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">&amp;#39;78 kg — 3:48:11.433600&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">&amp;#39;77 kg — 3:45:15.902400&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">&amp;#39;76 kg — 3:42:20.371200&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">&amp;#39;75 kg — 3:39:24.840000&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Stryd also just released &lt;a href="https://blog.stryd.com/2024/08/14/clayton-youngs-9th-place-finish-at-the-olympic-marathon/">a blog post&lt;/a> about Clayton Young and his preparation for the Olympic Marathon in Paris, in which he finished 9th: Stryd predicted his finish time at 2:09:54, and he ultimately ran 2:08:44.&lt;/p></description></item><item><title>Pomodoro timer in a terminal</title><link>http://patrick.vanderspie.gl/notes/2024-08-14/</link><pubDate>Wed, 14 Aug 2024 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/notes/2024-08-14/</guid><description>&lt;hr>
&lt;p>I usually use the &lt;a href="https://en.wikipedia.org/wiki/Pomodoro_Technique">Pomodoro Technique&lt;/a> to schedule my work: intervals of 25 minutes of work, followed by a 5 minute break. Traditionally, I used &lt;a href="https://pomofocus.io/">pomofocus.io&lt;/a> as a timer in my browser that alerts me when an interval is over. As I often have a terminal emulator opened somewhere — and as I wanted to keep track of the number of finished &lt;em>pomodori&lt;/em> in Obsidian, it made more sense to create my own shell script that:&lt;/p>
&lt;ul>
&lt;li>Starts a 25 minute timer, alerting me when time runs out&lt;/li>
&lt;li>Gives me the option to take a break, starting a 5 minute timer&lt;/li>
&lt;li>Keeps alternating between work and break intervals until I stop working&lt;/li>
&lt;li>Keeps track of finished pomodori for the day in my Obsidian work vault&lt;/li>
&lt;/ul>
&lt;h2 id="displaying-a-timer">Displaying a timer&lt;/h2>
&lt;p>Below, we define a function that takes a label (&lt;em>e.g., working or taking a break&lt;/em>) and the amount of seconds the countdown should take. We start by moving the text cursor to the upper left corner using &lt;code>echo -ne '\033[2A'&lt;/code>&lt;label for="sidenote-0" class="sidenote-number">&lt;/label>&lt;input type="checkbox" id="sidenote-0" class="sidenote-toggle">&lt;span class="sidenote">&lt;a href="https://stackoverflow.com/a/72667369">StackOverflow&lt;/a>&lt;/span>, and then, as long as our variable &lt;code>time_left&lt;/code> is larger than -1:&lt;/p>
&lt;ul>
&lt;li>Show the label in bold, using &lt;code>\033[1m• $label\033[0m&lt;/code>&lt;/li>
&lt;li>Format remaining time using &lt;code>$(date -d@$time_left -u +%M:%S)&lt;/code>, so that it shows as remaining time in minutes and seconds, instead of just seconds&lt;/li>
&lt;li>Use &lt;code>\033[0K&lt;/code> to delete text to the end of the line, and &lt;code>\r&lt;/code> to move to the beginning of the current line&lt;label for="sidenote-1" class="sidenote-number">&lt;/label>&lt;input type="checkbox" id="sidenote-1" class="sidenote-toggle">&lt;span class="sidenote">&lt;a href="https://stackoverflow.com/a/5861713">StackOverflow&lt;/a>&lt;/span>&lt;/li>
&lt;li>Wait one second using &lt;code>sleep&lt;/code>, and substract 1 from the &lt;code>time_left&lt;/code> variable&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>display_timer() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;font-style:italic">local&lt;/span> &lt;span style="color:#666;font-weight:bold;font-style:italic">label&lt;/span>=&lt;span style="color:#666;font-weight:bold;font-style:italic">$1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;font-style:italic">local&lt;/span> &lt;span style="color:#666;font-weight:bold;font-style:italic">time_left&lt;/span>=&lt;span style="color:#666;font-weight:bold;font-style:italic">$2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;font-style:italic">echo&lt;/span> -ne &lt;span style="color:#666;font-style:italic">&amp;#39;\033[2A&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;text-decoration:underline">while&lt;/span> [ &lt;span style="color:#666;font-weight:bold;font-style:italic">$time_left&lt;/span> -gt -1 ]; &lt;span style="font-weight:bold;text-decoration:underline">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;font-style:italic">echo&lt;/span> -ne &lt;span style="color:#666;font-style:italic">&amp;#34;\033[1m• &lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$label&lt;/span>&lt;span style="color:#666;font-style:italic">\033[0m — &lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">$(&lt;/span>date -d@&lt;span style="color:#666;font-weight:bold;font-style:italic">$time_left&lt;/span> -u +%M:%S&lt;span style="font-weight:bold;text-decoration:underline">)&lt;/span>&lt;span style="color:#666;font-style:italic">\033[0K\r&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sleep 1
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ((time_left--))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;text-decoration:underline">done&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>We can start and display our work timer and break timer as shown below. We start a 25 minute timer (displayed as &lt;code>• Work left — MM:SS&lt;/code>), immediately followed by a 5 minute timer (displayed as &lt;code>• Break left — MM:SS&lt;/code>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">WORK_TIME&lt;/span>=&lt;span style="font-weight:bold;text-decoration:underline">$((&lt;/span>60 * 25&lt;span style="font-weight:bold;text-decoration:underline">))&lt;/span> &lt;span style="color:#888;font-style:italic"># Work time (25 minutes)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">BREAK_TIME&lt;/span>=&lt;span style="font-weight:bold;text-decoration:underline">$((&lt;/span>60 * 5&lt;span style="font-weight:bold;text-decoration:underline">))&lt;/span> &lt;span style="color:#888;font-style:italic"># Break time (5 minutes)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>display_timer &lt;span style="color:#666;font-style:italic">&amp;#34;Work left&amp;#34;&lt;/span> &lt;span style="color:#666;font-weight:bold;font-style:italic">$WORK_TIME&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>display_timer &lt;span style="color:#666;font-style:italic">&amp;#34;Break left&amp;#34;&lt;/span> &lt;span style="color:#666;font-weight:bold;font-style:italic">$BREAK_TIME&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="keeping-track-of-s">Keeping track of 🍅&amp;rsquo;s&lt;/h2>
&lt;p>In my Obsidian journal, I have daily notes where I keep track of work done, to-dos, and ideas. I recently added a &lt;code>pomodori&lt;/code> field to the properties of my Markdown files, where I keep track of how many work intervals I have done that day:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-md" data-lang="md">&lt;span style="display:flex;">&lt;span>---
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>date: 2024-08-14
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>type: research journal
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>pomodori: 🍅🍅🍅🍅
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>---
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I want this script to automatically add a new interval to this property, by first defining the path to today&amp;rsquo;s note and then appending a 🍅 to the end of the line of that property using &lt;code>sed&lt;/code>. As the property will always be on the fourth line of my notes, we can write:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">WORK_TIME&lt;/span>=&lt;span style="font-weight:bold;text-decoration:underline">$((&lt;/span>60 * 25&lt;span style="font-weight:bold;text-decoration:underline">))&lt;/span> &lt;span style="color:#888;font-style:italic"># Work time (25 minutes)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">BREAK_TIME&lt;/span>=&lt;span style="font-weight:bold;text-decoration:underline">$((&lt;/span>60 * 5&lt;span style="font-weight:bold;text-decoration:underline">))&lt;/span> &lt;span style="color:#888;font-style:italic"># Break time (5 minutes)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">JOURNAL&lt;/span>=&lt;span style="color:#666;font-style:italic">&amp;#34;(⋯)/work/Research Journal&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">ENTRY&lt;/span>=&lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$JOURNAL&lt;/span>&lt;span style="color:#666;font-style:italic">/&lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">$(&lt;/span>date +%Y-%m-%d&lt;span style="font-weight:bold;text-decoration:underline">)&lt;/span>&lt;span style="color:#666;font-style:italic"> Research Journal.md&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>display_timer &lt;span style="color:#666;font-style:italic">&amp;#34;Work left&amp;#34;&lt;/span> &lt;span style="color:#666;font-weight:bold;font-style:italic">$WORK_TIME&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sed -i &lt;span style="color:#666;font-style:italic">&amp;#39;4s/$/🍅/&amp;#39;&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$ENTRY&lt;/span>&lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>display_timer &lt;span style="color:#666;font-style:italic">&amp;#34;Break left&amp;#34;&lt;/span> &lt;span style="color:#666;font-weight:bold;font-style:italic">$BREAK_TIME&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="user-input-to-keep-going">User input to keep going&lt;/h2>
&lt;p>Using &lt;code>zenity&lt;/code>, we can request user input to check if we want to switch from working to taking a break, and vice versa. We use &lt;code>exec &amp;quot;$(readlink -f &amp;quot;$0&amp;quot;)&amp;quot;&lt;/code>&lt;label for="sidenote-2" class="sidenote-number">&lt;/label>&lt;input type="checkbox" id="sidenote-2" class="sidenote-toggle">&lt;span class="sidenote">&lt;a href="https://askubuntu.com/questions/356800">AskUbuntu&lt;/a>&lt;/span> to restart the script when switching from taking a break back to working:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">if&lt;/span> zenity --question --title=&lt;span style="color:#666;font-style:italic">&amp;#34;Pomodoro&amp;#34;&lt;/span> --text=&lt;span style="color:#666;font-style:italic">&amp;#34;Time for a break?&amp;#34;&lt;/span>; &lt;span style="font-weight:bold;text-decoration:underline">then&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> display_timer &lt;span style="color:#666;font-style:italic">&amp;#34;Break left&amp;#34;&lt;/span> &lt;span style="color:#666;font-weight:bold;font-style:italic">$BREAK_TIME&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;text-decoration:underline">if&lt;/span> zenity --question --title=&lt;span style="color:#666;font-style:italic">&amp;#34;Pomodoro&amp;#34;&lt;/span> --text=&lt;span style="color:#666;font-style:italic">&amp;#34;Time to work?&amp;#34;&lt;/span>; &lt;span style="font-weight:bold;text-decoration:underline">then&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;font-style:italic">exec&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">$(&lt;/span>readlink -f &lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$0&lt;/span>&lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">)&lt;/span>&lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span> &lt;span style="color:#888;font-style:italic"># Restart the script for another Pomodoro cycle&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;text-decoration:underline">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">fi&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="putting-it-all-together">Putting it all together&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#888;font-weight:bold">#!/usr/bin/env bash
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#888;font-weight:bold">&lt;/span>clear
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#888;font-style:italic"># Paths and file names&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">JOURNAL&lt;/span>=&lt;span style="color:#666;font-style:italic">&amp;#34;(⋯)/work/Research Journal&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">ENTRY&lt;/span>=&lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$JOURNAL&lt;/span>&lt;span style="color:#666;font-style:italic">/&lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">$(&lt;/span>date +%Y-%m-%d&lt;span style="font-weight:bold;text-decoration:underline">)&lt;/span>&lt;span style="color:#666;font-style:italic"> Research Journal.md&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#888;font-style:italic"># Pomodoro timers (in seconds)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">WORK_TIME&lt;/span>=&lt;span style="font-weight:bold;text-decoration:underline">$((&lt;/span>60 * 25&lt;span style="font-weight:bold;text-decoration:underline">))&lt;/span> &lt;span style="color:#888;font-style:italic"># Work time (25 minutes)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">BREAK_TIME&lt;/span>=&lt;span style="font-weight:bold;text-decoration:underline">$((&lt;/span>60 * 5&lt;span style="font-weight:bold;text-decoration:underline">))&lt;/span> &lt;span style="color:#888;font-style:italic"># Break time (5 minutes)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#888;font-style:italic"># Function to display the timer&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>display_timer() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;font-style:italic">local&lt;/span> &lt;span style="color:#666;font-weight:bold;font-style:italic">label&lt;/span>=&lt;span style="color:#666;font-weight:bold;font-style:italic">$1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;font-style:italic">local&lt;/span> &lt;span style="color:#666;font-weight:bold;font-style:italic">time_left&lt;/span>=&lt;span style="color:#666;font-weight:bold;font-style:italic">$2&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;font-style:italic">echo&lt;/span> -ne &lt;span style="color:#666;font-style:italic">&amp;#39;\033[2A&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;text-decoration:underline">while&lt;/span> [ &lt;span style="color:#666;font-weight:bold;font-style:italic">$time_left&lt;/span> -gt -1 ]; &lt;span style="font-weight:bold;text-decoration:underline">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;font-style:italic">echo&lt;/span> -ne &lt;span style="color:#666;font-style:italic">&amp;#34;\033[1m• &lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$label&lt;/span>&lt;span style="color:#666;font-style:italic">\033[0m — &lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">$(&lt;/span>date -d@&lt;span style="color:#666;font-weight:bold;font-style:italic">$time_left&lt;/span> -u +%M:%S&lt;span style="font-weight:bold;text-decoration:underline">)&lt;/span>&lt;span style="color:#666;font-style:italic">\033[0K\r&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sleep 1
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ((time_left--))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;text-decoration:underline">done&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#888;font-style:italic"># Start the work session&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>display_timer &lt;span style="color:#666;font-style:italic">&amp;#34;Work left&amp;#34;&lt;/span> &lt;span style="color:#666;font-weight:bold;font-style:italic">$WORK_TIME&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#888;font-style:italic"># Add a 🍅 emoji to the fourth line of the journal entry&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sed -i &lt;span style="color:#666;font-style:italic">&amp;#39;4s/$/🍅/&amp;#39;&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$ENTRY&lt;/span>&lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#888;font-style:italic"># Prompt to take a break&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">if&lt;/span> zenity --question --title=&lt;span style="color:#666;font-style:italic">&amp;#34;Pomodoro&amp;#34;&lt;/span> --text=&lt;span style="color:#666;font-style:italic">&amp;#34;Time for a break?&amp;#34;&lt;/span>; &lt;span style="font-weight:bold;text-decoration:underline">then&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> display_timer &lt;span style="color:#666;font-style:italic">&amp;#34;Break left&amp;#34;&lt;/span> &lt;span style="color:#666;font-weight:bold;font-style:italic">$BREAK_TIME&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;text-decoration:underline">if&lt;/span> zenity --question --title=&lt;span style="color:#666;font-style:italic">&amp;#34;Pomodoro&amp;#34;&lt;/span> --text=&lt;span style="color:#666;font-style:italic">&amp;#34;Time to work?&amp;#34;&lt;/span>; &lt;span style="font-weight:bold;text-decoration:underline">then&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;font-style:italic">exec&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">$(&lt;/span>readlink -f &lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$0&lt;/span>&lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">)&lt;/span>&lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span> &lt;span style="color:#888;font-style:italic"># Restart the script for another Pomodoro cycle&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;text-decoration:underline">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">fi&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I have added &lt;code>clear&lt;/code> to the beginning of the script to clear the terminal screen before the script runs, just to ensure a clean display of the timer:&lt;/p>
&lt;p>










&lt;figure class="">

 &lt;div>
 &lt;img loading="lazy" alt="Demonstration of Pomodoro timer" src=" /notes/20240814-pomodoro-demo.gif">
 &lt;/div>

 
 
 &lt;div class="caption-container">
 &lt;figcaption> Demonstration of Pomodoro timer &lt;/figcaption>
 &lt;/div>
 
 
&lt;/figure>&lt;/p></description></item><item><title>Scraping TETR.IO Tetra League standings</title><link>http://patrick.vanderspie.gl/notes/2024-08-13/</link><pubDate>Tue, 13 Aug 2024 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/notes/2024-08-13/</guid><description>&lt;p>Since 2022, I&amp;rsquo;ve regularly enjoyed playing a competitive Tetris variant named &lt;a href="https://tetr.io">TETR.IO&lt;/a>. It&amp;rsquo;s been a while since I have played the game, but when visiting its website I read that the game just moved from it&amp;rsquo;s Alpha stage to Beta, and that the second season of TETR.IO&amp;rsquo;s &lt;code>TETRA LEAGUE&lt;/code> (the game&amp;rsquo;s competitive matchmaking mode) starts on the 16th of August.&lt;/p>
&lt;p>Something that I missed in the Alpha version of the game was the history of progression: I would have loved to see a plot that shows my rank and the game&amp;rsquo;s equivalent of my ELO score over time. With the second season of the game fast approaching, it is the perfect time to create a &lt;code>cron&lt;/code> job for a script that scrapes my &lt;a href="https://ch.tetr.io/u/geuze">TETR.IO profile&lt;/a> and writes the current score to a file. The data itself could then be used to create my plot, and maybe showcase it on my website.&lt;/p>
&lt;p>Inspecting the &lt;code>HTML&lt;/code> of the TETR.IO profile pages, the score is normally displayed in the following &lt;code>div&lt;/code>. Because it is the off-season, however, there is no score to display:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&amp;lt;div class=&lt;span style="color:#666;font-style:italic">&amp;#34;card categorical&amp;#34;&lt;/span> id=&lt;span style="color:#666;font-style:italic">&amp;#34;usercard_league&amp;#34;&lt;/span> style=&lt;span style="color:#666;font-style:italic">&amp;#34;--color: #375433&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>	&amp;lt;h1&amp;gt;&amp;lt;img src=&lt;span style="color:#666;font-style:italic">&amp;#34;/res/league.svg&amp;#34;&lt;/span>&amp;gt;TETRA LEAGUE&amp;lt;/h1&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>	&amp;lt;h6 id=&lt;span style="color:#666;font-style:italic">&amp;#34;user_league_np&amp;#34;&lt;/span>&amp;gt;Off-season&amp;lt;/h6&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>	&amp;lt;div id=&lt;span style="color:#666;font-style:italic">&amp;#34;user_leagueset&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>		&amp;lt;div id=&lt;span style="color:#666;font-style:italic">&amp;#34;user_leaguestateset&amp;#34;&lt;/span>&amp;gt;&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>		&amp;lt;div id=&lt;span style="color:#666;font-style:italic">&amp;#34;user_leaguestandingset&amp;#34;&lt;/span> class=&lt;span style="color:#666;font-style:italic">&amp;#34;standingset&amp;#34;&lt;/span>&amp;gt;abc&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>	&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>	⋯
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Initially, I planned to simply scrape this using:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>curl -s &lt;span style="color:#666;font-style:italic">&amp;#34;https://ch.tetr.io/u/geuze&amp;#34;&lt;/span> | grep -oP &lt;span style="color:#666;font-style:italic">&amp;#39;(?&amp;lt;=&amp;lt;h6 id=&amp;#34;user_league_np&amp;#34;&amp;gt;).*?(?=&amp;lt;/h6&amp;gt;)&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>A simple &lt;code>curl&lt;/code> and &lt;code>grep&lt;/code> of the webpage won&amp;rsquo;t do, however, as the information seems to be loaded using Javascript. Before the beginning of this new season, I&amp;rsquo;ll look into scraping Javascript-rendered web pages using Python.&lt;/p></description></item><item><title>Partially ordered sets with cycles</title><link>http://patrick.vanderspie.gl/notes/2024-08-12/</link><pubDate>Mon, 12 Aug 2024 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/notes/2024-08-12/</guid><description>&lt;p>A &lt;a href="https://mathworld.wolfram.com/PartiallyOrderedSet.html">partially ordered set&lt;/a> — or &lt;em>poset&lt;/em> — $P = (A, \preceq)$ is a pair consisting of a set $A$ and a partial order $\preceq$ on $A$. This partial order is a binary relation on a set $A$ such that — for all $x, y, z \in A$ — it is:&lt;/p>
&lt;ul>
&lt;li>reflexive ($x \preceq x$),&lt;/li>
&lt;li>antisymmetric ($x \preceq y$ and $y \preceq x$ imply $x = y$),&lt;/li>
&lt;li>and transitive ($x \preceq y$ and $y \preceq z$ imply $x \preceq z$).&lt;/li>
&lt;/ul>
&lt;p>For two elements $x, y \in P$, we say that $y$ &lt;a href="https://mathworld.wolfram.com/CoverRelation.html">covers&lt;/a> $x$ if $x \prec y$ and there is no element $z \in P$ such that $x \prec z \prec y$. We write this as $x \lessdot y$.&lt;/p>
&lt;p>I assumed that every poset could be described as a &lt;a href="https://mathworld.wolfram.com/AcyclicDigraph.html">directed acylic graph&lt;/a> through the cover relation between its elements. This intuitively makes sense to me, as cycles (e.g., $x \lessdot y$ and $y \lessdot x$) would be inconsistent with posets due to its antisymmetry, but $x \neq y$ given the definition of the cover relation. Thinking about cycles in orders reminded me of this great example on the link between directed acyclic graphs and genealogy, from the &lt;a href="https://books.google.be/books?id=mKkIGIea_BkC">&lt;em>Handbook of Graph Theory&lt;/em> by Jonathan L. Gross&lt;/a>:&lt;/p>
&lt;div class="quote-block">
 &lt;blockquote class="quote-content">A &amp;ldquo;family tree&amp;rdquo; is a digraph, where the orientation is traditionally given not by arrows but by the direction down for later generations. Despite the name, a family tree is usually not a tree, since people commonly marry distant cousins, knowingly or unknowingly. &lt;strong>However, it is always a DAG, because if there were a cycle, everyone on it would be older than everyone else on the cycle.&lt;/strong>&lt;/blockquote>
&lt;/div>&lt;div class="quote-source-link">
 &lt;a href="https://books.google.be/books?id=mKkIGIea_BkC" target="_blank" rel="noopener">Handbook of Graph Theory by Jonathan L. Gross&lt;/a>
&lt;/div>
&lt;p>I did, however, stumble upon a paper titled &lt;a href="https://inria.hal.science/hal-01360144/document">&lt;em>&amp;ldquo;Cyclic Ordering through Partial Orders&amp;rdquo;&lt;/em> by Stefan Haar&lt;/a>. Briefly scanning through the paper shows some interesting concepts, so I am certainly curious to learn more about this!&lt;/p></description></item><item><title>Describing activities with sparklines</title><link>http://patrick.vanderspie.gl/notes/2024-08-11/</link><pubDate>Sun, 11 Aug 2024 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/notes/2024-08-11/</guid><description>&lt;p>Yesterday, I learned about &lt;a href="https://indieweb.org/sparkline">sparklines&lt;/a> — &amp;ldquo;data-dense graphics with typographic resolution that is shown inline and in-context with relevant text.&amp;rdquo;&lt;/p>
&lt;p>When thinking about how I could use sparklines in an original way within my personal website, I thought they would be interesting to convey properties of sports activities. One could add the elevation profile of a course &lt;span class="sparks bar-medium" style="color: darkgreen"> 🏔️ {22,17,27,37,32,27,37,32,27,22,32,27,37,32,27,37,47,42,52,47} &lt;/span>, or a heart rate curve &lt;span class="sparks dotline-medium" style="color: crimson"> ❤ {22,17,27,37,32,27,37,32,27,22,32,27,37,32,27,37,47,42,52,47} &lt;/span> and power output &lt;span class="sparks dotline-medium" style="color: purple"> ⚡︎ {22,17,27,37,32,27,37,32,27,22,32,27,37,32,27,37,47,42,52,47} &lt;/span> over time, as a sparkline within a blog post about a race.&lt;/p>
&lt;p>With the help of &lt;a href="https://github.com/aftertheflood/sparks/tree/master">Sparks&lt;/a>, which is a typeface for creating sparklines in text, this was implemented quickly:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>{{&amp;lt; sparkline class=&amp;#34;bar-medium&amp;#34; color=&amp;#34;darkgreen&amp;#34;&amp;gt;}} 🏔️ {22,17,27,37,32,27,37,32,27,22,32,27,37,32,27,37,47,42,52,47} {{&amp;lt;/ sparkline &amp;gt;}}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{{&amp;lt; sparkline class=&amp;#34;dotline-medium&amp;#34; color=&amp;#34;crimson&amp;#34;&amp;gt;}} ❤ {22,17,27,37,32,27,37,32,27,22,32,27,37,32,27,37,47,42,52,47} {{&amp;lt;/ sparkline &amp;gt;}}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{{&amp;lt; sparkline class=&amp;#34;dotline-medium&amp;#34; color=&amp;#34;purple&amp;#34; &amp;gt;}} ⚡︎ {22,17,27,37,32,27,37,32,27,22,32,27,37,32,27,37,47,42,52,47} {{&amp;lt;/ sparkline &amp;gt;}}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>In the future, I&amp;rsquo;d like to use actual data from &lt;a href="https://intervals.icu">intervals.icu&lt;/a> for these sparklines.&lt;/p></description></item><item><title>Exploring breakfast spots with Overture Places</title><link>http://patrick.vanderspie.gl/notes/2024-08-10/</link><pubDate>Sat, 10 Aug 2024 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/notes/2024-08-10/</guid><description>&lt;p>I recently learned about Overture places data through a blog post by &lt;a href="https://www.dbreunig.com/2024/07/31/towards-standardizing-place.html">Drew Breunig&lt;/a>. This dataset &amp;ldquo;contains more than 53 million point representations of real-world entities: schools, businesses, (⋯), mountain peaks, and much more&amp;quot;&lt;label for="sidenote-0" class="sidenote-number">&lt;/label>&lt;input type="checkbox" id="sidenote-0" class="sidenote-toggle">&lt;span class="sidenote">&lt;a href="https://docs.overturemaps.org/guides/places/">Places - Overture Maps Documentation&lt;/a>&lt;/span>. You can explore this data through the &lt;a href="https://explore.overturemaps.org">Overture Maps Explorer&lt;/a>.&lt;/p>
&lt;p>I regularly have breakfast with a friend, and we like exploring new breakfast places. The Overture dataset could provide us with new breakfast places in Brussels. For this, we need the &lt;a href="https://wiki.openstreetmap.org/wiki/Bounding_box">bounding box&lt;/a> around the area of Brussels in which we&amp;rsquo;re interested in. We can find this using &lt;a href="http://bboxfinder.com/#50.832939,4.337368,50.858410,4.369812">bboxfinder.com&lt;/a>. For this example, we take the bounding box around &lt;a href="https://en.wikipedia.org/wiki/Pentagon_(Brussels)">the Vijfhoek of Brussels&lt;/a>.&lt;/p>
&lt;p>










&lt;figure class="">

 &lt;div>
 &lt;img loading="lazy" alt="Screenshot of the bounding box around the Vijfhoek of Brussels" src=" /notes/20240810-bbox.png">
 &lt;/div>

 
 
 &lt;div class="caption-container">
 &lt;figcaption> Screenshot of the bounding box around the Vijfhoek of Brussels &lt;/figcaption>
 &lt;/div>
 
 
&lt;/figure>&lt;/p>
&lt;p>Checking out some of the regular places we visit through the Maps Explorer shows that they have the &lt;code>&amp;quot;breakfast_and_brunch_restaurant&amp;quot;&lt;/code> category in common. Using &lt;a href="https://duckdb.org/">&lt;code>duckdb&lt;/code>&lt;/a>, we can download all places within our defined bounding box of &lt;code>50.832939, 4.337368, 50.858410, 4.369812&lt;/code>. The code below is based on an example from the &lt;a href="https://docs.overturemaps.org/guides/places/#data-usage-guidelines">Overture Maps website&lt;/a>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#888;font-style:italic">-- Load necessary modules
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#888;font-style:italic">&lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">LOAD&lt;/span> spatial;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">LOAD&lt;/span> httpfs;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#888;font-style:italic">-- Set the AWS S3 region
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#888;font-style:italic">&lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">SET&lt;/span> s3_region = &lt;span style="color:#666;font-style:italic">&amp;#39;us-west-2&amp;#39;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#888;font-style:italic">-- Copy the filtered data from S3 to a local Parquet file
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#888;font-style:italic">&lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">COPY&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;text-decoration:underline">SELECT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> id,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;text-decoration:underline">names&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> categories,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> websites,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> addresses,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ST_GeomFromWKB(geometry)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;text-decoration:underline">FROM&lt;/span> 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> read_parquet(&lt;span style="color:#666;font-style:italic">&amp;#39;s3://overturemaps-us-west-2/release/2024-07-22.0/theme=places/*/*&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;text-decoration:underline">WHERE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#888;font-style:italic">-- Filter by bounding box (xmin and ymin)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#888;font-style:italic">&lt;/span> bbox.xmin &lt;span style="font-weight:bold;text-decoration:underline">BETWEEN&lt;/span> 4.337368 &lt;span style="font-weight:bold;text-decoration:underline">AND&lt;/span> 4.369812 &lt;span style="font-weight:bold;text-decoration:underline">AND&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> bbox.ymin &lt;span style="font-weight:bold;text-decoration:underline">BETWEEN&lt;/span> 50.832939 &lt;span style="font-weight:bold;text-decoration:underline">AND&lt;/span> 50.858410
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>) &lt;span style="font-weight:bold;text-decoration:underline">TO&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#39;vijfhoek.parquet&amp;#39;&lt;/span>;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Next, we can query all the places where the primary category is &lt;code>&amp;quot;breakfast_and_brunch_restaurant&amp;quot;&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">SELECT&lt;/span> 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;text-decoration:underline">names&lt;/span>.&lt;span style="font-weight:bold;text-decoration:underline">primary&lt;/span> &lt;span style="font-weight:bold;text-decoration:underline">AS&lt;/span> name, 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> addresses[1].freeform &lt;span style="font-weight:bold;text-decoration:underline">AS&lt;/span> address, 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> websites[1] &lt;span style="font-weight:bold;text-decoration:underline">AS&lt;/span> website
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;text-decoration:underline">FROM&lt;/span> 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#666;font-style:italic">&amp;#39;vijfhoek.parquet&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;text-decoration:underline">WHERE&lt;/span> 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> categories.&lt;span style="font-weight:bold;text-decoration:underline">primary&lt;/span> = &lt;span style="color:#666;font-style:italic">&amp;#39;breakfast_and_brunch_restaurant&amp;#39;&lt;/span>;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre tabindex="0">&lt;code>┌──────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ name │ address │ website │
│ varchar │ varchar │ varchar │
├──────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Flower food │ Rue Blaes 264 │ │
│ Le Père Tranquille │ Rue des Renards 4 │ │
│ Brussels Waffle Workshop │ Rue des Foulons 30 │ http://www.waffleworkshop.com │
│ Woodpecker 20 │ Rue Jourdan 20 │ │
│ EatClub Brussels │ Rue de Stassart 18 │ │
│ Café Boudin │ Rue Ravenstein 20 │ https://cafeboudin.be │
│ bloom_and_brunch_by_sabri │ Rue de la Paix 7 │ │
│ Lastra │ Rue du Conseil 24 │ │
│ Le Coin Des Saveurs │ Boulevard de l&amp;#39;Abattoir 19 │ │
│ Kandinsky │ Chaussée de Gand 37 │ https://kandinsky-family.be/ │
│ Restaurant Baurade │ Boulevard Anspach 153 │ │
│ Kafei Dansaert │ Rue Antoine Dansaert 57 │ https://kafei.be │
│ Le Odin Royal │ Oude Graanmarkt 2 │ │
│ Movenpick │ │ │
│ Woodpecker St. Cath. │ Quai au Bois à Brûler 27 │ │
│ Restaurant Al Andalus │ Rue de Ribaucourt 4 │ │
│ Gaufres and Waffles │ Chemin du Croquet 1 │ http://www.gaufresandwaffles.com │
│ Oats Day Long │ Europakruispunt 3 │ https://www.oatsdaylong.com │
│ Belrose cake │ Boulevard Émile Jacqmain 3 │ https://www.belrosecake.com/ │
│ Spread the moon │ Rue du Gentilhomme 13 │ https://www.instagram.com/spreadthemoon/ │
│ Longitude Nord-Sud │ Quai aux Pierres de Taille 14 │ http://longitude.blog4ever.com/ │
├──────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 21 rows 3 columns │
└──────────────────────────────────────────────────────────────────────────────────────────────────────┘
&lt;/code>&lt;/pre>&lt;p>This is a good start, but lots of places we already visited are still missing from this list. We can query for all places where &lt;code>'breakfast_and_brunch_restaurant'&lt;/code> is listed under alternate categories using &lt;code>OR 'breakfast_and_brunch_restaurant' = ANY (categories.alternate);&lt;/code>.&lt;/p>
&lt;p>I&amp;rsquo;m planning to pick this idea up later, and check if I can find a way to include more breakfast and brunch spots. In a next step, I could attempt to scrape the provided websites to check menus for our usual order.&lt;/p></description></item><item><title>intervals.icu, GitHub Actions, and Hugo shortcodes</title><link>http://patrick.vanderspie.gl/notes/2024-08-09/</link><pubDate>Fri, 09 Aug 2024 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/notes/2024-08-09/</guid><description>&lt;h2 id="intervalsicu-api">Intervals.icu API&lt;/h2>
&lt;p>I sync all my Strava activities to &lt;a href="https://intervals.icu">intervals.icu&lt;/a>, which calculates my training load, fitness score, and my weekly ramp rate. To test out this platform&amp;rsquo;s &lt;code>API&lt;/code>, I wanted to calculate the distance that I ran or cycled given a custom date range, as this is something that I don&amp;rsquo;t immediately see inside intervals.icu itself. &lt;code>GET /api/v1/athlete/{id}/activities&lt;/code> returns a list of activities between the parameters &lt;code>oldest&lt;/code> and &lt;code>newest&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">BASE_URL&lt;/span>=&lt;span style="color:#666;font-style:italic">&amp;#34;https://intervals.icu/api/v1/athlete&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">OLDEST&lt;/span>=&lt;span style="color:#666;font-style:italic">&amp;#34;2024-01-01&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">NEWEST&lt;/span>=&lt;span style="color:#666;font-style:italic">&amp;#34;2024-12-31&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>curl -s -u API_KEY:&lt;span style="color:#666;font-weight:bold;font-style:italic">$API_KEY&lt;/span> &lt;span style="color:#666;font-style:italic">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$BASE_URL&lt;/span>&lt;span style="color:#666;font-style:italic">/&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$INTERVALS_UID&lt;/span>&lt;span style="color:#666;font-style:italic">/activities?oldest=&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$OLDEST&lt;/span>&lt;span style="color:#666;font-style:italic">&amp;amp;newest=&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$NEWEST&lt;/span>&lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>As we can&amp;rsquo;t select specific activity types through the &lt;code>API&lt;/code> (we&amp;rsquo;re only interested in runs for this example, but this list contains all types of activities), I made use of &lt;a href="https://jqlang.github.io/jq/">&lt;code>jq&lt;/code> — &amp;ldquo;a lightweight and flexible command-line &lt;code>JSON&lt;/code> processor&amp;rdquo;&lt;/a> — to select the name, start time, and distance of running activities only:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">BASE_URL&lt;/span>=&lt;span style="color:#666;font-style:italic">&amp;#34;https://intervals.icu/api/v1/athlete&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">OLDEST&lt;/span>=&lt;span style="color:#666;font-style:italic">&amp;#34;2024-01-01&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">NEWEST&lt;/span>=&lt;span style="color:#666;font-style:italic">&amp;#34;2024-12-31&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>curl -s -u API_KEY:&lt;span style="color:#666;font-weight:bold;font-style:italic">$API_KEY&lt;/span> &lt;span style="color:#666;font-style:italic">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$BASE_URL&lt;/span>&lt;span style="color:#666;font-style:italic">/&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$INTERVALS_UID&lt;/span>&lt;span style="color:#666;font-style:italic">/activities?oldest=&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$OLDEST&lt;/span>&lt;span style="color:#666;font-style:italic">&amp;amp;newest=&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$NEWEST&lt;/span>&lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span> | &lt;span style="color:#666;font-style:italic">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">&lt;/span>jq &lt;span style="color:#666;font-style:italic">&amp;#39;[.[] | select(.type == &amp;#34;Run&amp;#34;) | {name, start_date_local, distance}]&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>[
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="">⋯&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;#34;name&amp;#34;: &lt;span style="color:#666;font-style:italic">&amp;#34;Night Run&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;#34;start_date_local&amp;#34;: &lt;span style="color:#666;font-style:italic">&amp;#34;2024-08-06T21:44:54&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;#34;distance&amp;#34;: 6699
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;#34;name&amp;#34;: &lt;span style="color:#666;font-style:italic">&amp;#34;Afternoon Run&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;#34;start_date_local&amp;#34;: &lt;span style="color:#666;font-style:italic">&amp;#34;2024-08-06T16:06:24&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;#34;distance&amp;#34;: 3400.5
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;#34;name&amp;#34;: &lt;span style="color:#666;font-style:italic">&amp;#34;Evening Run&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;#34;start_date_local&amp;#34;: &lt;span style="color:#666;font-style:italic">&amp;#34;2024-08-04T20:58:21&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;#34;distance&amp;#34;: 4928
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="">⋯&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This shows that our filter for running activities works. To now sum the distance of all our activities, we just need to select the distance of every activity and map &lt;code>add&lt;/code> over it. This returns the total distance in meters.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">BASE_URL&lt;/span>=&lt;span style="color:#666;font-style:italic">&amp;#34;https://intervals.icu/api/v1/athlete&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">OLDEST&lt;/span>=&lt;span style="color:#666;font-style:italic">&amp;#34;2024-01-01&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">NEWEST&lt;/span>=&lt;span style="color:#666;font-style:italic">&amp;#34;2024-12-31&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>curl -s -u API_KEY:&lt;span style="color:#666;font-weight:bold;font-style:italic">$API_KEY&lt;/span> &lt;span style="color:#666;font-style:italic">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$BASE_URL&lt;/span>&lt;span style="color:#666;font-style:italic">/&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$INTERVALS_UID&lt;/span>&lt;span style="color:#666;font-style:italic">/activities?oldest=&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$OLDEST&lt;/span>&lt;span style="color:#666;font-style:italic">&amp;amp;newest=&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$NEWEST&lt;/span>&lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span> | &lt;span style="color:#666;font-style:italic">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">&lt;/span>jq &lt;span style="color:#666;font-style:italic">&amp;#39;map(select(.type == &amp;#34;Run&amp;#34;) | .distance) | add&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>701168.5
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="github-actions-for-hugo-shortcode">GitHub Actions for Hugo shortcode&lt;/h2>
&lt;p>As I wanted to do something with this distance, I made a GitHub Actions workflow that calculates the distance every day at midnight and uses &lt;code>sed&lt;/code> to replace the Hugo variable &lt;code>.Site.Params.distanceRun&lt;/code>. Using this variable, I created a Hugo shortcode &lt;code>{{&amp;lt; distance_run &amp;gt;}}&lt;/code> that returns the distance run in kilometers:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>{{- $distanceMeters := .Site.Params.distanceRun -}}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{{- $distanceKilometers := div $distanceMeters 1000 -}}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{{ printf &amp;#34;%.1f&amp;#34; $distanceKilometers }} km
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Using &lt;code>{{&amp;lt; distance_run &amp;gt;}}&lt;/code> in a Hugo content file returns: 418.2 km.
I can put this value in a &lt;a href="http://patrick.vanderspie.gl/notes/2024-08-08/">tooltip&lt;/a> with an explanation of the value, which is displayed as &lt;span class="tooltip">418.2 km&lt;span class="tooltiptext">Total distance since 1st of Jan.&lt;/span>&lt;/span>:&lt;/p>
&lt;pre tabindex="0">&lt;code>{{&amp;lt; tooltip &amp;gt;}}{{&amp;lt; distance_run &amp;gt;}} | Total distance since 1st of Jan.{{&amp;lt;/ tooltip &amp;gt;}}
&lt;/code>&lt;/pre></description></item><item><title>Tooltip on mouse hover with Hugo</title><link>http://patrick.vanderspie.gl/notes/2024-08-08/</link><pubDate>Thu, 08 Aug 2024 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/notes/2024-08-08/</guid><description>&lt;p>I added custom &lt;span class="tooltip">tooltips&lt;span class="tooltiptext">Just like this one!&lt;/span>&lt;/span> to this website, as I want to provide extra information to specific text in my posts and notes. This can be done using regular &lt;a href="https://gohugo.io/content-management/shortcodes/">shortcodes&lt;/a>, which are simple snippets inside a content file that Hugo will render using a &lt;code>HTML&lt;/code> template:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-markdown" data-lang="markdown">&lt;span style="display:flex;">&lt;span>{{&amp;lt; tooltip outer_text=&lt;span style="color:#666;font-style:italic">&amp;#34;tooltips&amp;#34;&lt;/span> tooltip_text=&lt;span style="color:#666;font-style:italic">&amp;#34;Just like this one!&amp;#34;&lt;/span> &amp;gt;}}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This works fine if we just provide simple, static text. One of the criteria for my tooltip shortcode, however, is that I should be able to put other shortcodes inside them, as I have a couple of shortcodes that make use of variables that are updated automatically using GitHub Actions. One example of this is the number of kilometers I ran this year &lt;em>&lt;span class="tooltip">(418.2 km, currently)&lt;span class="tooltiptext">Updated using GitHub actions + intervals.icu &lt;/span>&lt;/span>&lt;/em>, which I calculate using the &lt;a href="https://intervals.icu/">intervals.icu&lt;/a> &lt;code>API&lt;/code>.&lt;/p>
&lt;p>As far as I know, there is no standard way of adding these tooltips in Hugo. I created them myself by using paired shortcodes and using &lt;code>.Inner&lt;/code> to return the content between the opening and closing shortcode tags, which allows us to do the following:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-markdown" data-lang="markdown">&lt;span style="display:flex;">&lt;span>{{&amp;lt; tooltip &amp;gt;}} {{&amp;lt; inner_shortcode &amp;gt;}} {{&amp;lt;/ tooltip &amp;gt;}}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now, we can get the contents of the inner shortcode through &lt;code>.Inner&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&amp;lt;span class=&lt;span style="color:#666;font-style:italic">&amp;#34;tooltip&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {{- .Inner | safeHTML -}}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;span class=&lt;span style="color:#666;font-style:italic">&amp;#34;tooltiptext&amp;#34;&lt;/span>&amp;gt;{{- .Inner | safeHTML -}}&amp;lt;/span&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/span&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>We want to put some text as the outer text of the tooltip, and other text in the tooltip box itself, however. Through a hack provided by someone on the &lt;a href="https://discourse.gohugo.io/t/shortcode-nested-shortcode-multiple-inner/27441/9">Hugo forum&lt;/a>, we can split up &lt;code>.Inner&lt;/code> by providing a custom delimiter. For my tooltips, I divide the contents of the paired shortcodes in the outer text and the tooltip text using &lt;code>|&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>{{- $children := split .Inner &amp;#34; | &amp;#34; -}}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;span class=&lt;span style="color:#666;font-style:italic">&amp;#34;tooltip&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {{- index $children 0 | safeHTML -}}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;span class=&lt;span style="color:#666;font-style:italic">&amp;#34;tooltiptext&amp;#34;&lt;/span>&amp;gt;{{- index $children 1 | safeHTML -}}&amp;lt;/span&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/span&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This way, I can define tooltips like this, for example: &lt;span class="tooltip"> 418.2 km&lt;span class="tooltiptext">Updated using GitHub actions + intervals.icu &lt;/span>&lt;/span>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-markdown" data-lang="markdown">&lt;span style="display:flex;">&lt;span>{{&amp;lt; tooltip &amp;gt;}} {{&amp;lt; distance_run &amp;gt;}} km | See intervals.icu {{&amp;lt;/ tooltip &amp;gt;}}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></description></item><item><title>Daily notes with GitHub Actions</title><link>http://patrick.vanderspie.gl/notes/2024-08-07/</link><pubDate>Wed, 07 Aug 2024 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/notes/2024-08-07/</guid><description>&lt;p>I am using &lt;a href="https://github.com/features/actions">GitHub Actions&lt;/a> to automatically create new daily notes on this website. Every day at midnight, a GitHub Action checks the &lt;code>content/notes&lt;/code> folder for a file titled &lt;code>%Y-%m-%d.md&lt;/code> (matching the current date). If this file does not yet exist, a new &lt;code>%Y-%m-%d.md&lt;/code> file with a standard &lt;a href="https://gohugo.io/content-management/front-matter/">Hugo front matter&lt;/a> — filled in with the current date and a placeholder title — is created.&lt;/p>
&lt;p>To keep track of my daily writing streak, I also need to check if the previous day&amp;rsquo;s note was modified, and update my streak accordingly. I do this using a combination of &lt;code>git&lt;/code>, &lt;code>grep&lt;/code>, and &lt;code>sed&lt;/code>.&lt;/p>
&lt;h3 id="git-log">&lt;code>git log&lt;/code>&lt;/h3>
&lt;p>The &lt;a href="https://git-scm.com/docs/git-log">&lt;code>git log&lt;/code>&lt;/a> command shows the commit log, which is used to check if there have been any other commits involving the previous day&amp;rsquo;s note, outside of its creation by GitHub Actions. If this is the case, there should be more than one commit involving this file, and I can thus assume that I have written something. Based on this output, the existing streak — which is stored in the &lt;a href="https://gohugo.io/getting-started/configuration/#configuration-file">&lt;code>hugo.toml&lt;/code>&lt;/a> configuration file — is updated.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">PREV_DATE&lt;/span>=&lt;span style="font-weight:bold;text-decoration:underline">$(&lt;/span>date -d &lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="color:#666;font-style:italic">${&lt;/span>{ env.DATE &lt;span style="color:#666;font-style:italic">}&lt;/span>&lt;span style="color:#666;font-style:italic">} -1 day&amp;#34;&lt;/span> +&lt;span style="color:#666;font-style:italic">&amp;#39;%Y-%m-%d&amp;#39;&lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">if&lt;/span> [[ &lt;span style="font-weight:bold;text-decoration:underline">$(&lt;/span>git log --name-only --since=&lt;span style="color:#666;font-style:italic">&amp;#34;24 hours ago&amp;#34;&lt;/span> &lt;span style="color:#666;font-style:italic">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">&lt;/span>	 --pretty=format: -- &lt;span style="color:#666;font-style:italic">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">&lt;/span>	 content/notes/&lt;span style="color:#666;font-weight:bold;font-style:italic">$PREV_DATE&lt;/span>.md | &lt;span style="color:#666;font-style:italic">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">&lt;/span>	 grep -c content/notes/&lt;span style="color:#666;font-weight:bold;font-style:italic">$PREV_DATE&lt;/span>.md&lt;span style="font-weight:bold;text-decoration:underline">)&lt;/span> -gt 1 ]]; &lt;span style="font-weight:bold;text-decoration:underline">then&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;font-style:italic">echo&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#34;MODIFIED=true&amp;#34;&lt;/span> &amp;gt;&amp;gt; &lt;span style="color:#666;font-weight:bold;font-style:italic">$GITHUB_ENV&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">else&lt;/span> 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;font-style:italic">echo&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#34;MODIFIED=false&amp;#34;&lt;/span> &amp;gt;&amp;gt; &lt;span style="color:#666;font-weight:bold;font-style:italic">$GITHUB_ENV&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">fi&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>For this to work, however, we need to &lt;a href="https://stackoverflow.com/a/62335935">pass the &lt;code>HEAD&lt;/code>&amp;rsquo;s &lt;code>SHA&lt;/code> as a &lt;code>ref&lt;/code> to the checkout&lt;/a>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>- name: Checkout repository
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> uses: actions/checkout@v3
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> with:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ref: ${{ github.event.pull_request.head.sha }}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fetch-depth: 0
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="grep">&lt;code>grep&lt;/code>&lt;/h3>
&lt;p>To find the current value of the writing streak in the configuration file — and the corresponding date that matches this streak — I use &lt;code>grep&lt;/code>. These outputs are stored in environment variables.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">CURRENT_STREAK&lt;/span>=&lt;span style="font-weight:bold;text-decoration:underline">$(&lt;/span>grep -Po &lt;span style="color:#666;font-style:italic">&amp;#39;(?&amp;lt;=CURRENT_WRITING_STREAK = )\d+&amp;#39;&lt;/span> &lt;span style="color:#666;font-weight:bold;font-style:italic">$TOML_FILE&lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">CURRENT_STREAK_DATE&lt;/span>=&lt;span style="font-weight:bold;text-decoration:underline">$(&lt;/span>grep -Po &lt;span style="color:#666;font-style:italic">&amp;#39;(?&amp;lt;=CURRENT_WRITING_STREAK_DATE = )\d{4}-\d{2}-\d{2}&amp;#39;&lt;/span> &lt;span style="color:#666;font-weight:bold;font-style:italic">$TOML_FILE&lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="sed">&lt;code>sed&lt;/code>&lt;/h3>
&lt;p>The streak value in the configuration file is updated using &lt;code>sed&lt;/code>, based on our earlier result from the &lt;code>git log&lt;/code> command. We replace the current streak value with &lt;code>$((CURRENT_STREAK + 1))&lt;/code> if there is more than one commit involving the previous day&amp;rsquo;s note, or replace the streak with &lt;code>0&lt;/code> otherwise. This new streak is stored in the &lt;code>$NEW_STREAK&lt;/code> environment variable. We also do the same to keep track of the best writing streak to date.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>sed -i &lt;span style="color:#666;font-style:italic">&amp;#34;s/CURRENT_WRITING_STREAK = &lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$CURRENT_STREAK&lt;/span>&lt;span style="color:#666;font-style:italic">/CURRENT_WRITING_STREAK = &lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$NEW_STREAK&lt;/span>&lt;span style="color:#666;font-style:italic">/&amp;#34;&lt;/span> &lt;span style="color:#666;font-weight:bold;font-style:italic">$TOML_FILE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>sed -i &lt;span style="color:#666;font-style:italic">&amp;#34;s/CURRENT_WRITING_STREAK_DATE = &lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$CURRENT_STREAK_DATE&lt;/span>&lt;span style="color:#666;font-style:italic">/CURRENT_WRITING_STREAK_DATE = &lt;/span>&lt;span style="color:#666;font-style:italic">${&lt;/span>{ env.DATE &lt;span style="color:#666;font-style:italic">}&lt;/span>&lt;span style="color:#666;font-style:italic">}/&amp;#34;&lt;/span> &lt;span style="color:#666;font-weight:bold;font-style:italic">$TOML_FILE&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The above &lt;code>sed&lt;/code> command kept throwing an error, however: &lt;code>sed: -e (...) unterminated 's' command&lt;/code>. I tried everything from attempting to escape underscores, to adding &lt;code>--&lt;/code> to &lt;code>sed&lt;/code> (as maybe the hyphens in the dates could be confused with a &lt;code>sed&lt;/code> option that made something break), as I first thought that these characters were possibly causing these issues.&lt;/p>
&lt;p>In the end, I realised that the issue was caused due to &lt;code>grep&lt;/code> matching multiple lines in the &lt;code>hugo.toml&lt;/code> — which also included newlines and spaces. I did not realise that newlines would be included in the environment variables, but making sure the regex match was only limited to the &lt;code>CURRENT_WRITING_STREAK&lt;/code> variable in the configuration file and trimming any newlines, resolved the issue:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">CURRENT_STREAK&lt;/span>=&lt;span style="font-weight:bold;text-decoration:underline">$(&lt;/span>&lt;span style="font-weight:bold;font-style:italic">echo&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$CURRENT_STREAK&lt;/span>&lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span> | tr -d &lt;span style="color:#666;font-style:italic">&amp;#39;\n&amp;#39;&lt;/span> | tr -d &lt;span style="color:#666;font-style:italic">&amp;#39; &amp;#39;&lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-weight:bold;font-style:italic">CURRENT_STREAK_DATE&lt;/span>=&lt;span style="font-weight:bold;text-decoration:underline">$(&lt;/span>&lt;span style="font-weight:bold;font-style:italic">echo&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="color:#666;font-weight:bold;font-style:italic">$CURRENT_STREAK_DATE&lt;/span>&lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span> | tr -d &lt;span style="color:#666;font-style:italic">&amp;#39;\n&amp;#39;&lt;/span> | tr -d &lt;span style="color:#666;font-style:italic">&amp;#39; &amp;#39;&lt;/span>&lt;span style="font-weight:bold;text-decoration:underline">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>By double-checking the used regex and removing unwanted characters, the &lt;code>sed&lt;/code> command could successfully update the values in the &lt;code>hugo.toml&lt;/code> file without errors:&lt;/p>
&lt;ul>
&lt;li>Writing streak of
&lt;span class="tooltip">
	
	
	
	
	0 days&lt;span class="tooltiptext">Updated on May 10, 2026
&lt;/span>&lt;/span>&lt;/li>
&lt;li>Best writing streak to date is
&lt;span class="tooltip">
	
	
	
	
	
	20 days&lt;span class="tooltiptext">Achieved on Aug 27, 2024
&lt;/span>&lt;/span>&lt;/li>
&lt;/ul></description></item></channel></rss>