This commit is contained in:
Neale Pickett 2023-01-12 13:58:32 -07:00
commit a8a5b12ae4
31 changed files with 1877 additions and 19 deletions

View File

@ -1,9 +1,6 @@
---
date: "2021-12-23T00:00:00Z"
tags:
- drwho
title: 'Doctor Who S01E27-30: Doctor Who goes to Mexico'
url: blog/2021-12-23-drwho-S01E27-mexico/
title: "Doctor Who S01E27-30: Doctor Who goes to Mexico"
date: 2021-12-23
---
A bunch of white people pretend to be Aztecs, and explore their moral
@ -42,7 +39,7 @@ Barbara abusing her god status to outlaw human sacrifice. The Doctor gets
married or engaged or something. Susan finally expresses an emotion other
than terror:
{{< video "susan-well-hello-there.mp4" "Well, hello there." >}}
{{< video src="susan-well-hello-there.mp4" text="Well, hello there." >}}
I'm back to being uncomfortable with the cultural framing here. I mean,
maybe this is useful for viewing 1960s British culture, but that's still

View File

@ -1,7 +1,6 @@
---
date: "2022-09-06T12:11:00-0600"
title: Truck bling
url: blog/2022-09-06-truck-bling/
date: "2022-09-06T12:11:00-0600"
---
Yesterday,
@ -18,7 +17,7 @@ the friend soldered a bunch of stuff together,
and we plugged it in.
It friggin' worked!
{{< video "truck-bling.m4v" "Pickup truck with color-changing ground effects">}}
{{< video src="truck-bling.m4v" text="Pickup truck with color-changing ground effects">}}
The really cool part, at least for me,
is that now she can hang out with her laptop in the cabin,

View File

@ -1,6 +1,7 @@
---
title: CLRG's Cheating Scandal
date: 2022-10-04
tags: clrg
---
$SPOUSE just stumbled across a PowerPoint file with a bunch of text messages,

View File

