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: + +
+ + + + + + + +
RankingAward 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. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AliceBobCarol
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. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AliceBobCarol
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. + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AliceBobCarolDaveErin
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. + +
+
+ CLRG Award Points Speculator +
+ Points: + +
+ + + + + + +
Possible Rankings
Computing: this could take a while!
+
+
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 @@ +
+ {{- $img := $.Page.Resources.GetMatch (.Get "src")}} + {{.Get +
\ 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 {