Merge branch 'main' of https://git.woozle.org/neale/homepage
This commit is contained in:
commit
29fe50152e
|
@ -1,9 +1,6 @@
|
||||||
---
|
---
|
||||||
date: "2021-12-23T00:00:00Z"
|
title: "Doctor Who S01E27-30: Doctor Who goes to Mexico"
|
||||||
tags:
|
date: 2021-12-23
|
||||||
- drwho
|
|
||||||
title: 'Doctor Who S01E27-30: Doctor Who goes to Mexico'
|
|
||||||
url: blog/2021-12-23-drwho-S01E27-mexico/
|
|
||||||
---
|
---
|
||||||
|
|
||||||
A bunch of white people pretend to be Aztecs, and explore their moral
|
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
|
married or engaged or something. Susan finally expresses an emotion other
|
||||||
than terror:
|
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,
|
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
|
maybe this is useful for viewing 1960s British culture, but that's still
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
---
|
---
|
||||||
date: "2022-09-06T12:11:00-0600"
|
|
||||||
title: Truck bling
|
title: Truck bling
|
||||||
url: blog/2022-09-06-truck-bling/
|
date: "2022-09-06T12:11:00-0600"
|
||||||
---
|
---
|
||||||
|
|
||||||
Yesterday,
|
Yesterday,
|
||||||
|
@ -18,7 +17,7 @@ the friend soldered a bunch of stuff together,
|
||||||
and we plugged it in.
|
and we plugged it in.
|
||||||
It friggin' worked!
|
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,
|
The really cool part, at least for me,
|
||||||
is that now she can hang out with her laptop in the cabin,
|
is that now she can hang out with her laptop in the cabin,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
---
|
---
|
||||||
title: CLRG's Cheating Scandal
|
title: CLRG's Cheating Scandal
|
||||||
date: 2022-10-04
|
date: 2022-10-04
|
||||||
|
tags: clrg
|
||||||
---
|
---
|
||||||
|
|
||||||
$SPOUSE just stumbled across a PowerPoint file with a bunch of text messages,
|
$SPOUSE just stumbled across a PowerPoint file with a bunch of text messages,
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
let awardPoints = [
|
||||||
|
100, // 1
|
||||||
|
75, // 2
|
||||||
|
65, // 3
|
||||||
|
60, // 4
|
||||||
|
56, // 5
|
||||||
|
53, // 6
|
||||||
|
50, // 7
|
||||||
|
47, // 8
|
||||||
|
45, // 9
|
||||||
|
43, // 10
|
||||||
|
41, // 11
|
||||||
|
39, // 12
|
||||||
|
38, // 13
|
||||||
|
37, // 14
|
||||||
|
36, // 15
|
||||||
|
35, // 16
|
||||||
|
34, // 17
|
||||||
|
33, // 18
|
||||||
|
32, // 19
|
||||||
|
31, // 20
|
||||||
|
30, // 21
|
||||||
|
29, // 22
|
||||||
|
28, // 23
|
||||||
|
27, // 24
|
||||||
|
26, // 25
|
||||||
|
25, // 26
|
||||||
|
24, // 27
|
||||||
|
23, // 28
|
||||||
|
22, // 29
|
||||||
|
21, // 30
|
||||||
|
20, // 31
|
||||||
|
19, // 32
|
||||||
|
18, // 33
|
||||||
|
17, // 34
|
||||||
|
16, // 35
|
||||||
|
15, // 36
|
||||||
|
14, // 37
|
||||||
|
13, // 38
|
||||||
|
12, // 39
|
||||||
|
11, // 40
|
||||||
|
10, // 41
|
||||||
|
9, // 42
|
||||||
|
8, // 43
|
||||||
|
7, // 44
|
||||||
|
6, // 45
|
||||||
|
5, // 46
|
||||||
|
4, // 47
|
||||||
|
3, // 48
|
||||||
|
2, // 49
|
||||||
|
1, // 50
|
||||||
|
0.75, // 51
|
||||||
|
0.65, // 52
|
||||||
|
0.60, // 53
|
||||||
|
0.56, // 54
|
||||||
|
0.53, // 55
|
||||||
|
0.50, // 56
|
||||||
|
0.47, // 57
|
||||||
|
0.45, // 58
|
||||||
|
0.43, // 59
|
||||||
|
0.41, // 60
|
||||||
|
0.39, // 61
|
||||||
|
0.38, // 62
|
||||||
|
0.37, // 63
|
||||||
|
0.36, // 64
|
||||||
|
0.35, // 65
|
||||||
|
0.34, // 66
|
||||||
|
0.33, // 67
|
||||||
|
0.32, // 68
|
||||||
|
0.31, // 69
|
||||||
|
0.30, // 70
|
||||||
|
0.29, // 71
|
||||||
|
0.28, // 72
|
||||||
|
0.27, // 73
|
||||||
|
0.26, // 74
|
||||||
|
0.25, // 75
|
||||||
|
0.24, // 76
|
||||||
|
0.23, // 77
|
||||||
|
0.22, // 78
|
||||||
|
0.21, // 79
|
||||||
|
0.20, // 80
|
||||||
|
0.19, // 81
|
||||||
|
0.18, // 82
|
||||||
|
0.17, // 83
|
||||||
|
0.16, // 84
|
||||||
|
0.15, // 85
|
||||||
|
0.14, // 86
|
||||||
|
0.13, // 87
|
||||||
|
0.12, // 88
|
||||||
|
0.11, // 89
|
||||||
|
0.10, // 90
|
||||||
|
0.09, // 91
|
||||||
|
0.08, // 92
|
||||||
|
0.07, // 93
|
||||||
|
0.06, // 94
|
||||||
|
0.05, // 95
|
||||||
|
0.04, // 96
|
||||||
|
0.03, // 97
|
||||||
|
0.02, // 98
|
||||||
|
0.01, // 99
|
||||||
|
0.00, // 100
|
||||||
|
]
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
for (let tbody of document.querySelectorAll(".awardPoints tbody")) {
|
||||||
|
for (let i = 0; i < awardPoints.length; i++) {
|
||||||
|
let tr = tbody.appendChild(document.createElement("tr"))
|
||||||
|
tr.appendChild(document.createElement("td")).textContent = i + 1
|
||||||
|
tr.appendChild(document.createElement("td")).textContent = awardPoints[i].toFixed(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", init)
|
||||||
|
} else {
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
awardPoints,
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
|
@ -0,0 +1,261 @@
|
||||||
|
---
|
||||||
|
title: CLRG Scoring Analyzed
|
||||||
|
date: 2022-10-09
|
||||||
|
tags:
|
||||||
|
- clrg
|
||||||
|
stylesheets:
|
||||||
|
- toys.css
|
||||||
|
scripts:
|
||||||
|
- scorecard.mjs
|
||||||
|
---
|
||||||
|
|
||||||
|
Let's take a look how how CLRG does its scoring!
|
||||||
|
*With math!*
|
||||||
|
|
||||||
|
|
||||||
|
## How CLRG Scoring Works
|
||||||
|
|
||||||
|
As I am given to understand, the scoring works like so:
|
||||||
|
|
||||||
|
1. Adjudicators give you a "raw score": a real number between 0 and 100
|
||||||
|
2. The scoring system ranks each dancer per adjudicator, based on raw scores
|
||||||
|
3. These rankings are mapped into "award points"
|
||||||
|
4. All of a dancer's award points are summed
|
||||||
|
5. Final ranking is determined by comparing total award points
|
||||||
|
|
||||||
|
## Raw Scoring
|
||||||
|
|
||||||
|
The way raw scores translate into rankings and award points is a little
|
||||||
|
confusing, so I've made a little tool you can play with to get a feel for how it
|
||||||
|
works. Essentially, it's a way of normalizing places to an adjudicator: score
|
||||||
|
weights are only relative to the judge that assigns them.
|
||||||
|
|
||||||
|
Adjudicator A can assign scores between 80 and 100;
|
||||||
|
adjudicator B can assign scores between 1 and 40;
|
||||||
|
and they'll both have a first, second, third, fourth place, etc.
|
||||||
|
These places then get translated into award points.
|
||||||
|
|
||||||
|
|
||||||
|
## Award Points
|
||||||
|
|
||||||
|
Award points are handed out based on ranking against other dancers for that
|
||||||
|
adjudicator. I obtained these values from a FeisWorx results page for my kid:
|
||||||
|
|
||||||
|
<div class="awardPoints">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Ranking</th><th>Award Points</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
If there's a 2-way, 3-way, or n-way tie,
|
||||||
|
all tied dancers get the average of the next 2, 3, or n award points,
|
||||||
|
and the next 2, 3, or n rankings are skipped.
|
||||||
|
|
||||||
|
|
||||||
|
## What's with these values?
|
||||||
|
|
||||||
|
At first glance, the award points look like the output of an exponential function.
|
||||||
|
|
||||||
|
{{<figure src="chart.png" alt="Chart of scores vs. award points">}}
|
||||||
|
|
||||||
|
In an effort to figure out where these numbers came from,
|
||||||
|
I ran some curve fitting against the data.
|
||||||
|
Here's the best I could come up with:
|
||||||
|
|
||||||
|
| Ranking range | Award Points Function | Type of function |
|
||||||
|
| --: | --: | --- |
|
||||||
|
| 1 - 11 | 100 * x^-0.358 | Exponential |
|
||||||
|
| 12 - 50 | 51 - x | Linear |
|
||||||
|
| 51 - 60 | 14.2 - 0.46x + 0.00385x | Polynomial |
|
||||||
|
| 61 - 100 | 1 - x/100 | Linear |
|
||||||
|
|
||||||
|
If you, dear reader, are a mathematician,
|
||||||
|
I would love to hear your thoughts on why they went with this algorithm.
|
||||||
|
|
||||||
|
There are a few points to note here:
|
||||||
|
|
||||||
|
* 1st place is a *huge deal*. Disproportionately huge.
|
||||||
|
* Places 2-10 are similarly big deals compared to places 3-11.
|
||||||
|
* Places 12-50 operate the way most people probably assume ranking works: linearly.
|
||||||
|
* Places 51-60 fit best to a second degree polynomial, but it doesn't matter much for differences of hundreths of a point. This section is *really weird*, mathematically.
|
||||||
|
* Places 61-100 are all less than 1 point. If you're a judge trying to tank a top dancer, anywhere in this range is equivalent to anywhere else.
|
||||||
|
|
||||||
|
|
||||||
|
## Consequences of Exponential Award Points
|
||||||
|
|
||||||
|
Playing around with this,
|
||||||
|
I've found a few interesting consequences
|
||||||
|
of the exponential growth in the top 11 places.
|
||||||
|
|
||||||
|
|
||||||
|
### 1st place is super important
|
||||||
|
|
||||||
|
1st place is weighted so heavily that one judge could move a 5th place dancer into 2nd.
|
||||||
|
|
||||||
|
<table class="scorecard">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<th>Alice</th>
|
||||||
|
<th>Bob</th>
|
||||||
|
<th>Carol</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th class="justify-left">Adj. 1</th>
|
||||||
|
<td><input type="number" min=1 max=99 value=1 readonly></td>
|
||||||
|
<td><input type="number" min=1 max=99 value=3></td>
|
||||||
|
<td><input type="number" min=1 max=99 value=2></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="justify-left">Adj. 2</th>
|
||||||
|
<td><input type="number" min=1 max=99 value=5></td>
|
||||||
|
<td><input type="number" min=1 max=99 value=3></td>
|
||||||
|
<td><input type="number" min=1 max=99 value=1></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="justify-left">Adj. 3</th>
|
||||||
|
<td><input type="number" min=1 max=99 value=5></td>
|
||||||
|
<td><input type="number" min=1 max=99 value=3></td>
|
||||||
|
<td><input type="number" min=1 max=99 value=2></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<th class="justify-left">Award Points</th>
|
||||||
|
<td class="justify-right"><output name="points"></td>
|
||||||
|
<td class="justify-right"><output name="points"></td>
|
||||||
|
<td class="justify-right"><output name="points"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="justify-left">Ranking</th>
|
||||||
|
<td class="justify-right"><output name="ranking"></td>
|
||||||
|
<td class="justify-right"><output name="ranking"></td>
|
||||||
|
<td class="justify-right"><output name="ranking"></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
You can adjust these values to get a better feel for how scoring works.
|
||||||
|
|
||||||
|
|
||||||
|
### Tanking a high-ranked dancer is another way to cheat
|
||||||
|
|
||||||
|
Because of that exponential curve,
|
||||||
|
a low ranking from a single judge can carry a lot of weight.
|
||||||
|
|
||||||
|
<table class="scorecard">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<th>Alice</th>
|
||||||
|
<th>Bob</th>
|
||||||
|
<th>Carol</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th class="justify-left">Adj. 1</th>
|
||||||
|
<td><input type="number" min=1 max=99 value=3></td>
|
||||||
|
<td><input type="number" min=1 max=99 value=11></td>
|
||||||
|
<td><input type="number" min=1 max=99 value=2></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="justify-left">Adj. 2</th>
|
||||||
|
<td><input type="number" min=1 max=99 value=3></td>
|
||||||
|
<td><input type="number" min=1 max=99 value=1></td>
|
||||||
|
<td><input type="number" min=1 max=99 value=2></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="justify-left">Adj. 3</th>
|
||||||
|
<td><input type="number" min=1 max=99 value=3></td>
|
||||||
|
<td><input type="number" min=1 max=99 value=2></td>
|
||||||
|
<td><input type="number" min=1 max=99 value=1></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<th class="justify-left">Award Points</th>
|
||||||
|
<td class="justify-right"><output name="points"></td>
|
||||||
|
<td class="justify-right"><output name="points"></td>
|
||||||
|
<td class="justify-right"><output name="points"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="justify-left">Ranking</th>
|
||||||
|
<td class="justify-right"><output name="ranking"></td>
|
||||||
|
<td class="justify-right"><output name="ranking"></td>
|
||||||
|
<td class="justify-right"><output name="ranking"></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
### Being in 1st provides a nice buffer
|
||||||
|
|
||||||
|
Try playing around with Alice's rankings with Adjudicators 2 and 3 here.
|
||||||
|
She has to get ranked a lot lower before her overall ranking starts going down.
|
||||||
|
|
||||||
|
<div class="scrolly">
|
||||||
|
<table class="scorecard">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<th>Alice</th>
|
||||||
|
<th>Bob</th>
|
||||||
|
<th>Carol</th>
|
||||||
|
<th>Dave</th>
|
||||||
|
<th>Erin</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th class="justify-left">Adj. 1</th>
|
||||||
|
<td><input type="number" min=1 max=99 value=1></td>
|
||||||
|
<td><input type="number" min=1 max=99 value=3></td>
|
||||||
|
<td><input type="number" min=1 max=99 value=2></td>
|
||||||
|
<td><input type="number" min=1 max=99 value=4></td>
|
||||||
|
<td><input type="number" min=1 max=99 value=5></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="justify-left">Adj. 2</th>
|
||||||
|
<td><input type="number" min=1 max=99 value=7></td>
|
||||||
|
<td><input type="number" min=1 max=99 value=1></td>
|
||||||
|
<td><input type="number" min=1 max=99 value=2></td>
|
||||||
|
<td><input type="number" min=1 max=99 value=3></td>
|
||||||
|
<td><input type="number" min=1 max=99 value=4></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="justify-left">Adj. 3</th>
|
||||||
|
<td><input type="number" min=1 max=99 value=5></td>
|
||||||
|
<td><input type="number" min=1 max=99 value=2></td>
|
||||||
|
<td><input type="number" min=1 max=99 value=1></td>
|
||||||
|
<td><input type="number" min=1 max=99 value=3></td>
|
||||||
|
<td><input type="number" min=1 max=99 value=4></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<th class="justify-left">Award Points</th>
|
||||||
|
<td class="justify-right"><output name="points"></td>
|
||||||
|
<td class="justify-right"><output name="points"></td>
|
||||||
|
<td class="justify-right"><output name="points"></td>
|
||||||
|
<td class="justify-right"><output name="points"></td>
|
||||||
|
<td class="justify-right"><output name="points"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="justify-left">Ranking</th>
|
||||||
|
<td class="justify-right"><output name="ranking"></td>
|
||||||
|
<td class="justify-right"><output name="ranking"></td>
|
||||||
|
<td class="justify-right"><output name="ranking"></td>
|
||||||
|
<td class="justify-right"><output name="ranking"></td>
|
||||||
|
<td class="justify-right"><output name="ranking"></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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,
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
---
|
||||||
|
title: CLRG Award Points Artifacts
|
||||||
|
date: 2022-10-10T08:00:00-06:00
|
||||||
|
tags:
|
||||||
|
- clrg
|
||||||
|
stylesheets:
|
||||||
|
- toys.css
|
||||||
|
scripts:
|
||||||
|
- speculator.mjs
|
||||||
|
---
|
||||||
|
|
||||||
|
One quirk of awards points is that for any given overall
|
||||||
|
score, there are only a handful of possible judge rankings that could have led
|
||||||
|
to it. That means you can make some guesses about how each judge ranked an
|
||||||
|
individual dancer, based on only their total award points.
|
||||||
|
|
||||||
|
Here's a handy calculator!
|
||||||
|
It (currently) doesn't consider the possibility of a tie.
|
||||||
|
|
||||||
|
<div class="scrolly">
|
||||||
|
<fieldset class="speculator">
|
||||||
|
<legend>CLRG Award Points Speculator</legend>
|
||||||
|
<div>
|
||||||
|
Points: <input name="points" type="number" min=41 max=10000 value=188>
|
||||||
|
<input name="adjudicators" type="hidden">
|
||||||
|
</div>
|
||||||
|
<table class="results">
|
||||||
|
<caption>Possible Rankings</caption>
|
||||||
|
<thead>
|
||||||
|
<tr class="warning"><th>Computing: this could take a while!</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
# I don't have rights to copy any of the data :(
|
||||||
|
*.xml
|
|
@ -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)
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
title: Unfinished CLRG Data Analyzer
|
||||||
|
stylesheets:
|
||||||
|
- dataset.css
|
||||||
|
scripts:
|
||||||
|
- dataset.mjs
|
||||||
|
---
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This won't work because you don't have the datasets.
|
||||||
|
I can't provide them to you, due to copyright laws.
|
||||||
|
But if you get the results PDFs,
|
||||||
|
load them up in Adobe Acrobat,
|
||||||
|
and save them as XML,
|
||||||
|
they might load here :)
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h1>2021 Irish Dance North Americans 21A</h1>
|
||||||
|
<div class="clrg-dataset" data-url="2022-10-10 2021 Irish dance north Americans 21A.xml"></div>
|
||||||
|
|
||||||
|
<h1>2017 11 AB Wro</h1>
|
||||||
|
<div class="clrg-dataset" data-url="2017-11 AB Wro.xml"></div>
|
||||||
|
|
||||||
|
<h1>2019 09 Wro</h1>
|
||||||
|
<div class="clrg-dataset" data-url="2022-10-10 Wro2019-09.xml"></div>
|
|
@ -0,0 +1,131 @@
|
||||||
|
let awardPoints = [
|
||||||
|
100, // 1
|
||||||
|
75, // 2
|
||||||
|
65, // 3
|
||||||
|
60, // 4
|
||||||
|
56, // 5
|
||||||
|
53, // 6
|
||||||
|
50, // 7
|
||||||
|
47, // 8
|
||||||
|
45, // 9
|
||||||
|
43, // 10
|
||||||
|
41, // 11
|
||||||
|
39, // 12
|
||||||
|
38, // 13
|
||||||
|
37, // 14
|
||||||
|
36, // 15
|
||||||
|
35, // 16
|
||||||
|
34, // 17
|
||||||
|
33, // 18
|
||||||
|
32, // 19
|
||||||
|
31, // 20
|
||||||
|
30, // 21
|
||||||
|
29, // 22
|
||||||
|
28, // 23
|
||||||
|
27, // 24
|
||||||
|
26, // 25
|
||||||
|
25, // 26
|
||||||
|
24, // 27
|
||||||
|
23, // 28
|
||||||
|
22, // 29
|
||||||
|
21, // 30
|
||||||
|
20, // 31
|
||||||
|
19, // 32
|
||||||
|
18, // 33
|
||||||
|
17, // 34
|
||||||
|
16, // 35
|
||||||
|
15, // 36
|
||||||
|
14, // 37
|
||||||
|
13, // 38
|
||||||
|
12, // 39
|
||||||
|
11, // 40
|
||||||
|
10, // 41
|
||||||
|
9, // 42
|
||||||
|
8, // 43
|
||||||
|
7, // 44
|
||||||
|
6, // 45
|
||||||
|
5, // 46
|
||||||
|
4, // 47
|
||||||
|
3, // 48
|
||||||
|
2, // 49
|
||||||
|
1, // 50
|
||||||
|
0.75, // 51
|
||||||
|
0.65, // 52
|
||||||
|
0.60, // 53
|
||||||
|
0.56, // 54
|
||||||
|
0.53, // 55
|
||||||
|
0.50, // 56
|
||||||
|
0.47, // 57
|
||||||
|
0.45, // 58
|
||||||
|
0.43, // 59
|
||||||
|
0.41, // 60
|
||||||
|
0.39, // 61
|
||||||
|
0.38, // 62
|
||||||
|
0.37, // 63
|
||||||
|
0.36, // 64
|
||||||
|
0.35, // 65
|
||||||
|
0.34, // 66
|
||||||
|
0.33, // 67
|
||||||
|
0.32, // 68
|
||||||
|
0.31, // 69
|
||||||
|
0.30, // 70
|
||||||
|
0.29, // 71
|
||||||
|
0.28, // 72
|
||||||
|
0.27, // 73
|
||||||
|
0.26, // 74
|
||||||
|
0.25, // 75
|
||||||
|
0.24, // 76
|
||||||
|
0.23, // 77
|
||||||
|
0.22, // 78
|
||||||
|
0.21, // 79
|
||||||
|
0.20, // 80
|
||||||
|
0.19, // 81
|
||||||
|
0.18, // 82
|
||||||
|
0.17, // 83
|
||||||
|
0.16, // 84
|
||||||
|
0.15, // 85
|
||||||
|
0.14, // 86
|
||||||
|
0.13, // 87
|
||||||
|
0.12, // 88
|
||||||
|
0.11, // 89
|
||||||
|
0.10, // 90
|
||||||
|
0.09, // 91
|
||||||
|
0.08, // 92
|
||||||
|
0.07, // 93
|
||||||
|
0.06, // 94
|
||||||
|
0.05, // 95
|
||||||
|
0.04, // 96
|
||||||
|
0.03, // 97
|
||||||
|
0.02, // 98
|
||||||
|
0.01, // 99
|
||||||
|
0.00, // 100
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a score, calculate what placings could have gotten it.
|
||||||
|
*
|
||||||
|
* @param {Number} score Score we're going to guess
|
||||||
|
* @param {Number} tied Highest number n-way tie to consider
|
||||||
|
* @returns {Array.<Number>} List of possible placings
|
||||||
|
*/
|
||||||
|
function guessPlacing(score, tied=3) {
|
||||||
|
let placings = []
|
||||||
|
for (let t = tied; t > 0; t--) {
|
||||||
|
let totalPoints = score * t
|
||||||
|
for (let placing = 0; placing < awardPoints.length - t; placing++) {
|
||||||
|
let acc = 0
|
||||||
|
for (let i = 0; i < t; i++) {
|
||||||
|
acc += awardPoints[placing+i]
|
||||||
|
}
|
||||||
|
if (acc == totalPoints) {
|
||||||
|
placings.push(placing+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return placings
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
awardPoints,
|
||||||
|
guessPlacing,
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -0,0 +1,180 @@
|
||||||
|
/**
|
||||||
|
* Feis Dataset Importer
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as FeisWorx from "./feisworx.mjs"
|
||||||
|
import * as FeisResults from "./feisresults.mjs"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import("./types.mjs").Results} Results
|
||||||
|
* @typedef {import("./types.mjs").Result} Result
|
||||||
|
* @typedef {import("./types.mjs").Round} Round
|
||||||
|
* @typedef {import("./types.mjs").Adjudication} Adjudication
|
||||||
|
* @typedef {Array.<Array.<String>>} RawData
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new element and appends it to parent
|
||||||
|
*
|
||||||
|
* @param {Element} parent Element to append to
|
||||||
|
* @param {String} type Type of element to create
|
||||||
|
* @param {Object} [dataset] Data fields to set
|
||||||
|
* @returns {Element}
|
||||||
|
*/
|
||||||
|
function newElement(parent, type, dataset={}) {
|
||||||
|
let child = parent.appendChild(document.createElement(type))
|
||||||
|
for (let k in dataset) {
|
||||||
|
child.dataset[k] = dataset[k]
|
||||||
|
}
|
||||||
|
return child
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a file and parse it into Results.
|
||||||
|
*
|
||||||
|
* @param {URL|String} url Location of file to load
|
||||||
|
* @returns {Results} Parsed results
|
||||||
|
*/
|
||||||
|
async function loadData(url) {
|
||||||
|
let resp = await fetch(url)
|
||||||
|
let contentType = resp.headers.get("Content-Type")
|
||||||
|
if (! contentType.includes("/xml")) {
|
||||||
|
console.error(`Cannot load data with content-type ${contentType}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let text = await resp.text()
|
||||||
|
let doc = new DOMParser().parseFromString(text, "text/xml")
|
||||||
|
let rawData = parseXMLDocument(doc)
|
||||||
|
return parseRawData(rawData)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse an XML document of feis results into a 2D array of strings
|
||||||
|
*
|
||||||
|
* @param {Document} doc XML Document
|
||||||
|
* @returns {RawData} Raw data
|
||||||
|
*/
|
||||||
|
function parseXMLDocument(doc) {
|
||||||
|
let table = doc.querySelector("Table")
|
||||||
|
let rawData = []
|
||||||
|
|
||||||
|
for (let dataRow of table.children) {
|
||||||
|
if (! ["tr"].includes(dataRow.tagName.toLowerCase())) {
|
||||||
|
console.warn(`Warning: unexpected XML tag ${dataRow.tagName}, expecting tr`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let row = []
|
||||||
|
for (let dataCell of dataRow.children) {
|
||||||
|
if (! ["th", "td"].includes(dataCell.tagName.toLowerCase())) {
|
||||||
|
console.warn(`Warning: unexpected XML tag ${dataRow.tagName}, expecting th/td`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
row.push(dataCell.textContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawData.push(row)
|
||||||
|
}
|
||||||
|
return rawData
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse raw data into a list of adjudicators and results
|
||||||
|
*
|
||||||
|
* @param {RawData} rawData Raw data
|
||||||
|
* @returns {Results} Parsed Results
|
||||||
|
*/
|
||||||
|
function parseRawData(rawData) {
|
||||||
|
let firstRow = rawData[0]
|
||||||
|
if (firstRow[0].trim().toLowerCase() == "place awd pts") {
|
||||||
|
return FeisWorx.parse(rawData)
|
||||||
|
}
|
||||||
|
if (firstRow[firstRow.length-1].trim().toLowerCase() == "total ip *") {
|
||||||
|
return FeisResults.parse(rawData)
|
||||||
|
}
|
||||||
|
console.error("First row doesn't resemble anything I can cope with", firstRow)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Fills a table element with some results
|
||||||
|
*
|
||||||
|
* @param {Element} table Table to fill in
|
||||||
|
* @param {Results} results Results to fill with
|
||||||
|
*/
|
||||||
|
function fillTable(table, results) {
|
||||||
|
let head = newElement(table, "thead")
|
||||||
|
let row0 = newElement(head, "tr")
|
||||||
|
let row1 = newElement(head, "tr")
|
||||||
|
let row2 = newElement(head, "tr")
|
||||||
|
|
||||||
|
newElement(row0, "th").colSpan = 2
|
||||||
|
newElement(row1, "th").colSpan = 2
|
||||||
|
newElement(row2, "th").textContent = "Name"
|
||||||
|
newElement(row2, "th").textContent = "Rank"
|
||||||
|
|
||||||
|
let roundNumber = 0
|
||||||
|
for (let round of results[0].rounds) {
|
||||||
|
let roundCell = newElement(row0, "th")
|
||||||
|
roundCell.textContent = `Round ${++roundNumber}`
|
||||||
|
roundCell.colSpan = 3*round.length
|
||||||
|
for (let adjudication of round) {
|
||||||
|
let adjudicator = adjudication.adjudicator
|
||||||
|
let cell = newElement(row1, "th")
|
||||||
|
cell.textContent = adjudicator
|
||||||
|
cell.colSpan = 3
|
||||||
|
|
||||||
|
newElement(row2, "th").textContent = "Raw"
|
||||||
|
newElement(row2, "th").textContent = "Placing"
|
||||||
|
newElement(row2, "th").textContent = "Points"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = newElement(table, "tbody")
|
||||||
|
for (let result of results) {
|
||||||
|
let row = newElement(body, "tr")
|
||||||
|
|
||||||
|
newElement(row, "th").textContent = result.name
|
||||||
|
newElement(row, "th").textContent = result.overallRank
|
||||||
|
|
||||||
|
let i = 0
|
||||||
|
for (let round of result.rounds) {
|
||||||
|
let first = true
|
||||||
|
for (let adjudication of round) {
|
||||||
|
let raw = newElement(row, "td")
|
||||||
|
raw.textContent = adjudication.raw
|
||||||
|
raw.classList.add("new-adjudication")
|
||||||
|
if (first) {
|
||||||
|
raw.classList.add("new-round")
|
||||||
|
first = false
|
||||||
|
}
|
||||||
|
|
||||||
|
newElement(row, "td").textContent = adjudication.placing
|
||||||
|
newElement(row, "td").textContent = adjudication.points
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
for (let div of document.querySelectorAll(".clrg-dataset")) {
|
||||||
|
let results = await loadData(div.dataset.url)
|
||||||
|
|
||||||
|
let table = newElement(div, "table")
|
||||||
|
fillTable(table, results)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", init)
|
||||||
|
} else {
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
loadData,
|
||||||
|
parseXMLDocument,
|
||||||
|
parseRawData,
|
||||||
|
}
|
|
@ -0,0 +1,139 @@
|
||||||
|
/**
|
||||||
|
* Feisresults.com parser
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {awardPoints, guessPlacing} from "./awardPoints.mjs"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import("./types.mjs").Results} Results
|
||||||
|
* @typedef {import("./types.mjs").Result} Result
|
||||||
|
* @typedef {import("./types.mjs").Round} Round
|
||||||
|
* @typedef {import("./types.mjs").Adjudication} Adjudication
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse feisresults data
|
||||||
|
*
|
||||||
|
* @param {Array.<Array.<String>>} rawData Raw data
|
||||||
|
* @returns {Results}
|
||||||
|
*/
|
||||||
|
function parse(rawData) {
|
||||||
|
/** @type {Results} */
|
||||||
|
let results = []
|
||||||
|
let adjudicators = []
|
||||||
|
let numRounds = 0
|
||||||
|
let adjudicatorsPerRound = 0
|
||||||
|
|
||||||
|
let possibleTiesByAdjudicatorRound = {}
|
||||||
|
|
||||||
|
for (let rowIndex = 0; rowIndex < rawData.length; rowIndex++) {
|
||||||
|
let cells = rawData[rowIndex]
|
||||||
|
|
||||||
|
// Is it a page heading?
|
||||||
|
if ((cells[0].trim().toLowerCase() == "card")) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is it a list of adjudicators?
|
||||||
|
if (cells[cells.length-1].trim().toLowerCase() == "total ip *") {
|
||||||
|
cells.splice(cells.length-1, 1) // -1: total IP *
|
||||||
|
cells.splice(0, 5) // 0 - 4: blank
|
||||||
|
adjudicators = []
|
||||||
|
for (let cell of cells) {
|
||||||
|
cell = cell.trim()
|
||||||
|
if (cell.toLowerCase().includes("rounds 1")) {
|
||||||
|
// skip it
|
||||||
|
} else if (cell.toLowerCase().includes("round total")) {
|
||||||
|
numRounds++
|
||||||
|
} else {
|
||||||
|
adjudicators.push(cell)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adjudicatorsPerRound = adjudicators.length / numRounds
|
||||||
|
if (! Number.isSafeInteger(adjudicatorsPerRound)) {
|
||||||
|
console.error(`Irrational number of adjudicators for number of rounds: (${adjudicators.length}/${numRounds})`)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let row = {}
|
||||||
|
row.number = Number(cells[0])
|
||||||
|
// cells[1]: Position at recall
|
||||||
|
row.overallRank = Number(cells[2])
|
||||||
|
{
|
||||||
|
let parts = cells[3].trim().split(/\s:\s/)
|
||||||
|
console.log(parts, cells[3])
|
||||||
|
let nameSchool = parts[0]
|
||||||
|
// parts[1]: region
|
||||||
|
// We're going to take a wild-ass guess here that the dancer only has two names
|
||||||
|
let subparts = nameSchool.split(/\s+/)
|
||||||
|
row.name = subparts.slice(0, 2).join(" ")
|
||||||
|
row.school = subparts.slice(2).join(" ")
|
||||||
|
}
|
||||||
|
row.qualifier = cells[4].trim()
|
||||||
|
|
||||||
|
/** @type {Round} */
|
||||||
|
let round = []
|
||||||
|
/** @type {Array.<Round>} */
|
||||||
|
row.rounds = []
|
||||||
|
let adjudicatorNumber = 0
|
||||||
|
for (let cellIndex = 5; cellIndex < cells.length; cellIndex++) {
|
||||||
|
let cell = cells[cellIndex].trim()
|
||||||
|
if (! cell.includes("/")) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {Adjudication} */
|
||||||
|
let adjudication = {}
|
||||||
|
adjudication.adjudicator = adjudicators[adjudicatorNumber++]
|
||||||
|
|
||||||
|
let parts = cell.split("/")
|
||||||
|
adjudication.raw = Number(parts[0])
|
||||||
|
adjudication.points = Number(parts[1])
|
||||||
|
adjudication.placing = guessPlacing(adjudication.points)
|
||||||
|
// Guidebook reports don't list every dancer: we'll guess placing later
|
||||||
|
|
||||||
|
round.push(adjudication)
|
||||||
|
if (round.length == adjudicatorsPerRound) {
|
||||||
|
row.rounds.push(round)
|
||||||
|
round = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results.push(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
disambiguatePlacings(results, numRounds, adjudicatorsPerRound)
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
function disambiguatePlacings(results, numRounds, adjudicatorsPerRound) {
|
||||||
|
for (let roundNumber = 0; roundNumber < numRounds; roundNumber++) {
|
||||||
|
/**
|
||||||
|
* A list of raw score, award points, and placing
|
||||||
|
*
|
||||||
|
* @type {Array.<Adjudication>}
|
||||||
|
*/
|
||||||
|
for (let judgeNumber = 0; judgeNumber < adjudicatorsPerRound; judgeNumber++) {
|
||||||
|
let scores = []
|
||||||
|
for (let result of results) {
|
||||||
|
scores.push(result.rounds[roundNumber][judgeNumber])
|
||||||
|
}
|
||||||
|
scores.sort((a,b) => b.raw - a.raw)
|
||||||
|
|
||||||
|
let greatestPlacing = 0
|
||||||
|
for (let adjudication of scores) {
|
||||||
|
let possibilities = guessPlacing(adjudication.points)
|
||||||
|
possibilities.sort((a,b) => b-a)
|
||||||
|
// XXX: eliminate possibilities less than greatestPlacing, then pick the largest
|
||||||
|
}
|
||||||
|
console.log(scores)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
parse,
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
/**
|
||||||
|
* FeisWorx parser
|
||||||
|
*
|
||||||
|
* This is the output of Adobe Reader saving the PDF as XML.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import("./types.mjs").Results} Results
|
||||||
|
* @typedef {import("./types.mjs").Result} Result
|
||||||
|
* @typedef {import("./types.mjs").Round} Round
|
||||||
|
* @typedef {import("./types.mjs").Adjudication} Adjudication
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse FeisWorx data
|
||||||
|
*
|
||||||
|
* @param {Array.<Array.<String>>} rawData Raw data
|
||||||
|
* @returns {Results}
|
||||||
|
*/
|
||||||
|
function parse(rawData) {
|
||||||
|
/** @type {Results} */
|
||||||
|
let results = []
|
||||||
|
let adjudicators = []
|
||||||
|
let numRounds = 0
|
||||||
|
let adjudicatorsPerRound = 0
|
||||||
|
|
||||||
|
for (let rowIndex = 0; rowIndex < rawData.length; rowIndex++) {
|
||||||
|
let cells = rawData[rowIndex]
|
||||||
|
|
||||||
|
// Is it a page heading?
|
||||||
|
if ((cells.length >= 11) && (cells[0].trim().toLowerCase().startsWith("place"))) {
|
||||||
|
if (numRounds == 0) {
|
||||||
|
for (let cell of cells) {
|
||||||
|
if (cell.toLowerCase().startsWith("round")) {
|
||||||
|
numRounds++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (adjudicators.length == 0) {
|
||||||
|
let fishy = false
|
||||||
|
for (let adjudicator of cells) {
|
||||||
|
if (Number(adjudicator) > 0) {
|
||||||
|
fishy = true
|
||||||
|
}
|
||||||
|
adjudicators.push(adjudicator.trim())
|
||||||
|
}
|
||||||
|
if (fishy) {
|
||||||
|
console.warn("Adjudicators row doesn't look right", cells)
|
||||||
|
}
|
||||||
|
adjudicatorsPerRound = adjudicators.length / numRounds
|
||||||
|
if (! Number.isSafeInteger(adjudicatorsPerRound)) {
|
||||||
|
console.error(`Irrational number of adjudicators for number of rounds: (${adjudicators.length}/${numRounds})`)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is this just a list of adjudicators again?
|
||||||
|
if (cells.length >= adjudicators.length) {
|
||||||
|
let lenDiff = cells.length - adjudicators.length
|
||||||
|
let same = true
|
||||||
|
for (let i = adjudicators.length-1; i >= 0; i--) {
|
||||||
|
if (adjudicators[i] != cells[i+lenDiff].trim()) {
|
||||||
|
same = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (same) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let row = {}
|
||||||
|
|
||||||
|
{
|
||||||
|
let parts = cells[0].trim().split(/\s+/)
|
||||||
|
row.overallRank = Number(parts[0])
|
||||||
|
row.overallPoints = Number(parts[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let match = cells[1].trim().match(/(\d+) - (.+) \((.+) *\)[ -]*(.+)?/)
|
||||||
|
if (match) {
|
||||||
|
row.number = Number(match[1])
|
||||||
|
row.name = match[2]
|
||||||
|
row.school = match[3]
|
||||||
|
row.qualifier = match[4]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {Round} */
|
||||||
|
let round = []
|
||||||
|
/** @type {Array.<Round>} */
|
||||||
|
row.rounds = []
|
||||||
|
for (let cellIndex = 2; cellIndex < cells.length; cellIndex++) {
|
||||||
|
let cell = cells[cellIndex]
|
||||||
|
/** @type {Adjudication} */
|
||||||
|
let adjudication = {}
|
||||||
|
let parts = cell.trim().split(/ - ?|\s/)
|
||||||
|
|
||||||
|
adjudication.adjudicator = adjudicators[cellIndex - 2]
|
||||||
|
|
||||||
|
if ((parts.length == 5) && (parts[3] == "AP")) {
|
||||||
|
parts.splice(3, 0, "NaN")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((parts.length == 7) && (parts[4] == "T")) {
|
||||||
|
adjudication.tie = true
|
||||||
|
parts.splice(4, 1)
|
||||||
|
} else {
|
||||||
|
adjudication.tie = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts.length != 6) {
|
||||||
|
console.error(`Wrong number of fields in row ${rowIndex} cell ${cellIndex}:`, parts, cells)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < parts.length; i += 2) {
|
||||||
|
let key = parts[i]
|
||||||
|
let val = Number(parts[i+1])
|
||||||
|
switch (key) {
|
||||||
|
case "Raw":
|
||||||
|
adjudication.raw = val
|
||||||
|
break
|
||||||
|
case "Plc":
|
||||||
|
adjudication.placing = val
|
||||||
|
break
|
||||||
|
case "AP":
|
||||||
|
adjudication.points = val
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
console.error(`Unknown key ${key} in row ${rowIndex} cell ${cellIndex}:`, cell)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
round.push(adjudication)
|
||||||
|
if (round.length == adjudicatorsPerRound) {
|
||||||
|
row.rounds.push(round)
|
||||||
|
round = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results.push(row)
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
parse
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
/**
|
||||||
|
* A collection of results
|
||||||
|
* @typedef {Array.<Result>} Results
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single result
|
||||||
|
*
|
||||||
|
* @typedef {Object} Result
|
||||||
|
* @property {String} name Competitor's name
|
||||||
|
* @property {Number} number Competitor's bib number
|
||||||
|
* @property {String} school Competitor's school
|
||||||
|
* @property {Number} overallPoints Overall award points for this competitor
|
||||||
|
* @property {Number} overallRank Overall ranking for this competitor
|
||||||
|
* @property {String} qualifier Any qualifiers this ranking earned
|
||||||
|
* @property {Array.<Round>} rounds How this competitor was judged in each round
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The results for one dancer for one round
|
||||||
|
*
|
||||||
|
* @typedef Round
|
||||||
|
* @type {Array.<Adjudication>}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One adjudicator's results for one dancer for one round
|
||||||
|
* @typedef {Object} Adjudication
|
||||||
|
* @property {String} adjudicator Adjudicator who recorded this score
|
||||||
|
* @property {Number} raw Raw score
|
||||||
|
* @property {Number} placing Placing relative to this adjudicator's other scores
|
||||||
|
* @property {Number} points Award points
|
||||||
|
* @property {Boolean} tie Whether this score was a tie
|
||||||
|
*/
|
|
@ -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 :)
|
|
@ -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!
|
|
@ -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!
|
|
@ -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)
|
|
@ -2,3 +2,15 @@
|
||||||
title: Blog
|
title: Blog
|
||||||
url: 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.
|
||||||
|
|
|
@ -5,13 +5,28 @@
|
||||||
<meta name="viewport" content="width=device-width">
|
<meta name="viewport" content="width=device-width">
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:regular,bold,italic">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:regular,bold,italic">
|
||||||
<!-- My stuff -->
|
<!-- My stuff -->
|
||||||
<link rel="stylesheet" media="screen" href="{{"/assets/css/default.css" | relURL}}">
|
|
||||||
<link rel="icon" type="image/png" href="{{"/assets/images/face.png" | relURL}}">
|
<link rel="icon" type="image/png" href="{{"/assets/images/face.png" | relURL}}">
|
||||||
|
<link rel="stylesheet" media="screen" href="{{"/assets/css/default.css" | relURL}}">
|
||||||
{{range .AlternativeOutputFormats}}
|
{{range .AlternativeOutputFormats}}
|
||||||
<link rel="{{.Rel}}" type="{{.MediaType.Type}}" title="{{$.Site.Title}}" href="{{.Permalink}}">
|
<link rel="{{.Rel}}" type="{{.MediaType.Type}}" title="{{$.Site.Title}}" href="{{.Permalink}}">
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{range .Params.stylesheets}}
|
||||||
|
{{$url := .}}
|
||||||
|
{{with $.Page.Resources.GetMatch .}}
|
||||||
|
{{$url = .RelPermalink}}
|
||||||
|
{{end}}
|
||||||
|
<link rel="stylesheet" media="screen" href="{{$url}}">
|
||||||
|
{{end}}
|
||||||
{{range .Params.scripts}}
|
{{range .Params.scripts}}
|
||||||
<script src="{{.}}"></script>
|
{{end}}
|
||||||
|
{{range .Params.scripts}}
|
||||||
|
{{$url := .}}
|
||||||
|
{{with $.Page.Resources.GetMatch .}}
|
||||||
|
{{$url = .RelPermalink}}
|
||||||
|
{{end}}
|
||||||
|
<script src="{{$url}}"
|
||||||
|
{{- if strings.HasSuffix . ".mjs"}}type="module"{{end -}}
|
||||||
|
></script>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{range .Params.headers}}
|
{{range .Params.headers}}
|
||||||
{{. | safeHTML}}
|
{{. | safeHTML}}
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
<figure>
|
||||||
|
{{- $img := $.Page.Resources.GetMatch (.Get "src")}}
|
||||||
|
<img src="{{$img.RelPermalink}}" alt="{{.Get "alt"}}">
|
||||||
|
</figure>
|
|
@ -1,4 +1,4 @@
|
||||||
<video autoplay muted loop>
|
<video autoplay muted loop>
|
||||||
<source src="{{.Get 0}}">
|
<source src="{{($.Page.Resources.GetMatch (.Get "src")).RelPermalink}}">
|
||||||
{{.Get 1}}
|
{{.Get "text"}}
|
||||||
</video>
|
</video>
|
||||||
|
|
5
run.sh
5
run.sh
|
@ -4,6 +4,9 @@ case "$(hostname)" in
|
||||||
sweetums)
|
sweetums)
|
||||||
baseURL=http://sweetums.lan:1313/
|
baseURL=http://sweetums.lan:1313/
|
||||||
;;
|
;;
|
||||||
|
penguin)
|
||||||
|
baseURL=http://penguin.linux.test:1313/
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
baseURL=http://$(hostname --fqdn):1313/
|
baseURL=http://$(hostname --fqdn):1313/
|
||||||
;;
|
;;
|
||||||
|
@ -15,4 +18,6 @@ docker run \
|
||||||
-u $(id -u):$(id -g) \
|
-u $(id -u):$(id -g) \
|
||||||
-p 1313:1313 \
|
-p 1313:1313 \
|
||||||
klakegg/hugo:ext server \
|
klakegg/hugo:ext server \
|
||||||
|
--buildFuture \
|
||||||
|
--buildDrafts \
|
||||||
--baseURL "$baseURL"
|
--baseURL "$baseURL"
|
||||||
|
|
|
@ -19,12 +19,13 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
font-family: "Lato", "Roboto", sans-serif;
|
font-family: inherit;
|
||||||
font-size: 13pt;
|
font-size: inherit;
|
||||||
border: 0;
|
border: thin solid #ccc;
|
||||||
outline: 0;
|
}
|
||||||
background: transparent;
|
input:read-only {
|
||||||
border-bottom: 1px solid black;
|
color: #444;
|
||||||
|
background-color: #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title, td.main {
|
.title, td.main {
|
||||||
|
@ -146,6 +147,36 @@ button.big {
|
||||||
background: inherit;
|
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 {
|
.tags {
|
||||||
font-size: small;
|
font-size: small;
|
||||||
}
|
}
|
||||||
|
@ -159,6 +190,10 @@ button.big {
|
||||||
img, video {
|
img, video {
|
||||||
max-width: 60%;
|
max-width: 60%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
figure img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
html {
|
html {
|
||||||
|
|
Loading…
Reference in New Issue