@ -0,0 +1,122 @@
let awardPoints = [
100, // 1
75, // 2
65, // 3
60, // 4
56, // 5
53, // 6
50, // 7
47, // 8
45, // 9
43, // 10
41, // 11
39, // 12
38, // 13
37, // 14
36, // 15
35, // 16
34, // 17
33, // 18
32, // 19
31, // 20
30, // 21
29, // 22
28, // 23
27, // 24
26, // 25
25, // 26
24, // 27
23, // 28
22, // 29
21, // 30
20, // 31
19, // 32
18, // 33
17, // 34
16, // 35
15, // 36
14, // 37
13, // 38
12, // 39
11, // 40
10, // 41
9, // 42
8, // 43
7, // 44
6, // 45
5, // 46
4, // 47
3, // 48
2, // 49
1, // 50
0.75, // 51
0.65, // 52
0.60, // 53
0.56, // 54
0.53, // 55
0.50, // 56
0.47, // 57
0.45, // 58
0.43, // 59
0.41, // 60
0.39, // 61
0.38, // 62
0.37, // 63
0.36, // 64
0.35, // 65
0.34, // 66
0.33, // 67
0.32, // 68
0.31, // 69
0.30, // 70
0.29, // 71
0.28, // 72
0.27, // 73
0.26, // 74
0.25, // 75
0.24, // 76
0.23, // 77
0.22, // 78
0.21, // 79
0.20, // 80
0.19, // 81
0.18, // 82
0.17, // 83
0.16, // 84
0.15, // 85
0.14, // 86
0.13, // 87
0.12, // 88
0.11, // 89
0.10, // 90
0.09, // 91
0.08, // 92
0.07, // 93
0.06, // 94
0.05, // 95
0.04, // 96
0.03, // 97
0.02, // 98
0.01, // 99
0.00, // 100
]
function init() {
for (let tbody of document.querySelectorAll(".awardPoints tbody")) {
for (let i = 0; i < awardPoints.length; i++) {
let tr = tbody.appendChild(document.createElement("tr"))
tr.appendChild(document.createElement("td")).textContent = i + 1
tr.appendChild(document.createElement("td")).textContent = awardPoints[i].toFixed(2)
}
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init)
} else {
init()
}
export {
awardPoints,
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,261 @@
---
title: CLRG Scoring Analyzed
date: 2022-10-09
tags:
- clrg
stylesheets:
- toys.css
scripts:
- scorecard.mjs
---
Let's take a look how how CLRG does its scoring!
*With math!*
## How CLRG Scoring Works
As I am given to understand, the scoring works like so:
1. Adjudicators give you a "raw score": a real number between 0 and 100
2. The scoring system ranks each dancer per adjudicator, based on raw scores
3. These rankings are mapped into "award points"
4. All of a dancer's award points are summed
5. Final ranking is determined by comparing total award points
## Raw Scoring
The way raw scores translate into rankings and award points is a little
confusing, so I've made a little tool you can play with to get a feel for how it
works. Essentially, it's a way of normalizing places to an adjudicator: score
weights are only relative to the judge that assigns them.
Adjudicator A can assign scores between 80 and 100;
adjudicator B can assign scores between 1 and 40;
and they'll both have a first, second, third, fourth place, etc.
These places then get translated into award points.
## Award Points
Award points are handed out based on ranking against other dancers for that
adjudicator. I obtained these values from a FeisWorx results page for my kid:
<div class="awardPoints">
<table>
<thead>
<tr>
<th>Ranking</th><th>Award Points</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
If there's a 2-way, 3-way, or n-way tie,
all tied dancers get the average of the next 2, 3, or n award points,
and the next 2, 3, or n rankings are skipped.
## What's with these values?
At first glance, the award points look like the output of an exponential function.
{{<figure src="chart.png" alt="Chart of scores vs. award points">}}
In an effort to figure out where these numbers came from,
I ran some curve fitting against the data.
Here's the best I could come up with:
| Ranking range | Award Points Function | Type of function |
| --: | --: | --- |
| 1 - 11 | 100 * x^-0.358 | Exponential |
| 12 - 50 | 51 - x | Linear |
| 51 - 60 | 14.2 - 0.46x + 0.00385x | Polynomial |
| 61 - 100 | 1 - x/100 | Linear |
If you, dear reader, are a mathematician,
I would love to hear your thoughts on why they went with this algorithm.
There are a few points to note here:
* 1st place is a *huge deal*. Disproportionately huge.
* Places 2-10 are similarly big deals compared to places 3-11.
* Places 12-50 operate the way most people probably assume ranking works: linearly.
* Places 51-60 fit best to a second degree polynomial, but it doesn't matter much for differences of hundreths of a point. This section is *really weird*, mathematically.
* Places 61-100 are all less than 1 point. If you're a judge trying to tank a top dancer, anywhere in this range is equivalent to anywhere else.
## Consequences of Exponential Award Points
Playing around with this,
I've found a few interesting consequences
of the exponential growth in the top 11 places.
### 1st place is super important
1st place is weighted so heavily that one judge could move a 5th place dancer into 2nd.
<table class="scorecard">
<thead>
<tr>
<td></td>
<th>Alice</th>
<th>Bob</th>
<th>Carol</th>
</tr>
</thead>
<tbody>
<tr>
<th class="justify-left">Adj. 1</th>
<td><input type="number" min=1 max=99 value=1 readonly></td>
<td><input type="number" min=1 max=99 value=3></td>
<td><input type="number" min=1 max=99 value=2></td>
</tr>
<tr>
<th class="justify-left">Adj. 2</th>
<td><input type="number" min=1 max=99 value=5></td>
<td><input type="number" min=1 max=99 value=3></td>
<td><input type="number" min=1 max=99 value=1></td>
</tr>
<tr>
<th class="justify-left">Adj. 3</th>
<td><input type="number" min=1 max=99 value=5></td>
<td><input type="number" min=1 max=99 value=3></td>
<td><input type="number" min=1 max=99 value=2></td>
</tr>
</tbody>
<tfoot>
<tr>
<th class="justify-left">Award Points</th>
<td class="justify-right"><output name="points"></td>
<td class="justify-right"><output name="points"></td>
<td class="justify-right"><output name="points"></td>
</tr>
<tr>
<th class="justify-left">Ranking</th>
<td class="justify-right"><output name="ranking"></td>
<td class="justify-right"><output name="ranking"></td>
<td class="justify-right"><output name="ranking"></td>
</tr>
</tfoot>
</table>
You can adjust these values to get a better feel for how scoring works.
### Tanking a high-ranked dancer is another way to cheat
Because of that exponential curve,
a low ranking from a single judge can carry a lot of weight.
<table class="scorecard">
<thead>
<tr>
<td></td>
<th>Alice</th>
<th>Bob</th>
<th>Carol</th>
</tr>
</thead>
<tbody>
<tr>
<th class="justify-left">Adj. 1</th>
<td><input type="number" min=1 max=99 value=3></td>
<td><input type="number" min=1 max=99 value=11></td>
<td><input type="number" min=1 max=99 value=2></td>
</tr>
<tr>
<th class="justify-left">Adj. 2</th>
<td><input type="number" min=1 max=99 value=3></td>
<td><input type="number" min=1 max=99 value=1></td>
<td><input type="number" min=1 max=99 value=2></td>
</tr>
<tr>
<th class="justify-left">Adj. 3</th>
<td><input type="number" min=1 max=99 value=3></td>
<td><input type="number" min=1 max=99 value=2></td>
<td><input type="number" min=1 max=99 value=1></td>
</tr>
</tbody>
<tfoot>
<tr>
<th class="justify-left">Award Points</th>
<td class="justify-right"><output name="points"></td>
<td class="justify-right"><output name="points"></td>
<td class="justify-right"><output name="points"></td>
</tr>
<tr>
<th class="justify-left">Ranking</th>
<td class="justify-right"><output name="ranking"></td>
<td class="justify-right"><output name="ranking"></td>
<td class="justify-right"><output name="ranking"></td>
</tr>
</tfoot>
</table>
### Being in 1st provides a nice buffer
Try playing around with Alice's rankings with Adjudicators 2 and 3 here.
She has to get ranked a lot lower before her overall ranking starts going down.
<div class="scrolly">
<table class="scorecard">
<thead>
<tr>
<td></td>
<th>Alice</th>
<th>Bob</th>
<th>Carol</th>
<th>Dave</th>
<th>Erin</th>
</tr>
</thead>
<tbody>
<tr>
<th class="justify-left">Adj. 1</th>
<td><input type="number" min=1 max=99 value=1></td>
<td><input type="number" min=1 max=99 value=3></td>
<td><input type="number" min=1 max=99 value=2></td>
<td><input type="number" min=1 max=99 value=4></td>
<td><input type="number" min=1 max=99 value=5></td>
</tr>
<tr>
<th class="justify-left">Adj. 2</th>
<td><input type="number" min=1 max=99 value=7></td>
<td><input type="number" min=1 max=99 value=1></td>
<td><input type="number" min=1 max=99 value=2></td>
<td><input type="number" min=1 max=99 value=3></td>
<td><input type="number" min=1 max=99 value=4></td>
</tr>
<tr>
<th class="justify-left">Adj. 3</th>
<td><input type="number" min=1 max=99 value=5></td>
<td><input type="number" min=1 max=99 value=2></td>
<td><input type="number" min=1 max=99 value=1></td>
<td><input type="number" min=1 max=99 value=3></td>
<td><input type="number" min=1 max=99 value=4></td>
</tr>
</tbody>
<tfoot>
<tr>
<th class="justify-left">Award Points</th>
<td class="justify-right"><output name="points"></td>
<td class="justify-right"><output name="points"></td>
<td class="justify-right"><output name="points"></td>
<td class="justify-right"><output name="points"></td>
<td class="justify-right"><output name="points"></td>
</tr>
<tr>
<th class="justify-left">Ranking</th>
<td class="justify-right"><output name="ranking"></td>
<td class="justify-right"><output name="ranking"></td>
<td class="justify-right"><output name="ranking"></td>
<td class="justify-right"><output name="ranking"></td>
<td class="justify-right"><output name="ranking"></td>
</tr>
</tfoot>
</table>
</div>

View File

@ -0,0 +1,66 @@
import {awardPoints} from "./awardPoints.mjs"
function scorecardUpdate(scorecard) {
let scores = []
let points = []
let highestRank = []
let firstRow = scorecard.querySelector("tbody tr")
for (let input of firstRow.querySelectorAll("input")) {
scores.push(0)
points.push(0)
}
for (let row of scorecard.querySelectorAll("tbody tr")) {
let i = 0
for (let input of row.querySelectorAll("input")) {
let ranking = Number(input.value)
scores[i] += ranking
points[i] += awardPoints[ranking]
highestRank[i] = Math.min(highestRank[i] || 100, ranking)
i += 1
}
}
{
let i = 0
for (let out of scorecard.querySelectorAll("tfoot output[name='points']")) {
out.value = points[i]
i += 1
}
}
{
let rankOffset = 0
let overallRanking = []
let rankedPoints = [...points].sort((a, b) => b - a)
for (let i = 0; i < points.length; i++) {
overallRanking[i] = rankedPoints.indexOf(points[i]) + 1
if (overallRanking[i] == 1) {
rankOffset = highestRank[i]
}
}
let i = 0
for (let out of scorecard.querySelectorAll("tfoot output[name='ranking']")) {
out.value = rankedPoints.indexOf(points[i]) + rankOffset
i += 1
}
}
}
function init() {
for (let scorecard of document.querySelectorAll(".scorecard")) {
for (let input of scorecard.querySelectorAll("input")) {
input.addEventListener("input", () => scorecardUpdate(scorecard))
}
scorecardUpdate(scorecard)
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init)
} else {
init()
}

View File

@ -0,0 +1,33 @@
.warning {
color: #e64;
display: none;
}
.warning.visible {
display: initial;
}
.scrolly {
max-width: 100%;
overflow-x: auto;
}
.awardPoints {
display: inline-block;
max-height: 60vh;
overflow-y: auto;
margin: 1em;
}
.awardPoints table {
margin: initial;
}
.awardPoints thead {
position: sticky;
top: 0;
}
.awardPoints tbody {
max-height: 60vh;
overflow-y: auto;
}
.awardPoints td {
text-align: right;
}

View File

@ -0,0 +1,122 @@
let awardPoints = [
100, // 1
75, // 2
65, // 3
60, // 4
56, // 5
53, // 6
50, // 7
47, // 8
45, // 9
43, // 10
41, // 11
39, // 12
38, // 13
37, // 14
36, // 15
35, // 16
34, // 17
33, // 18
32, // 19
31, // 20
30, // 21
29, // 22
28, // 23
27, // 24
26, // 25
25, // 26
24, // 27
23, // 28
22, // 29
21, // 30
20, // 31
19, // 32
18, // 33
17, // 34
16, // 35
15, // 36
14, // 37
13, // 38
12, // 39
11, // 40
10, // 41
9, // 42
8, // 43
7, // 44
6, // 45
5, // 46
4, // 47
3, // 48
2, // 49
1, // 50
0.75, // 51
0.65, // 52
0.60, // 53
0.56, // 54
0.53, // 55
0.50, // 56
0.47, // 57
0.45, // 58
0.43, // 59
0.41, // 60
0.39, // 61
0.38, // 62
0.37, // 63
0.36, // 64
0.35, // 65
0.34, // 66
0.33, // 67
0.32, // 68
0.31, // 69
0.30, // 70
0.29, // 71
0.28, // 72
0.27, // 73
0.26, // 74
0.25, // 75
0.24, // 76
0.23, // 77
0.22, // 78
0.21, // 79
0.20, // 80
0.19, // 81
0.18, // 82
0.17, // 83
0.16, // 84
0.15, // 85
0.14, // 86
0.13, // 87
0.12, // 88
0.11, // 89
0.10, // 90
0.09, // 91
0.08, // 92
0.07, // 93
0.06, // 94
0.05, // 95
0.04, // 96
0.03, // 97
0.02, // 98
0.01, // 99
0.00, // 100
]
function init() {
for (let tbody of document.querySelectorAll(".awardPoints tbody")) {
for (let i = 0; i < awardPoints.length; i++) {
let tr = tbody.appendChild(document.createElement("tr"))
tr.appendChild(document.createElement("td")).textContent = i + 1
tr.appendChild(document.createElement("td")).textContent = awardPoints[i].toFixed(2)
}
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init)
} else {
init()
}
export {
awardPoints,
}

View File

@ -0,0 +1,35 @@
---
title: CLRG Award Points Artifacts
date: 2022-10-10T08:00:00-06:00
tags:
- clrg
stylesheets:
- toys.css
scripts:
- speculator.mjs
---
One quirk of awards points is that for any given overall
score, there are only a handful of possible judge rankings that could have led
to it. That means you can make some guesses about how each judge ranked an
individual dancer, based on only their total award points.
Here's a handy calculator!
It (currently) doesn't consider the possibility of a tie.
<div class="scrolly">
<fieldset class="speculator">
<legend>CLRG Award Points Speculator</legend>
<div>
Points: <input name="points" type="number" min=41 max=10000 value=188>
<input name="adjudicators" type="hidden">
</div>
<table class="results">
<caption>Possible Rankings</caption>
<thead>
<tr class="warning"><th>Computing: this could take a while!</th></tr>
</thead>
<tbody></tbody>
</table>
</fieldset>
</div>

View File

@ -0,0 +1,104 @@
import { awardPoints } from "./awardPoints.mjs"
function arraysEqual(a, b) {
if (a === b) return true;
if (a == null || b == null) return false;
if (a.length != b.length) return false;
for (let i = 0; i < a.length; ++i) {
if (a[i] !== b[i]) return false;
}
return true;
}
function awardPossibilities(total=0, depth=1) {
if (depth == 1) {
if (awardPoints.includes(total)) {
return [[total]]
} else {
return []
}
}
let possibilities = []
for (let p of awardPoints) {
if (p <= total) {
for (let subPossibility of awardPossibilities(total - p, depth - 1)) {
let v = [p].concat(subPossibility)
v.sort((a,b) => b-a)
possibilities.push(v)
}
}
}
possibilities.sort((a,b) => b.reduce((a,b) => a*100+b) - a.reduce((a,b) => a*100+b))
let uniquePossibilities = []
for (let p of possibilities) {
if (uniquePossibilities.length == 0 || !arraysEqual(p, uniquePossibilities[uniquePossibilities.length - 1])) {
uniquePossibilities.push(p)
}
}
return uniquePossibilities
}
function speculate(calc) {
let points = calc.querySelector("[name=points]").value || 0
let adjudicators = calc.querySelector("[name=adjudicators]").value || 3
let results = calc.querySelector(".results tbody")
while (results.firstChild) {
results.removeChild(results.firstChild)
}
for (let warning of calc.querySelectorAll(".warning")) {
if (adjudicators >3) {
warning.classList.add("visible")
setTimeout(() => asyncSpeculate(calc, points, adjudicators), 0)
} else {
warning.classList.remove("visible")
asyncSpeculate(calc, points, adjudicators)
}
}
}
async function asyncSpeculate(calc, points, adjudicators) {
let results = calc.querySelector(".results tbody")
let possibilites = awardPossibilities(points, adjudicators)
if (possibilites.length == 0) {
let row = results.appendChild(document.createElement("tr"))
let cell = row.appendChild(document.createElement("th"))
cell.textContent = "No possible combinations"
} else {
let row = results.appendChild(document.createElement("tr"))
for (let i = 1; i <= adjudicators; ++i) {
let cell = row.appendChild(document.createElement("th"))
cell.textContent = "Adj. " + i
}
for (let possibility of possibilites) {
let row = results.appendChild(document.createElement("tr"))
for (let p of possibility) {
let cell = row.appendChild(document.createElement("td"))
cell.textContent = p
}
}
}
for (let warning of calc.querySelectorAll(".warning")) {
warning.classList.remove("visible")
}
}
function init() {
for (let calc of document.querySelectorAll(".speculator")) {
for (let input of calc.querySelectorAll("input")) {
input.addEventListener("input", () => speculate(calc))
}
speculate(calc)
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init)
} else {
init()
}

View File

@ -0,0 +1,33 @@
.warning {
color: #e64;
display: none;
}
.warning.visible {
display: initial;
}
.scrolly {
max-width: 100%;
overflow-x: auto;
}
.awardPoints {
display: inline-block;
max-height: 60vh;
overflow-y: auto;
margin: 1em;
}
.awardPoints table {
margin: initial;
}
.awardPoints thead {
position: sticky;
top: 0;
}
.awardPoints tbody {
max-height: 60vh;
overflow-y: auto;
}
.awardPoints td {
text-align: right;
}

View File

@ -0,0 +1,2 @@
# I don't have rights to copy any of the data :(
*.xml

View File

@ -0,0 +1,157 @@
---
title: CLRG Results Analysis
date: 2022-10-28T10:45:00-0600
---
# Our Findings
Here's a summary of what the team I've been working with has found:
## It's clearly widespread throughout the organization
It's more than 12 people.
It's more than 24 people.
It's probably more than 48 people.
Every data set we found had some pretty clear weirdness,
and that was just looking at numbers.
Once we tied names back in to weirdness,
we were like, "oh, yeah, we had a feeling this person was up to something."
Unless there are some *major* changes made,
we're still going to have corruption in CLRG.
(Spoiler alert: there will not me major changes made.)
That's just the world you're in with CLRG,
and most other subjectively-judged competitive events.
I hope any new families getting involved understand this:
In order to get into the upper tiers,
the way you compete becomes more about politics than dancing.
And by "politics" I mean corruption.
That's not to say the dancers at the upper levels aren't excellent dancers: to
recall at a national event, you have to be an excellent dancer. But you might
also be an excellent dancer and not recall, because your parents/coach/teacher
aren't playing the corruption game as well as somebody else's
parents/coach/teacher.
## The top 11 places are bizarre
We knew this from [previous analysis](/blog/2022-10-09-CLRG-Scoring.html):
the top 11 places are scored totally differently than places 12-50.
And places 51-100 are placed separately.
Any large event is actually three separate competitions,
and it's very very difficult to break out if any judge places you in one of these categories:
| Placing | Comment |
| ---- | ---- |
| 1st - 11th | Strange exponential points category |
| 12th - 50th | Scoring here works the way you assumed it would |
| 51st - 100th | Everybody's fighting for a fraction of one point |
Please note that this is just my hot take!
You should play with the
[scoring tool](/blog/2022-10-09-CLRG-Scoring.html) I made
to get a feel for how this all works. It's weird!
And it's difficult enough to explain accurately by someone trying to.
I'm not trying to in this section.
# I won't publish any more tools
I started writing a thing to highlight weirdness in CLRG rankings.
You'd give it a ranking sheet,
and it would highlight what weirdness it found,
with an explanation about why it looks weird and what it might mean.
But I gave up after a day's work.
Here's why:
## I don't have the right to copy data
The results of competitions is owned by various companies. It seems to be a
different company depending on who gets the contract to provide the scoring
software for a particular event. In any case, none of them provide a license
that allows me to redistribute their data. That means I can't host any scores on
this web site: you have to get it from the company that owns it.
## The data is distributed as PDF files
Adobe Acrobat (or whatever they call it now) actually has an "export as XML"
function that does a good job turning PDF files back into something like a
spreadsheet.
In order for any tool I make to be generally useful, I would also need to
provide instructions on doing that Acrobat export, probably with an accompanying
video and multiple screen shots. I don't even run Windows or Mac OS, to say
nothing of being notoriously bad at this sort of instructional page / video.
## It's not clear anybody really cares
Reading the "voy forums", it's clear that the main thing people are getting out
of this is righteous indignation. I don't think a post full of math would really
appeal to the people there.
I'm in touch with a couple of reporters covering this story, but I don't think
the math angle is going to be very interesting to their readership either.
That means I'd need to go and try to figure out who *does* care. I found a small
group of people who care, and this group has already loaded some data into a
spreadsheet and done a manual analysis.
After finding mathematical evidence supporting what we already knew (this whole
process is corrupt), what then? I guess I just go on with my life.
I can already just go on with my life, I don't have to put in a bunch of work first.
# Parting thoughts
My kid is a high school senior.
She has a lot of things to look forward to in her immediate future that aren't Irish Dance,
and is winding down her involvement,
so our family is sort of meh about this whole thing.
If she were in elementary or middle school,
I would probably be howling right now and pressing hard to pull her out.
But maybe there's some value to still doing all this,
even though it's corrupt and she's never going to get a top ranking.
She still wants to compete for some reason,
and practicing has helped her develop a work ethic that will help her later.
In addition,
she's made friends through this;
she's learned how to care for others and talk to new people;
she's learned how to teach;
and she's gained a strong sense of self.
Those are all good things that didn't depend on fair judging.
The other day I was talking with a woman who runs an after-school program for
black kids who are interested in science and technology. I mentioned that the
winning papers at the statewide computer science contest never seem to integrate
the social justice aspects she's asking her kids to focus on. We kicked that
idea around a while, and wound up convincing each other that the work is worth
doing even if the judging is biased against it. I think the same thing might be
true here.
# Do you care?
Are you a regular reader of my blog? (HA HA HA) Do you care about mathematical
analysis of this stuff? Are you willing to jump through some technical hoops in
order to look at things without running afoul of copyright law? Get in touch
with me and let me know there's actually an audience!
All of the code I wrote is checked in to git for this blog page.
So you don't even need to contact me,
you can just take the scraping code and go nuts.
It uses a standard API for scraped data from two different sources,
does some smarts to determine missing data,
and should be pretty simple to interface with.
If you need help getting the XML data into it,
I'd be glad to help you with that.
Here are the files:
* [Feisworx report scraping code](feisworx.mjs)
* [Feis Results report scraping code](feisresults.mjs)
* [Code to guess placing given award points, used by feisresults.mjs](awardpoints.mjs)
* [JSDoc documentation of some global data structures](types.mjs)
* [Some stub code to populate an HTML page with data](dataset.mjs)

View File

@ -0,0 +1,25 @@
---
title: Unfinished CLRG Data Analyzer
stylesheets:
- dataset.css
scripts:
- dataset.mjs
---
<p>
This won't work because you don't have the datasets.
I can't provide them to you, due to copyright laws.
But if you get the results PDFs,
load them up in Adobe Acrobat,
and save them as XML,
they might load here :)
</p>
<h1>2021 Irish Dance North Americans 21A</h1>
<div class="clrg-dataset" data-url="2022-10-10 2021 Irish dance north Americans 21A.xml"></div>
<h1>2017 11 AB Wro</h1>
<div class="clrg-dataset" data-url="2017-11 AB Wro.xml"></div>
<h1>2019 09 Wro</h1>
<div class="clrg-dataset" data-url="2022-10-10 Wro2019-09.xml"></div>

