diff --git a/content/blog/2021-12-23-drwho-S01E27-mexico/index.md b/content/blog/2021-12-23-drwho-S01E27-mexico/index.md
index 591972c..41b05e5 100644
--- a/content/blog/2021-12-23-drwho-S01E27-mexico/index.md
+++ b/content/blog/2021-12-23-drwho-S01E27-mexico/index.md
@@ -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
diff --git a/content/blog/2022-09-06-truck-bling/index.md b/content/blog/2022-09-06-truck-bling/index.md
index 701f5cf..38ab566 100644
--- a/content/blog/2022-09-06-truck-bling/index.md
+++ b/content/blog/2022-09-06-truck-bling/index.md
@@ -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,
diff --git a/content/blog/2022-10-04-CLRG-cheating.md b/content/blog/2022-10-04-CLRG-cheating.md
index c72b9a2..dfbfa32 100644
--- a/content/blog/2022-10-04-CLRG-cheating.md
+++ b/content/blog/2022-10-04-CLRG-cheating.md
@@ -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,
diff --git a/content/blog/2022-10-09-CLRG-Scoring/awardPoints.mjs b/content/blog/2022-10-09-CLRG-Scoring/awardPoints.mjs
new file mode 100644
index 0000000..40a3372
--- /dev/null
+++ b/content/blog/2022-10-09-CLRG-Scoring/awardPoints.mjs
@@ -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,
+}
diff --git a/content/blog/2022-10-09-CLRG-Scoring/chart.png b/content/blog/2022-10-09-CLRG-Scoring/chart.png
new file mode 100644
index 0000000..df2e4ef
Binary files /dev/null and b/content/blog/2022-10-09-CLRG-Scoring/chart.png differ
diff --git a/content/blog/2022-10-09-CLRG-Scoring/index.md b/content/blog/2022-10-09-CLRG-Scoring/index.md
new file mode 100644
index 0000000..4e3ebc8
--- /dev/null
+++ b/content/blog/2022-10-09-CLRG-Scoring/index.md
@@ -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:
+
+
+
+
+
+
Ranking
Award Points
+
+
+
+
+
+
+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.
+
+{{
}}
+
+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.
+
+
+
+
+
+
Alice
+
Bob
+
Carol
+
+
+
+
+
Adj. 1
+
+
+
+
+
+
Adj. 2
+
+
+
+
+
+
Adj. 3
+
+
+
+
+
+
+
+
Award Points
+
+
+
+
+
+
Ranking
+
+
+
+
+
+
+
+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.
+
+
+
+
+
+
Alice
+
Bob
+
Carol
+
+
+
+
+
Adj. 1
+
+
+
+
+
+
Adj. 2
+
+
+
+
+
+
Adj. 3
+
+
+
+
+
+
+
+
Award Points
+
+
+
+
+
+
Ranking
+
+
+
+
+
+
+
+
+### 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.
+
+
+
+
+
+
+
Alice
+
Bob
+
Carol
+
Dave
+
Erin
+
+
+
+
+
Adj. 1
+
+
+
+
+
+
+
+
Adj. 2
+
+
+
+
+
+
+
+
Adj. 3
+
+
+
+
+
+
+
+
+
+
Award Points
+
+
+
+
+
+
+
+
Ranking
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/content/blog/2022-10-09-CLRG-Scoring/scorecard.mjs b/content/blog/2022-10-09-CLRG-Scoring/scorecard.mjs
new file mode 100644
index 0000000..cdacf31
--- /dev/null
+++ b/content/blog/2022-10-09-CLRG-Scoring/scorecard.mjs
@@ -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()
+}
+
\ No newline at end of file
diff --git a/content/blog/2022-10-09-CLRG-Scoring/toys.css b/content/blog/2022-10-09-CLRG-Scoring/toys.css
new file mode 100644
index 0000000..7cf3861
--- /dev/null
+++ b/content/blog/2022-10-09-CLRG-Scoring/toys.css
@@ -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;
+}
\ No newline at end of file
diff --git a/content/blog/2022-10-10-CLRG-Scoring-Artifacts/awardPoints.mjs b/content/blog/2022-10-10-CLRG-Scoring-Artifacts/awardPoints.mjs
new file mode 100644
index 0000000..40a3372
--- /dev/null
+++ b/content/blog/2022-10-10-CLRG-Scoring-Artifacts/awardPoints.mjs
@@ -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,
+}
diff --git a/content/blog/2022-10-10-CLRG-Scoring-Artifacts/index.md b/content/blog/2022-10-10-CLRG-Scoring-Artifacts/index.md
new file mode 100644
index 0000000..52da72a
--- /dev/null
+++ b/content/blog/2022-10-10-CLRG-Scoring-Artifacts/index.md
@@ -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.
+
+
+
+
diff --git a/content/blog/2022-10-10-CLRG-Scoring-Artifacts/speculator.mjs b/content/blog/2022-10-10-CLRG-Scoring-Artifacts/speculator.mjs
new file mode 100644
index 0000000..e9eaca1
--- /dev/null
+++ b/content/blog/2022-10-10-CLRG-Scoring-Artifacts/speculator.mjs
@@ -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()
+}
+
\ No newline at end of file
diff --git a/content/blog/2022-10-10-CLRG-Scoring-Artifacts/toys.css b/content/blog/2022-10-10-CLRG-Scoring-Artifacts/toys.css
new file mode 100644
index 0000000..7cf3861
--- /dev/null
+++ b/content/blog/2022-10-10-CLRG-Scoring-Artifacts/toys.css
@@ -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;
+}
\ No newline at end of file
diff --git a/content/blog/2022-10-28-CLRG-Results-Analysis/.gitignore b/content/blog/2022-10-28-CLRG-Results-Analysis/.gitignore
new file mode 100644
index 0000000..2292010
--- /dev/null
+++ b/content/blog/2022-10-28-CLRG-Results-Analysis/.gitignore
@@ -0,0 +1,2 @@
+# I don't have rights to copy any of the data :(
+*.xml
diff --git a/content/blog/2022-10-28-CLRG-Results-Analysis/_index.md b/content/blog/2022-10-28-CLRG-Results-Analysis/_index.md
new file mode 100644
index 0000000..4db901e
--- /dev/null
+++ b/content/blog/2022-10-28-CLRG-Results-Analysis/_index.md
@@ -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)
diff --git a/content/blog/2022-10-28-CLRG-Results-Analysis/analyzer.html b/content/blog/2022-10-28-CLRG-Results-Analysis/analyzer.html
new file mode 100644
index 0000000..afeb39f
--- /dev/null
+++ b/content/blog/2022-10-28-CLRG-Results-Analysis/analyzer.html
@@ -0,0 +1,25 @@
+---
+title: Unfinished CLRG Data Analyzer
+stylesheets:
+ - dataset.css
+scripts:
+ - dataset.mjs
+---
+
+
+ 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 :)
+
+
+
2021 Irish Dance North Americans 21A
+
+
+
2017 11 AB Wro
+
+
+
2019 09 Wro
+
diff --git a/content/blog/2022-10-28-CLRG-Results-Analysis/awardPoints.mjs b/content/blog/2022-10-28-CLRG-Results-Analysis/awardPoints.mjs
new file mode 100644
index 0000000..8d54c87
--- /dev/null
+++ b/content/blog/2022-10-28-CLRG-Results-Analysis/awardPoints.mjs
@@ -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.} 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,
+}
diff --git a/content/blog/2022-10-28-CLRG-Results-Analysis/dataset.css b/content/blog/2022-10-28-CLRG-Results-Analysis/dataset.css
new file mode 100644
index 0000000..33d83cf
--- /dev/null
+++ b/content/blog/2022-10-28-CLRG-Results-Analysis/dataset.css
@@ -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;
+}
diff --git a/content/blog/2022-10-28-CLRG-Results-Analysis/dataset.mjs b/content/blog/2022-10-28-CLRG-Results-Analysis/dataset.mjs
new file mode 100644
index 0000000..d1bdba3
--- /dev/null
+++ b/content/blog/2022-10-28-CLRG-Results-Analysis/dataset.mjs
@@ -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.>} 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,
+}
diff --git a/content/blog/2022-10-28-CLRG-Results-Analysis/feisresults.mjs b/content/blog/2022-10-28-CLRG-Results-Analysis/feisresults.mjs
new file mode 100644
index 0000000..f1132b6
--- /dev/null
+++ b/content/blog/2022-10-28-CLRG-Results-Analysis/feisresults.mjs
@@ -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.>} 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.} */
+ 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.}
+ */
+ 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,
+ }
+
\ No newline at end of file
diff --git a/content/blog/2022-10-28-CLRG-Results-Analysis/feisworx.mjs b/content/blog/2022-10-28-CLRG-Results-Analysis/feisworx.mjs
new file mode 100644
index 0000000..bbbde98
--- /dev/null
+++ b/content/blog/2022-10-28-CLRG-Results-Analysis/feisworx.mjs
@@ -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.>} 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.} */
+ 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
+}
diff --git a/content/blog/2022-10-28-CLRG-Results-Analysis/types.mjs b/content/blog/2022-10-28-CLRG-Results-Analysis/types.mjs
new file mode 100644
index 0000000..32dff1f
--- /dev/null
+++ b/content/blog/2022-10-28-CLRG-Results-Analysis/types.mjs
@@ -0,0 +1,34 @@
+/**
+ * A collection of results
+ * @typedef {Array.} 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.} rounds How this competitor was judged in each round
+ */
+
+/**
+ * The results for one dancer for one round
+ *
+ * @typedef Round
+ * @type {Array.}
+ */
+
+/**
+ * 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
+ */
diff --git a/content/blog/2022-10-28-curmudgeon.md b/content/blog/2022-10-28-curmudgeon.md
new file mode 100644
index 0000000..ee07b28
--- /dev/null
+++ b/content/blog/2022-10-28-curmudgeon.md
@@ -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 :)
diff --git a/content/blog/2022-11-28-smartwatch.md b/content/blog/2022-11-28-smartwatch.md
new file mode 100644
index 0000000..2e9766f
--- /dev/null
+++ b/content/blog/2022-11-28-smartwatch.md
@@ -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!
diff --git a/content/blog/2023-01-06-iceland.md b/content/blog/2023-01-06-iceland.md
new file mode 100644
index 0000000..5cbf2f4
--- /dev/null
+++ b/content/blog/2023-01-06-iceland.md
@@ -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!
diff --git a/content/blog/2023-01-07-yurt.md b/content/blog/2023-01-07-yurt.md
new file mode 100644
index 0000000..583f50f
--- /dev/null
+++ b/content/blog/2023-01-07-yurt.md
@@ -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)
diff --git a/content/blog/_index.md b/content/blog/_index.md
index 9629e59..46960a2 100644
--- a/content/blog/_index.md
+++ b/content/blog/_index.md
@@ -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.
diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html
index 196ff59..2473ab5 100644
--- a/layouts/_default/baseof.html
+++ b/layouts/_default/baseof.html
@@ -5,13 +5,28 @@
-
+
{{range .AlternativeOutputFormats}}
{{end}}
+ {{range .Params.stylesheets}}
+ {{$url := .}}
+ {{with $.Page.Resources.GetMatch .}}
+ {{$url = .RelPermalink}}
+ {{end}}
+
+ {{end}}
{{range .Params.scripts}}
-
+ {{end}}
+ {{range .Params.scripts}}
+ {{$url := .}}
+ {{with $.Page.Resources.GetMatch .}}
+ {{$url = .RelPermalink}}
+ {{end}}
+
{{end}}
{{range .Params.headers}}
{{. | safeHTML}}
diff --git a/layouts/shortcodes/figure.html b/layouts/shortcodes/figure.html
new file mode 100644
index 0000000..1105d69
--- /dev/null
+++ b/layouts/shortcodes/figure.html
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/layouts/shortcodes/video.html b/layouts/shortcodes/video.html
index 8d10820..551cfe5 100644
--- a/layouts/shortcodes/video.html
+++ b/layouts/shortcodes/video.html
@@ -1,4 +1,4 @@
diff --git a/run.sh b/run.sh
index 5d78714..20bd01f 100755
--- a/run.sh
+++ b/run.sh
@@ -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"
diff --git a/static/assets/css/default.css b/static/assets/css/default.css
index 7f370d7..4d84d1d 100644
--- a/static/assets/css/default.css
+++ b/static/assets/css/default.css
@@ -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 {