View File

@ -0,0 +1,131 @@
let awardPoints = [
100, // 1
75, // 2
65, // 3
60, // 4
56, // 5
53, // 6
50, // 7
47, // 8
45, // 9
43, // 10
41, // 11
39, // 12
38, // 13
37, // 14
36, // 15
35, // 16
34, // 17
33, // 18
32, // 19
31, // 20
30, // 21
29, // 22
28, // 23
27, // 24
26, // 25
25, // 26
24, // 27
23, // 28
22, // 29
21, // 30
20, // 31
19, // 32
18, // 33
17, // 34
16, // 35
15, // 36
14, // 37
13, // 38
12, // 39
11, // 40
10, // 41
9, // 42
8, // 43
7, // 44
6, // 45
5, // 46
4, // 47
3, // 48
2, // 49
1, // 50
0.75, // 51
0.65, // 52
0.60, // 53
0.56, // 54
0.53, // 55
0.50, // 56
0.47, // 57
0.45, // 58
0.43, // 59
0.41, // 60
0.39, // 61
0.38, // 62
0.37, // 63
0.36, // 64
0.35, // 65
0.34, // 66
0.33, // 67
0.32, // 68
0.31, // 69
0.30, // 70
0.29, // 71
0.28, // 72
0.27, // 73
0.26, // 74
0.25, // 75
0.24, // 76
0.23, // 77
0.22, // 78
0.21, // 79
0.20, // 80
0.19, // 81
0.18, // 82
0.17, // 83
0.16, // 84
0.15, // 85
0.14, // 86
0.13, // 87
0.12, // 88
0.11, // 89
0.10, // 90
0.09, // 91
0.08, // 92
0.07, // 93
0.06, // 94
0.05, // 95
0.04, // 96
0.03, // 97
0.02, // 98
0.01, // 99
0.00, // 100
]
/**
* Given a score, calculate what placings could have gotten it.
*
* @param {Number} score Score we're going to guess
* @param {Number} tied Highest number n-way tie to consider
* @returns {Array.<Number>} List of possible placings
*/
function guessPlacing(score, tied=3) {
let placings = []
for (let t = tied; t > 0; t--) {
let totalPoints = score * t
for (let placing = 0; placing < awardPoints.length - t; placing++) {
let acc = 0
for (let i = 0; i < t; i++) {
acc += awardPoints[placing+i]
}
if (acc == totalPoints) {
placings.push(placing+1)
}
}
}
return placings
}
export {
awardPoints,
guessPlacing,
}

View File

@ -0,0 +1,12 @@
.clrg-dataset {
max-width: 100%;
overflow-x: auto;
}
.clrg-dataset tbody td.new-adjudication {
border-left: thin solid black;
}
.clrg-dataset tbody td.new-round {
border-left: thick solid black;
}

View File

@ -0,0 +1,180 @@
/**
* Feis Dataset Importer
*/
import * as FeisWorx from "./feisworx.mjs"
import * as FeisResults from "./feisresults.mjs"
/**
* @typedef {import("./types.mjs").Results} Results
* @typedef {import("./types.mjs").Result} Result
* @typedef {import("./types.mjs").Round} Round
* @typedef {import("./types.mjs").Adjudication} Adjudication
* @typedef {Array.<Array.<String>>} RawData
*/
/**
* Creates a new element and appends it to parent
*
* @param {Element} parent Element to append to
* @param {String} type Type of element to create
* @param {Object} [dataset] Data fields to set
* @returns {Element}
*/
function newElement(parent, type, dataset={}) {
let child = parent.appendChild(document.createElement(type))
for (let k in dataset) {
child.dataset[k] = dataset[k]
}
return child
}
/**
* Load a file and parse it into Results.
*
* @param {URL|String} url Location of file to load
* @returns {Results} Parsed results
*/
async function loadData(url) {
let resp = await fetch(url)
let contentType = resp.headers.get("Content-Type")
if (! contentType.includes("/xml")) {
console.error(`Cannot load data with content-type ${contentType}`)
return
}
let text = await resp.text()
let doc = new DOMParser().parseFromString(text, "text/xml")
let rawData = parseXMLDocument(doc)
return parseRawData(rawData)
}
/**
* Parse an XML document of feis results into a 2D array of strings
*
* @param {Document} doc XML Document
* @returns {RawData} Raw data
*/
function parseXMLDocument(doc) {
let table = doc.querySelector("Table")
let rawData = []
for (let dataRow of table.children) {
if (! ["tr"].includes(dataRow.tagName.toLowerCase())) {
console.warn(`Warning: unexpected XML tag ${dataRow.tagName}, expecting tr`)
continue
}
let row = []
for (let dataCell of dataRow.children) {
if (! ["th", "td"].includes(dataCell.tagName.toLowerCase())) {
console.warn(`Warning: unexpected XML tag ${dataRow.tagName}, expecting th/td`)
continue
}
row.push(dataCell.textContent)
}
rawData.push(row)
}
return rawData
}
/**
* Parse raw data into a list of adjudicators and results
*
* @param {RawData} rawData Raw data
* @returns {Results} Parsed Results
*/
function parseRawData(rawData) {
let firstRow = rawData[0]
if (firstRow[0].trim().toLowerCase() == "place awd pts") {
return FeisWorx.parse(rawData)
}
if (firstRow[firstRow.length-1].trim().toLowerCase() == "total ip *") {
return FeisResults.parse(rawData)
}
console.error("First row doesn't resemble anything I can cope with", firstRow)
}
/**
*
* Fills a table element with some results
*
* @param {Element} table Table to fill in
* @param {Results} results Results to fill with
*/
function fillTable(table, results) {
let head = newElement(table, "thead")
let row0 = newElement(head, "tr")
let row1 = newElement(head, "tr")
let row2 = newElement(head, "tr")
newElement(row0, "th").colSpan = 2
newElement(row1, "th").colSpan = 2
newElement(row2, "th").textContent = "Name"
newElement(row2, "th").textContent = "Rank"
let roundNumber = 0
for (let round of results[0].rounds) {
let roundCell = newElement(row0, "th")
roundCell.textContent = `Round ${++roundNumber}`
roundCell.colSpan = 3*round.length
for (let adjudication of round) {
let adjudicator = adjudication.adjudicator
let cell = newElement(row1, "th")
cell.textContent = adjudicator
cell.colSpan = 3
newElement(row2, "th").textContent = "Raw"
newElement(row2, "th").textContent = "Placing"
newElement(row2, "th").textContent = "Points"
}
}
let body = newElement(table, "tbody")
for (let result of results) {
let row = newElement(body, "tr")
newElement(row, "th").textContent = result.name
newElement(row, "th").textContent = result.overallRank
let i = 0
for (let round of result.rounds) {
let first = true
for (let adjudication of round) {
let raw = newElement(row, "td")
raw.textContent = adjudication.raw
raw.classList.add("new-adjudication")
if (first) {
raw.classList.add("new-round")
first = false
}
newElement(row, "td").textContent = adjudication.placing
newElement(row, "td").textContent = adjudication.points
i++
}
}
}
}
async function init() {
for (let div of document.querySelectorAll(".clrg-dataset")) {
let results = await loadData(div.dataset.url)
let table = newElement(div, "table")
fillTable(table, results)
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init)
} else {
init()
}
export {
loadData,
parseXMLDocument,
parseRawData,
}

View File

@ -0,0 +1,139 @@
/**
* Feisresults.com parser
*
*/
import {awardPoints, guessPlacing} from "./awardPoints.mjs"
/**
* @typedef {import("./types.mjs").Results} Results
* @typedef {import("./types.mjs").Result} Result
* @typedef {import("./types.mjs").Round} Round
* @typedef {import("./types.mjs").Adjudication} Adjudication
*/
/**
* Parse feisresults data
*
* @param {Array.<Array.<String>>} rawData Raw data
* @returns {Results}
*/
function parse(rawData) {
/** @type {Results} */
let results = []
let adjudicators = []
let numRounds = 0
let adjudicatorsPerRound = 0
let possibleTiesByAdjudicatorRound = {}
for (let rowIndex = 0; rowIndex < rawData.length; rowIndex++) {
let cells = rawData[rowIndex]
// Is it a page heading?
if ((cells[0].trim().toLowerCase() == "card")) {
continue
}
// Is it a list of adjudicators?
if (cells[cells.length-1].trim().toLowerCase() == "total ip *") {
cells.splice(cells.length-1, 1) // -1: total IP *
cells.splice(0, 5) // 0 - 4: blank
adjudicators = []
for (let cell of cells) {
cell = cell.trim()
if (cell.toLowerCase().includes("rounds 1")) {
// skip it
} else if (cell.toLowerCase().includes("round total")) {
numRounds++
} else {
adjudicators.push(cell)
}
}
adjudicatorsPerRound = adjudicators.length / numRounds
if (! Number.isSafeInteger(adjudicatorsPerRound)) {
console.error(`Irrational number of adjudicators for number of rounds: (${adjudicators.length}/${numRounds})`)
}
continue
}
let row = {}
row.number = Number(cells[0])
// cells[1]: Position at recall
row.overallRank = Number(cells[2])
{
let parts = cells[3].trim().split(/\s:\s/)
console.log(parts, cells[3])
let nameSchool = parts[0]
// parts[1]: region
// We're going to take a wild-ass guess here that the dancer only has two names
let subparts = nameSchool.split(/\s+/)
row.name = subparts.slice(0, 2).join(" ")
row.school = subparts.slice(2).join(" ")
}
row.qualifier = cells[4].trim()
/** @type {Round} */
let round = []
/** @type {Array.<Round>} */
row.rounds = []
let adjudicatorNumber = 0
for (let cellIndex = 5; cellIndex < cells.length; cellIndex++) {
let cell = cells[cellIndex].trim()
if (! cell.includes("/")) {
continue
}
/** @type {Adjudication} */
let adjudication = {}
adjudication.adjudicator = adjudicators[adjudicatorNumber++]
let parts = cell.split("/")
adjudication.raw = Number(parts[0])
adjudication.points = Number(parts[1])
adjudication.placing = guessPlacing(adjudication.points)
// Guidebook reports don't list every dancer: we'll guess placing later
round.push(adjudication)
if (round.length == adjudicatorsPerRound) {
row.rounds.push(round)
round = []
}
}
results.push(row)
}
disambiguatePlacings(results, numRounds, adjudicatorsPerRound)
return results
}
function disambiguatePlacings(results, numRounds, adjudicatorsPerRound) {
for (let roundNumber = 0; roundNumber < numRounds; roundNumber++) {
/**
* A list of raw score, award points, and placing
*
* @type {Array.<Adjudication>}
*/
for (let judgeNumber = 0; judgeNumber < adjudicatorsPerRound; judgeNumber++) {
let scores = []
for (let result of results) {
scores.push(result.rounds[roundNumber][judgeNumber])
}
scores.sort((a,b) => b.raw - a.raw)
let greatestPlacing = 0
for (let adjudication of scores) {
let possibilities = guessPlacing(adjudication.points)
possibilities.sort((a,b) => b-a)
// XXX: eliminate possibilities less than greatestPlacing, then pick the largest
}
console.log(scores)
}
}
}
export {
parse,
}

View File

@ -0,0 +1,153 @@
/**
* FeisWorx parser
*
* This is the output of Adobe Reader saving the PDF as XML.
*/
/**
* @typedef {import("./types.mjs").Results} Results
* @typedef {import("./types.mjs").Result} Result
* @typedef {import("./types.mjs").Round} Round
* @typedef {import("./types.mjs").Adjudication} Adjudication
*/
/**
* Parse FeisWorx data
*
* @param {Array.<Array.<String>>} rawData Raw data
* @returns {Results}
*/
function parse(rawData) {
/** @type {Results} */
let results = []
let adjudicators = []
let numRounds = 0
let adjudicatorsPerRound = 0
for (let rowIndex = 0; rowIndex < rawData.length; rowIndex++) {
let cells = rawData[rowIndex]
// Is it a page heading?
if ((cells.length >= 11) && (cells[0].trim().toLowerCase().startsWith("place"))) {
if (numRounds == 0) {
for (let cell of cells) {
if (cell.toLowerCase().startsWith("round")) {
numRounds++
}
}
}
continue
}
if (adjudicators.length == 0) {
let fishy = false
for (let adjudicator of cells) {
if (Number(adjudicator) > 0) {
fishy = true
}
adjudicators.push(adjudicator.trim())
}
if (fishy) {
console.warn("Adjudicators row doesn't look right", cells)
}
adjudicatorsPerRound = adjudicators.length / numRounds
if (! Number.isSafeInteger(adjudicatorsPerRound)) {
console.error(`Irrational number of adjudicators for number of rounds: (${adjudicators.length}/${numRounds})`)
}
continue
}
// Is this just a list of adjudicators again?
if (cells.length >= adjudicators.length) {
let lenDiff = cells.length - adjudicators.length
let same = true
for (let i = adjudicators.length-1; i >= 0; i--) {
if (adjudicators[i] != cells[i+lenDiff].trim()) {
same = false
break
}
}
if (same) {
continue
}
}
let row = {}
{
let parts = cells[0].trim().split(/\s+/)
row.overallRank = Number(parts[0])
row.overallPoints = Number(parts[1])
}
{
let match = cells[1].trim().match(/(\d+) - (.+) \((.+) *\)[ -]*(.+)?/)
if (match) {
row.number = Number(match[1])
row.name = match[2]
row.school = match[3]
row.qualifier = match[4]
}
}
/** @type {Round} */
let round = []
/** @type {Array.<Round>} */
row.rounds = []
for (let cellIndex = 2; cellIndex < cells.length; cellIndex++) {
let cell = cells[cellIndex]
/** @type {Adjudication} */
let adjudication = {}
let parts = cell.trim().split(/ - ?|\s/)
adjudication.adjudicator = adjudicators[cellIndex - 2]
if ((parts.length == 5) && (parts[3] == "AP")) {
parts.splice(3, 0, "NaN")
}
if ((parts.length == 7) && (parts[4] == "T")) {
adjudication.tie = true
parts.splice(4, 1)
} else {
adjudication.tie = false
}
if (parts.length != 6) {
console.error(`Wrong number of fields in row ${rowIndex} cell ${cellIndex}:`, parts, cells)
break
}
for (let i = 0; i < parts.length; i += 2) {
let key = parts[i]
let val = Number(parts[i+1])
switch (key) {
case "Raw":
adjudication.raw = val
break
case "Plc":
adjudication.placing = val
break
case "AP":
adjudication.points = val
break
default:
console.error(`Unknown key ${key} in row ${rowIndex} cell ${cellIndex}:`, cell)
break
}
}
round.push(adjudication)
if (round.length == adjudicatorsPerRound) {
row.rounds.push(round)
round = []
}
}
results.push(row)
}
return results
}
export {
parse
}

View File

@ -0,0 +1,34 @@
/**
* A collection of results
* @typedef {Array.<Result>} Results
*/
/**
* A single result
*
* @typedef {Object} Result
* @property {String} name Competitor's name
* @property {Number} number Competitor's bib number
* @property {String} school Competitor's school
* @property {Number} overallPoints Overall award points for this competitor
* @property {Number} overallRank Overall ranking for this competitor
* @property {String} qualifier Any qualifiers this ranking earned
* @property {Array.<Round>} rounds How this competitor was judged in each round
*/
/**
* The results for one dancer for one round
*
* @typedef Round
* @type {Array.<Adjudication>}
*/
/**
* One adjudicator's results for one dancer for one round
* @typedef {Object} Adjudication
* @property {String} adjudicator Adjudicator who recorded this score
* @property {Number} raw Raw score
* @property {Number} placing Placing relative to this adjudicator's other scores
* @property {Number} points Award points
* @property {Boolean} tie Whether this score was a tie
*/

View File

@ -0,0 +1,38 @@
---
title: My technological flag in the sand
date: 2022-10-28T14:59:00-0600
---
Elon Musk just bought Twitter. It got me thinking about Twitter, something that
comes up a lot, because a lot happens there in 2022, and some of that filters
through to me.
I've long known that the day would come when I would just not be willing to
accept some sort of societal change, and this would be what defined me as an old
person. This isn't some unique thing I'd be doing: this is a time-honored human
trait. Previous generations have put their flags in the sand by rejecting polyphonic
sacred music, rejecting automobiles, rejecting email, and rejecting smartphones.
Today I realized what my flag in the sand is going to be. I am not going to
become active on Twitter, or Instagram, or Facebook, or Myspace, or LiveJournal.
I see no personal benefit to adopting the newer technologies, and I see a lot of
personal benefits to avoiding them.
I'm not doing this because I think it's just a fad that humanity will move past.
No: now that we have it, and are beginning to realize how it actually works,
we're having to figure out how to use it responsibly as a species. This is a
pretty familiar story for any new technology. I'm just deciding I'm not
interested in being a part of that process.
At some point, some related technology will come along and sweep me up with it.
It will be awkward for me, and young people will recognize that I'm just no good
at it. They'll wonder how any human being could be so inept at something so
obvious. It will become a mental shortcut for the elderly that they are awkward
with this technology, and the elderly will discuss how pointless it is and how
they just can't understand why anyone needs it. We'll watch with some degree of
sadness as our way of life dies with us.
When you hear writers and philosophers talking about how limited lifespans are a
gift to humanity, they're talking about this sort of thing.
I hope y'all have a nice time :)

View File

@ -0,0 +1,40 @@
---
title: Smart Watches
date: 2022-11-28
---
My employer has this add-on to the health plan where,
if you wear a pedometer,
you get money in your health spending account.
If you also provide sleep tracking,
you get even more money.
It doesn't seem to care (currently) about your pulse.
I don't have any huge issue with this plan.
But I'd like to, as much as possible,
have this not be at the foreground of my day-to-day life.
After a lot of thinking,
I wound up realizing that the main consideration for me was battery life!
Believe it or not, all the other doodads and gizmos weren't actually that important.
So my requirements list is basically:
1. Tells me the time
2. Can go for a long time without needing to be recharged
3. Counts my steps and automatically puts them in this health plan thing
4. Also tell the health plan how long I sleep
I wound up going back to a watch I had a few years ago,
which has a month-long battery life between recharges.
If you turn off the thing where it vibrates when you get a phone call,
I think it will go even longer.
It also has the advantage of telling me the time the old-fashioned way:
with hands.
So I can just glance at it and see what time it is,
without needing to flick my wrist or get into a dark enough area or whatever.
I started writing this article in November,
and I'm finishing it in January.
I've been wearing that watch for a couple months now and it's no big deal.
It feels kind of silly writing about it.
But this is something a lot of people spend time and money thinking about in 2022!

View File

@ -0,0 +1,78 @@
---
title: Iceland trip
date: 2023-01-06
---
We went to Iceland for two weeks over the winter break.
I couldn't think of anything I wanted to write in this blog about it,
but someone on somethingawful just asked me for some tips,
and my reply seems kinda interesting,
so here it is:
---
> Hey, really random, but saw you mention in the EV thread you just got back
> from Iceland, got any winter travel tips? I'm going in a few weeks, and I
> think pretty much everything I've read is focused on visiting in the summer.
I overpacked because I wasn't sure what to expect, but I didn't overpack by
much. I brought:
* A pair of insulated water-resistant pants (lined with fleece I think, sort of
like ski pants). Wore them everywhere.
* Two pairs of wool socks
* Waterproof boots: mine were Danner, my wife had Sorel
* two long-sleeved wool t-shirts
* two wool sweaters
* A waterproof parka with a hood
* A wool hat
* Warm waterproof gloves with the thing so you can still use the phone
touchscreen (this was so great to have)
I had planned to buy one of those Icelandic wool sweaters when I landed, which I
did, so I could have done without the second sweater I brought. The Icelandic
one was crazy warm: I found myself stripping off the coat and sometimes the
sweater while driving, to prevent overheating.
We only did little walks, like a short one to take a photo of Sólheimjökull, a
little one through Þingvellir. And lots of walking through snow in Reykjavik and
other rural places we stayed. The stuff we packed was totally adequate. I did
use the coat's hood way more than I thought I would, and was glad to have it. El
Cheapo crampons like YakTrax would have been a good idea for Þingvellir if we'd
wanted to go further, but once we saw the sign saying "if you get stuck past
here in the winter, we won't come help you", we turned around and found another
less-icy path. We weren't prepared for glacier climbing on a nature trail.
We traveled with another family, who had the smart idea to buy food at the
grocery store and prepare meals. That saved us a ton of money and I would highly
recommend doing it. Eating out was incredibly expensive. Groceries were also
expensive but more manageable.
For driving, just know your limits. They have these pullouts on the roads,
marked with a sign and an indication of how long the pullout is. If you are even
idly considering turning around, take the pullout and have a nice calm think
through that. I wound up seeing one, thinking "hmm, maybe I should turn around",
and then doing a K-turn on the freaking highway during a whiteout blizzard.
Nothing bad happened, thank goodness, but it could have gone very badly for me,
and I wished I had just taken a couple pullouts to discuss with my wife whether
we wanted to keep going. So take those pullouts.
Snow on the road is unlike anything I've ever witnessed in the Rocky Mountains,
and when they say "difficult driving conditions" in safetravel.is, pay
attention. It's not "if you're from California you'll have a hard time with
this", it's "if you're from any other country". We quickly learned to respect
their suggestions.
Lastly, the N1 fuel pumps wanted a PIN on my credit card, which I didn't have. I
think I could have used an ATM card, but instead we just asked the other family
to buy fuel for us, and we paid them back when we got home. Maybe bring an ATM
card and know the PIN! There were other gas stations where I could use Google
Pay on my phone, or Visa's tap-pay thing, with no problems. And inside the N1, I
could also use Google Pay. It was really just the pumps at the N1 stations.
Incidentally, you should pop into an N1 if you pass by one. It's a real trip.
You can even plan to have a meal there: I saw hot bars, salad bars, frozen
yogurt bars, nice custom sandwich things...
Overall the trip was great! We especially liked the westernfjörd area. The
south, near Sólheimjökull, was super touristy and I wouldn't go there on a
subsequent trip. I'm glad we got to go, and I hope you have a great time!

View File

@ -0,0 +1,25 @@
---
date: 2023-01-07
title: The Yurt
---
One day,
back in the late 1990s,
I discovered that the content filter appliance at work
had blocked woozle.org
for hosting porn.
Because I worked in the group that ran that filter,
I looked into what triggered this block.
I was pretty sure I didn't have any porn.
The offending image was called `yurt.png`.
It was a cartoon drawing of a yurt.
I told the filter it was a "false positive":
it wasn't actually porn.
That got the entire site unblocked.
I don't know why I put that image on my web server,
but the yurt has stayed on woozle.org ever since.
![A cartoon yurt](/assets/images/yurt.png)

View File

@ -2,3 +2,15 @@
title: Blog
url: blog/
---
Blog is short for "web log".
It's a sort of online journal,
that anyone who finds it can read.
Mostly when I write here,
I'm thinking the reader will either be someone looking me up online who wants to know more about me,
or someone in the future who wants to know what life was like during my time.
I don't have a lot of personal anecdotes,
or commentary on current events.
Mostly this is just hobbies and idle thoughts.

View File

@ -5,13 +5,28 @@
<meta name="viewport" content="width=device-width">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:regular,bold,italic">
<!-- My stuff -->
<link rel="stylesheet" media="screen" href="{{"/assets/css/default.css" | relURL}}">
<link rel="icon" type="image/png" href="{{"/assets/images/face.png" | relURL}}">
<link rel="stylesheet" media="screen" href="{{"/assets/css/default.css" | relURL}}">
{{range .AlternativeOutputFormats}}
<link rel="{{.Rel}}" type="{{.MediaType.Type}}" title="{{$.Site.Title}}" href="{{.Permalink}}">
{{end}}
{{range .Params.stylesheets}}
{{$url := .}}
{{with $.Page.Resources.GetMatch .}}
{{$url = .RelPermalink}}
{{end}}
<link rel="stylesheet" media="screen" href="{{$url}}">
{{end}}
{{range .Params.scripts}}
<script src="{{.}}"></script>
{{end}}
{{range .Params.scripts}}
{{$url := .}}
{{with $.Page.Resources.GetMatch .}}
{{$url = .RelPermalink}}
{{end}}
<script src="{{$url}}"
{{- if strings.HasSuffix . ".mjs"}}type="module"{{end -}}
></script>
{{end}}
{{range .Params.headers}}
{{. | safeHTML}}

View File

@ -0,0 +1,4 @@
<figure>
{{- $img := $.Page.Resources.GetMatch (.Get "src")}}
<img src="{{$img.RelPermalink}}" alt="{{.Get "alt"}}">
</figure>

View File

@ -1,4 +1,4 @@
<video autoplay muted loop>
<source src="{{.Get 0}}">
{{.Get 1}}
<source src="{{($.Page.Resources.GetMatch (.Get "src")).RelPermalink}}">
{{.Get "text"}}
</video>

5
run.sh
View File

@ -4,6 +4,9 @@ case "$(hostname)" in
sweetums)
baseURL=http://sweetums.lan:1313/
;;
penguin)
baseURL=http://penguin.linux.test:1313/
;;
*)
baseURL=http://$(hostname --fqdn):1313/
;;
@ -15,4 +18,6 @@ docker run \
-u $(id -u):$(id -g) \
-p 1313:1313 \
klakegg/hugo:ext server \
--buildFuture \
--buildDrafts \
--baseURL "$baseURL"

View File

@ -19,12 +19,13 @@ body {
}
input {
font-family: "Lato", "Roboto", sans-serif;
font-size: 13pt;
border: 0;
outline: 0;
background: transparent;
border-bottom: 1px solid black;
font-family: inherit;
font-size: inherit;
border: thin solid #ccc;
}
input:read-only {
color: #444;
background-color: #eee;
}
.title, td.main {
@ -146,6 +147,36 @@ button.big {
background: inherit;
}
legend {
background-color: #e0e4cc;
}
table {
margin: 1em 0;
border-collapse: collapse;
}
thead, tfoot {
color: #e64;
background-color: rgba(224, 228, 204, 0.8);
}
tbody tr:nth-of-type(even) {
background-color: rgba(238, 102, 68, 0.05);
}
td, th {
padding: 0.2em 0.5em;
}
caption {
caption-side: bottom;
font-size: small;
}
.justify-right, input[type="number"] {
text-align: right;
}
.justify-left {
text-align: left;
}
.tags {
font-size: small;
}
@ -159,6 +190,10 @@ button.big {
img, video {
max-width: 60%;
}
figure img {
max-width: 100%;
}
}
@media (prefers-color-scheme: dark) {
html {