brrag 0.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. brrag-0.0.2/.github/workflows/check.yml +65 -0
  2. brrag-0.0.2/.github/workflows/release.yml +223 -0
  3. brrag-0.0.2/.gitignore +33 -0
  4. brrag-0.0.2/LICENSE +43 -0
  5. brrag-0.0.2/MLDRRoadFactors2025.xlsm +0 -0
  6. brrag-0.0.2/Makefile +89 -0
  7. brrag-0.0.2/PKG-INFO +200 -0
  8. brrag-0.0.2/README.md +171 -0
  9. brrag-0.0.2/assets/brrag-0.0.2-excel.zip +0 -0
  10. brrag-0.0.2/brrag/__init__.py +42 -0
  11. brrag-0.0.2/brrag/cli.py +161 -0
  12. brrag-0.0.2/brrag/core.py +273 -0
  13. brrag-0.0.2/brrag/data/mldr_road.json +17951 -0
  14. brrag-0.0.2/brrag/ui.py +72 -0
  15. brrag-0.0.2/brrag.egg-info/PKG-INFO +200 -0
  16. brrag-0.0.2/brrag.egg-info/SOURCES.txt +69 -0
  17. brrag-0.0.2/brrag.egg-info/dependency_links.txt +1 -0
  18. brrag-0.0.2/brrag.egg-info/entry_points.txt +2 -0
  19. brrag-0.0.2/brrag.egg-info/scm_file_list.json +66 -0
  20. brrag-0.0.2/brrag.egg-info/scm_version.json +8 -0
  21. brrag-0.0.2/brrag.egg-info/top_level.txt +1 -0
  22. brrag-0.0.2/data/mldr_road.json +17951 -0
  23. brrag-0.0.2/data/source_sha256.txt +4 -0
  24. brrag-0.0.2/docs/01-concepts.md +145 -0
  25. brrag-0.0.2/docs/02-tables-2025.md +168 -0
  26. brrag-0.0.2/docs/03-algorithm.md +237 -0
  27. brrag-0.0.2/docs/04-methodology.md +248 -0
  28. brrag-0.0.2/docs/05-provenance.md +155 -0
  29. brrag-0.0.2/docs/06-edge-cases.md +88 -0
  30. brrag-0.0.2/docs/07-implementation.md +168 -0
  31. brrag-0.0.2/docs/08-validation.md +132 -0
  32. brrag-0.0.2/docs/09-history.md +90 -0
  33. brrag-0.0.2/docs/10-excel-addin.md +90 -0
  34. brrag-0.0.2/docs/11-releasing.md +155 -0
  35. brrag-0.0.2/docs/glossary.md +96 -0
  36. brrag-0.0.2/docs/index.md +43 -0
  37. brrag-0.0.2/docs/references.md +71 -0
  38. brrag-0.0.2/page.html +829 -0
  39. brrag-0.0.2/pyproject.toml +55 -0
  40. brrag-0.0.2/requirements-tools.txt +11 -0
  41. brrag-0.0.2/setup.cfg +4 -0
  42. brrag-0.0.2/tools/extract_tables.py +73 -0
  43. brrag-0.0.2/tools/js_oracle.js +133 -0
  44. brrag-0.0.2/tools/render_wiki.py +341 -0
  45. brrag-0.0.2/tools/reproduce_curve.py +150 -0
  46. brrag-0.0.2/tools/sync_package_data.py +52 -0
  47. brrag-0.0.2/tools/verify.py +251 -0
  48. brrag-0.0.2/tools/wheel_smoke.py +97 -0
  49. brrag-0.0.2/xll/Makefile +124 -0
  50. brrag-0.0.2/xll/README.md +239 -0
  51. brrag-0.0.2/xll/docs/ad-hoc-tables.md +183 -0
  52. brrag-0.0.2/xll/docs/framework-patches.md +357 -0
  53. brrag-0.0.2/xll/docs/functions.md +249 -0
  54. brrag-0.0.2/xll/docs/tables-and-caching.md +116 -0
  55. brrag-0.0.2/xll/examples/age-grading-sample.xlsx +0 -0
  56. brrag-0.0.2/xll/examples/build_sample_workbook.py +1810 -0
  57. brrag-0.0.2/xll/examples/tables/bellville-vets.csv +3 -0
  58. brrag-0.0.2/xll/excel4_compat.cpp +217 -0
  59. brrag-0.0.2/xll/patches/xll-mingw-portability.patch +161 -0
  60. brrag-0.0.2/xll/road_running_age_grading.def +33 -0
  61. brrag-0.0.2/xll/src/ag_api.cpp +418 -0
  62. brrag-0.0.2/xll/src/ag_api.h +96 -0
  63. brrag-0.0.2/xll/src/ag_core.cpp +1017 -0
  64. brrag-0.0.2/xll/src/ag_core.h +237 -0
  65. brrag-0.0.2/xll/src/ag_tables_generated.h +2167 -0
  66. brrag-0.0.2/xll/src/road_running_age_grading.cpp +407 -0
  67. brrag-0.0.2/xll/test/expected.h +487 -0
  68. brrag-0.0.2/xll/test/test_main.cpp +575 -0
  69. brrag-0.0.2/xll/tools/apply-xll-patches.sh +45 -0
  70. brrag-0.0.2/xll/tools/gen_expectations.py +100 -0
  71. brrag-0.0.2/xll/tools/gen_tables.py +196 -0
@@ -0,0 +1,65 @@
1
+ name: check
2
+
3
+ # Proves three things on every push:
4
+ # 1. the committed data/mldr_road.json really is what the published calculator
5
+ # page says (re-extracted and diffed);
6
+ # 2. the published artefacts have not changed (SHA-256);
7
+ # 3. the implementation still reproduces the calculator and the workbooks.
8
+
9
+ on:
10
+ push:
11
+ branches: [main]
12
+ pull_request:
13
+ schedule:
14
+ # monthly: tells us when the upstream tables or calculator change
15
+ - cron: "0 6 1 * *"
16
+ workflow_dispatch:
17
+
18
+ jobs:
19
+ check:
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+
24
+ - uses: actions/setup-python@v5
25
+ with:
26
+ python-version: "3.12"
27
+
28
+ - name: Install tool dependencies
29
+ run: |
30
+ python -m pip install --quiet --upgrade pip
31
+ python -m pip install --quiet -r requirements-tools.txt
32
+
33
+ - name: Fetch sources (calculator page, workbook, upstream tables repo)
34
+ run: make fetch
35
+
36
+ - name: Re-extract the factor tables and require a clean diff
37
+ run: |
38
+ make data
39
+ if ! git diff --exit-code -- data/mldr_road.json; then
40
+ echo "::error file=data/mldr_road.json::Published tables changed since data/ was extracted"
41
+ exit 1
42
+ fi
43
+
44
+ - name: Validate (exact grid, JS oracle, standards cross-check)
45
+ run: make verify
46
+
47
+ - name: Reproduce the fitted curves from their parameters
48
+ run: make curve
49
+
50
+ - name: Check documentation links
51
+ run: make check-links
52
+
53
+ # The Excel add-in's portable core: table lookup, vector handling, both
54
+ # ad hoc table layouts, caching. Building the .xll itself needs mingw-w64
55
+ # and the mingw-patched xll library, so that stays a local step.
56
+ - name: Excel add-in — generated files up to date
57
+ run: |
58
+ python3 xll/tools/gen_tables.py --check
59
+ python3 xll/tools/gen_expectations.py --check
60
+
61
+ - name: Excel add-in — core test suite
62
+ run: make xll-test
63
+
64
+ - name: Excel add-in — sample workbook up to date
65
+ run: python3 xll/examples/build_sample_workbook.py --check
@@ -0,0 +1,223 @@
1
+ name: release
2
+
3
+ # A tag starting with v is the whole release procedure. The version is not in
4
+ # any file: setuptools-scm derives it from the tag, so `git tag v0.0.2` publishes
5
+ # 0.0.2 and an untagged commit cannot ship by accident.
6
+
7
+ on:
8
+ push:
9
+ tags: ["v*"]
10
+ workflow_dispatch:
11
+ inputs:
12
+ note:
13
+ description: "Optional note. A dispatch builds and tests but publishes nothing (no tag)."
14
+ required: false
15
+
16
+ permissions:
17
+ contents: read
18
+
19
+ concurrency:
20
+ group: release-${{ github.ref }}
21
+ cancel-in-progress: false
22
+
23
+ jobs:
24
+ build:
25
+ name: build and test the distribution
26
+ runs-on: ubuntu-latest
27
+ outputs:
28
+ version: ${{ steps.version.outputs.version }}
29
+ steps:
30
+ - uses: actions/checkout@v4
31
+ with:
32
+ # setuptools-scm needs the tags, so a shallow clone is not enough.
33
+ fetch-depth: 0
34
+
35
+ - uses: actions/setup-python@v5
36
+ with:
37
+ python-version: "3.12"
38
+
39
+ - name: Install build tools
40
+ run: python -m pip install --upgrade build
41
+
42
+ - name: Build sdist and wheel
43
+ run: python -m build
44
+
45
+ - name: The version must be the tag
46
+ id: version
47
+ run: |
48
+ set -euo pipefail
49
+ built=$(ls dist/*.whl | sed -E 's|^dist/brrag-([^-]+)-.*$|\1|')
50
+ echo "built $built"
51
+ echo "version=$built" >> "$GITHUB_OUTPUT"
52
+ if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
53
+ tag="${GITHUB_REF_NAME#v}"
54
+ if [ "$built" != "$tag" ]; then
55
+ echo "::error::tag is $GITHUB_REF_NAME but the build is $built - bump or move the tag"
56
+ exit 1
57
+ fi
58
+ else
59
+ echo "::notice::not a tag, so nothing will be published (version $built)"
60
+ fi
61
+
62
+ - name: The wheel must carry its own tables
63
+ run: |
64
+ set -euo pipefail
65
+ python -m venv /tmp/wheeltest
66
+ /tmp/wheeltest/bin/pip install --quiet --disable-pip-version-check dist/*.whl
67
+ # Run from outside the checkout: brrag must not fall back to ../data.
68
+ cd /tmp
69
+ /tmp/wheeltest/bin/python "$GITHUB_WORKSPACE/tools/wheel_smoke.py" "${{ steps.version.outputs.version }}"
70
+
71
+ - name: Install the mingw cross-compiler
72
+ run: |
73
+ set -euo pipefail
74
+ sudo apt-get update -qq
75
+ sudo apt-get install -y -qq --no-install-recommends g++-mingw-w64-x86-64
76
+
77
+ - name: Check out the xll framework this build is pinned to
78
+ run: |
79
+ set -euo pipefail
80
+ # Pinned to the commit xll/patches/xll-mingw-portability.patch applies to:
81
+ # a moving upstream would silently change the .xll we ship.
82
+ XLL_DIR="$HOME/code/xll"
83
+ mkdir -p "$XLL_DIR"
84
+ git -C "$XLL_DIR" init -q
85
+ git -C "$XLL_DIR" remote add origin https://github.com/xlladdins/xll
86
+ git -C "$XLL_DIR" fetch -q --depth 1 origin 481178bced1022f0b36b582388b79a6eabe1a11f
87
+ git -C "$XLL_DIR" checkout -q FETCH_HEAD
88
+ git -C "$XLL_DIR" rev-parse --short HEAD
89
+ ./xll/tools/apply-xll-patches.sh
90
+
91
+ - name: Build the add-in and audit its exports
92
+ run: |
93
+ set -euo pipefail
94
+ make -C xll shim
95
+ make -C xll xll verify
96
+ # The link is timestamp-free, so with THIS compiler the binary is
97
+ # reproducible byte for byte. Record the version, because a different
98
+ # mingw produces a different binary from identical sources.
99
+ x86_64-w64-mingw32-g++ --version | head -1
100
+ sha256sum xll/road_running_age_grading.xll
101
+ x86_64-w64-mingw32-objdump -f xll/road_running_age_grading.xll | head -3
102
+ strings xll/road_running_age_grading.xll | grep -c '^RRAG\.' | xargs -I{} test {} -eq 12
103
+
104
+ - name: Package the Excel bundle
105
+ run: |
106
+ set -euo pipefail
107
+ python -m pip install --quiet "openpyxl>=3.1"
108
+ # --check rebuilds it and fails if the committed copy is stale, so the
109
+ # asset is known to match this commit rather than merely to exist.
110
+ python xll/examples/build_sample_workbook.py --check
111
+ version="${{ steps.version.outputs.version }}"
112
+ mkdir -p assets
113
+ python - "$version" <<'PY'
114
+ import sys, zipfile
115
+ version = sys.argv[1]
116
+ target = f"assets/brrag-{version}-excel.zip"
117
+ with zipfile.ZipFile(target, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as z:
118
+ z.write("xll/road_running_age_grading.xll", "road_running_age_grading.xll")
119
+ z.write("xll/examples/age-grading-sample.xlsx", "age-grading-sample.xlsx")
120
+ with zipfile.ZipFile(target) as z:
121
+ names = z.namelist()
122
+ for i in z.infolist():
123
+ print(f" {i.filename:32} {i.file_size:>9,} -> {i.compress_size:>8,}")
124
+ assert names == ["road_running_age_grading.xll", "age-grading-sample.xlsx"], names
125
+ PY
126
+ ls -l dist/ assets/
127
+
128
+ # Two artifacts on purpose: `dist` is exactly what twine may upload, and
129
+ # gh-action-pypi-publish rejects anything else in it (a stray .zip is read
130
+ # as an sdist: "Too many top-level members in sdist archive").
131
+ - name: Upload the distribution
132
+ uses: actions/upload-artifact@v4
133
+ with:
134
+ name: dist
135
+ path: dist/
136
+ if-no-files-found: error
137
+
138
+ - name: Upload the Excel bundle
139
+ uses: actions/upload-artifact@v4
140
+ with:
141
+ name: assets
142
+ path: assets/
143
+ if-no-files-found: error
144
+
145
+ github-release:
146
+ name: publish the GitHub release
147
+ needs: build
148
+ if: github.ref_type == 'tag'
149
+ runs-on: ubuntu-latest
150
+ permissions:
151
+ contents: write
152
+ steps:
153
+ - uses: actions/download-artifact@v4
154
+ with:
155
+ name: dist
156
+ path: dist
157
+
158
+ - uses: actions/download-artifact@v4
159
+ with:
160
+ name: assets
161
+ path: assets
162
+
163
+ - name: Checksums
164
+ run: |
165
+ set -euo pipefail
166
+ # Named explicitly: `sha256sum *` would include the file being written.
167
+ sha256sum dist/* assets/* > SHA256SUMS
168
+ cat SHA256SUMS
169
+
170
+ - name: Create the release
171
+ env:
172
+ GH_TOKEN: ${{ github.token }}
173
+ GH_REPO: ${{ github.repository }}
174
+ run: |
175
+ set -euo pipefail
176
+ if gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then
177
+ echo "::error::a release for $GITHUB_REF_NAME already exists - bump the version instead of reusing a tag"
178
+ exit 1
179
+ fi
180
+ gh release create "$GITHUB_REF_NAME" dist/* assets/* SHA256SUMS \
181
+ --title "$GITHUB_REF_NAME" \
182
+ --generate-notes \
183
+ --verify-tag
184
+ gh release view "$GITHUB_REF_NAME" --json url,assets \
185
+ --jq '.url, (.assets[] | " " + .name + " " + (.size|tostring) + " B")'
186
+
187
+ pypi:
188
+ name: publish to PyPI
189
+ needs: build
190
+ # Trusted publishing: no token, no secret - PyPI verifies this workflow's
191
+ # OIDC identity instead. Set the repository variable PYPI_ENABLED=true once
192
+ # the pending publisher exists on pypi.org; see docs/11-releasing.md.
193
+ if: github.ref_type == 'tag' && vars.PYPI_ENABLED == 'true'
194
+ runs-on: ubuntu-latest
195
+ environment:
196
+ name: pypi
197
+ url: https://pypi.org/p/brrag
198
+ permissions:
199
+ id-token: write
200
+ steps:
201
+ - uses: actions/download-artifact@v4
202
+ with:
203
+ name: dist
204
+ path: dist
205
+
206
+ - name: Publish
207
+ uses: pypa/gh-action-pypi-publish@release/v1
208
+
209
+ - name: Prove it landed
210
+ run: |
211
+ set -euo pipefail
212
+ version="${{ needs.build.outputs.version }}"
213
+ for _ in $(seq 1 12); do
214
+ if curl -fsS "https://pypi.org/pypi/brrag/${version}/json" >/dev/null; then
215
+ echo "brrag ${version} is on PyPI"
216
+ curl -fsS "https://pypi.org/pypi/brrag/${version}/json" \
217
+ | python -c 'import json,sys; d=json.load(sys.stdin); print("\n".join(" "+u["filename"] for u in d["urls"]))'
218
+ exit 0
219
+ fi
220
+ sleep 5
221
+ done
222
+ echo "::error::PyPI does not list brrag ${version} yet"
223
+ exit 1
brrag-0.0.2/.gitignore ADDED
@@ -0,0 +1,33 @@
1
+ # virtualenvs and caches
2
+ .venv/
3
+ __pycache__/
4
+ *.pyc
5
+
6
+ # unzipped copies of source artefacts (regenerate with `make unpack`)
7
+ raw/
8
+
9
+ # generated documentation HTML (regenerate with `make wiki`)
10
+ wiki/
11
+
12
+ # upstream clone for the standards cross-check (regenerate with `make fetch`)
13
+ repo/
14
+
15
+ # extracted text of upstream PDFs
16
+ notes/
17
+
18
+ # editable-install metadata
19
+ *.egg-info/
20
+
21
+ # build outputs (`make dist`); releases carry them, git does not
22
+ dist/
23
+ build/
24
+
25
+ # --- Excel add-in build artefacts (xll/) -----------------------------------
26
+ xll/*.o
27
+ xll/*.xll
28
+ xll/road_running_age_grading_test
29
+ xll_test_tmp/
30
+ # case-fixing header symlinks, created by `make -C xll shim`
31
+ xll/shim/
32
+ # The generated sources ARE committed on purpose (a Windows/MSVC user must not
33
+ # need Python): xll/src/ag_tables_generated.h and xll/test/expected.h.
brrag-0.0.2/LICENSE ADDED
@@ -0,0 +1,43 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Francois Botha
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ SCOPE
26
+
27
+ This licence covers the code in this repository: the `brrag/` package, the
28
+ `tools/` scripts and the prose in `docs/` and `README.md`.
29
+
30
+ It does NOT cover the age-grading tables or the source artefacts. Those are the
31
+ work of others:
32
+
33
+ * age-grade factors and age standards — compiled by Alan Jones
34
+ (alanjones@grests.com) with single-age bests by Tom Bernhard, approved by
35
+ the Masters Long Distance Running Council of USA Track & Field.
36
+ Upstream: https://github.com/AlanLyttonJones/Age-Grade-Tables
37
+ * the calculator page (`page.html`) and the factors workbook
38
+ (`MLDRRoadFactors2025.xlsm`) — Howard Grubb,
39
+ https://www.howardgrubb.co.uk/athletics/
40
+
41
+ They are included here unmodified, for reference and reproducibility, with
42
+ SHA-256 hashes recorded in `data/source_sha256.txt`. Cite the upstream sources,
43
+ not this repository, when publishing graded results.
Binary file
brrag-0.0.2/Makefile ADDED
@@ -0,0 +1,89 @@
1
+ # Age-grading docs + reference implementation.
2
+ # `make check` is what CI (or a future table revision) should run.
3
+
4
+ PY ?= $(shell [ -x .venv/bin/python ] && echo .venv/bin/python || echo python3)
5
+ UPSTREAM := https://github.com/AlanLyttonJones/Age-Grade-Tables.git
6
+
7
+ .PHONY: help all setup fetch unpack data pkgdata dist verify curve wiki check check-links xll-test sample clean
8
+
9
+ help:
10
+ @echo "setup create .venv and install tool dependencies"
11
+ @echo "fetch download the source artefacts (calculator page, xlsm, upstream clone)"
12
+ @echo "unpack unzip the xlsm (sheet XML + vbaProject.bin) into raw/"
13
+ @echo "data re-extract factor tables from page.html -> data/mldr_road.json"
14
+ @echo "pkgdata copy data/mldr_road.json into the package, so a wheel ships its tables"
15
+ @echo "dist build the sdist and wheel into dist/"
16
+ @echo "verify run checks A/B/C (exact grid, JS oracle, standards cross-check)"
17
+ @echo "curve reproduce factor curves from the fitted parameters"
18
+ @echo "wiki render docs/*.md -> wiki/*.html"
19
+ @echo "check verify + curve + wiki link check <-- the CI target"
20
+ @echo "check-links verify every docs/ cross-page link and anchor"
21
+ @echo "xll-test verify the Excel add-in core (needs g++ only)"
22
+ @echo "sample regenerate the sample workbook (needs openpyxl)"
23
+ @echo "all fetch + unpack + data + verify + curve + wiki"
24
+
25
+ all: fetch unpack data verify curve wiki
26
+ @echo OK
27
+
28
+ setup:
29
+ uv venv --python 3.12 .venv
30
+ uv pip install --python .venv/bin/python -r requirements-tools.txt
31
+
32
+ fetch:
33
+ @echo "-> calculator page"
34
+ curl -sS -o page.html https://www.howardgrubb.co.uk/athletics/mldrroad25.html
35
+ @echo "-> factors workbook"
36
+ curl -sS -o MLDRRoadFactors2025.xlsm https://www.howardgrubb.co.uk/athletics/data/MLDRRoadFactors2025.xlsm
37
+ @echo "-> upstream tables repository (needed by check C)"
38
+ test -d repo || git clone --depth 1 $(UPSTREAM) repo
39
+ @echo "-> verifying hashes"; sha256sum -c data/source_sha256.txt
40
+
41
+ unpack:
42
+ rm -rf raw && mkdir -p raw && cd raw && unzip -o ../MLDRRoadFactors2025.xlsm >/dev/null
43
+ @echo "raw/ holds the sheet XML and xl/vbaProject.bin; dump the macros with:"
44
+ @echo " .venv/bin/olevba raw/xl/vbaProject.bin"
45
+
46
+ data:
47
+ $(PY) tools/extract_tables.py page.html data/mldr_road.json
48
+ $(PY) tools/sync_package_data.py
49
+
50
+ pkgdata:
51
+ $(PY) tools/sync_package_data.py
52
+
53
+ # setuptools-scm takes the version from the git tag, so this is only meaningful
54
+ # (and only publishable) on a tagged commit.
55
+ dist:
56
+ $(PY) -m build
57
+
58
+ verify:
59
+ $(PY) tools/verify.py --all
60
+
61
+ curve:
62
+ $(PY) tools/reproduce_curve.py
63
+
64
+ wiki:
65
+ $(PY) tools/render_wiki.py
66
+
67
+ check-links:
68
+ $(PY) tools/render_wiki.py --check
69
+
70
+ # The add-in's portable core and test suite. Building the .xll itself needs
71
+ # mingw-w64 and the patched xll library; see xll/README.md.
72
+ xll-test:
73
+ $(MAKE) -C xll test
74
+
75
+ sample:
76
+ $(PY) xll/examples/build_sample_workbook.py
77
+
78
+ check:
79
+ $(PY) tools/sync_package_data.py --check && \
80
+ $(PY) tools/verify.py --all && \
81
+ $(PY) tools/reproduce_curve.py && \
82
+ $(PY) tools/render_wiki.py --check && \
83
+ $(PY) xll/tools/gen_tables.py --check && \
84
+ $(PY) xll/tools/gen_expectations.py --check && \
85
+ $(MAKE) -C xll test && \
86
+ $(PY) xll/examples/build_sample_workbook.py --check
87
+
88
+ clean:
89
+ rm -rf wiki raw
brrag-0.0.2/PKG-INFO ADDED
@@ -0,0 +1,200 @@
1
+ Metadata-Version: 2.4
2
+ Name: brrag
3
+ Version: 0.0.2
4
+ Summary: BRRAG - Botha's Road Running Age Grading on the USATF MLDR 2025 tables
5
+ Author-email: Francois Botha <igitur@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/igitur/road-running-age-grading
8
+ Project-URL: Documentation, https://github.com/igitur/road-running-age-grading/blob/main/docs/index.md
9
+ Project-URL: Source, https://github.com/igitur/road-running-age-grading
10
+ Project-URL: Changelog, https://github.com/igitur/road-running-age-grading/releases
11
+ Project-URL: Published calculator, https://www.howardgrubb.co.uk/athletics/mldrroad25.html
12
+ Keywords: running,age-grading,usatf,mldr,road-running,athletics
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Games/Entertainment
24
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
25
+ Requires-Python: >=3.10
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Dynamic: license-file
29
+
30
+ # BRRAG — Botha's Road Running Age Grading
31
+
32
+ Age grading on the USATF MLDR 2025 road tables. The Python package, the
33
+ CLI and the PyPI distribution are called `brrag`; the repository keeps its
34
+ name and URL.
35
+
36
+ [![check](https://github.com/igitur/road-running-age-grading/actions/workflows/check.yml/badge.svg)](https://github.com/igitur/road-running-age-grading/actions/workflows/check.yml)
37
+
38
+ Reference documentation and a verified implementation of **road-running age
39
+ grading** on the USATF Masters Long Distance Running (MLDR) 2025 tables.
40
+
41
+ You give it a sex, an age, a distance and a time. It tells you the **age-graded
42
+ time** and the **age grade percentage** — one number that lets a 12-year-old's
43
+ 5 km and a 72-year-old's marathon be compared on the same scale.
44
+
45
+ ```console
46
+ $ brrag 40:00 --age 40 --sex M --distance 10k
47
+ factor 0.9636
48
+ age-graded 38:32.6
49
+ age grade 68.49%
50
+
51
+ $ brrag 32:00 --age 65 --sex M --distance 10k
52
+ factor 0.7761
53
+ age-graded 24:50.1
54
+ age grade 106.30% # above 100% = better than the age standard
55
+ ```
56
+
57
+ ## Install
58
+
59
+ ```console
60
+ $ pip install brrag
61
+ $ brrag 40:00 --age 40 --sex M --distance 10k
62
+ ```
63
+
64
+ Stdlib-only, no dependencies, and the wheel carries its own tables so it works
65
+ offline. Versions come from the git tag; see
66
+ [docs/11-releasing.md](docs/11-releasing.md).
67
+
68
+ ## Where to start
69
+
70
+ | If you want to… | Read |
71
+ | --- | --- |
72
+ | Understand the idea and the numbers | [docs/01-concepts.md](docs/01-concepts.md) |
73
+ | See what the 2025 tables contain | [docs/02-tables-2025.md](docs/02-tables-2025.md) |
74
+ | **Grade a time (the algorithm)** | [docs/03-algorithm.md](docs/03-algorithm.md) |
75
+ | Know how the tables were built | [docs/04-methodology.md](docs/04-methodology.md) |
76
+ | Trust or audit the data | [docs/05-provenance.md](docs/05-provenance.md) |
77
+ | Handle weird inputs | [docs/06-edge-cases.md](docs/06-edge-cases.md) |
78
+ | Write your own implementation | [docs/07-implementation.md](docs/07-implementation.md) |
79
+ | Check our claims | [docs/08-validation.md](docs/08-validation.md) |
80
+ | Know the revision history / what 2026 needs | [docs/09-history.md](docs/09-history.md) |
81
+ | **Grade a whole club in Excel** | [docs/10-excel-addin.md](docs/10-excel-addin.md) |
82
+ | Release or publish a version | [docs/11-releasing.md](docs/11-releasing.md) |
83
+
84
+ Full index: [docs/index.md](docs/index.md).
85
+
86
+ ## Quick use (Python)
87
+
88
+ ```python
89
+ from brrag import table, format_time
90
+
91
+ t = table(2025, "M") # or table(2025, "F")
92
+ g = t.grade("M", 40, 40 * 60, dist_km=10) # 40:00 for 10 km at age 40
93
+ print(g.factor, g.percent) # 0.9636 68.49
94
+ print(format_time(g.age_graded_s)) # 38:32.6
95
+ ```
96
+
97
+ No dependencies outside the standard library for the library itself
98
+ (`openpyxl` is only needed by the table-extraction and verification tools).
99
+
100
+ ## In Excel
101
+
102
+ `xll/road_running_age_grading.xll` is a native add-in for grading a whole squad at once:
103
+
104
+ ```excel
105
+ =RRAG.GRADE(B2:B181, C2:C181, D2:D181, E2:E181) 180 age grades, spilled
106
+ =RRAG.BLOCK(B2:B181, C2:C181, D2:D181, E2:E181) factor, standards, graded time, percent
107
+ ```
108
+
109
+ Four editions are compiled in (2025/2020/2015/2010), the fifth argument selects
110
+ one or names a custom `<name>.csv` table, and a range passed there is used as an
111
+ ad-hoc table. Build and install: [xll/README.md](xll/README.md); design notes:
112
+ [docs/10-excel-addin.md](docs/10-excel-addin.md).
113
+
114
+ ## Getting started
115
+
116
+ ```console
117
+ $ git clone https://github.com/igitur/road-running-age-grading.git
118
+ $ cd road-running-age-grading
119
+ $ make setup # .venv + openpyxl/oletools/pypdf
120
+ $ make check # the full validation suite
121
+ ```
122
+
123
+ The repo ships `data/mldr_road.json`, so the library and CLI work immediately
124
+ with nothing but Python 3.10+. `make fetch` downloads the two source artefacts
125
+ and clones the upstream tables repository when you want checks B and C to run
126
+ (after a fresh clone they are already present, since the artefacts are committed).
127
+
128
+ ## Layout
129
+
130
+ ```
131
+ data/mldr_road.json 2025 (+2020/2015/2010) factors, extracted and hashed
132
+ data/source_sha256.txt hashes of every source artefact we read
133
+ brrag/ reference implementation (core.py, ui.py, cli.py)
134
+ tools/extract_tables.py pull factor tables out of the calculator page -> JSON
135
+ tools/js_oracle.js run the *published calculator's own JS* under Node
136
+ tools/verify.py three-way validation harness (A/B/C)
137
+ tools/reproduce_curve.py rebuild each factor curve from its 10 fitted parameters
138
+ tools/render_wiki.py docs/*.md -> wiki/*.html (no JS, no CDN)
139
+ xll/ native Excel add-in: columns in, age grades out
140
+ docs/ the documentation set (Markdown; source of truth)
141
+ page.html archived copy of the calculator page we read
142
+ MLDRRoadFactors2025.xlsm archived copy of the official factors workbook (VBA)
143
+ ```
144
+
145
+ Generated or fetched, not committed: `wiki/` (`make wiki`), `raw/` (`make unpack`),
146
+ `repo/` and `notes/` (`make fetch` + `pypdf`).
147
+
148
+ `make help` lists the tasks; `make all` re-downloads the sources, re-extracts the
149
+ data, validates it, reproduces the curves and re-renders the wiki.
150
+
151
+ ## Verify it
152
+
153
+ ```console
154
+ $ make check
155
+ A. exact-event grid: 4224 lookups, 0 mismatches
156
+ B. JS oracle: 1412 requests, 0 mismatches, worst percent delta 0.000000 pp
157
+ C. standards file: 4224 cells, 0 mismatches, 18 known anomalies
158
+ RESULT: PASS
159
+ worst absolute error across the curves that match exactly: 0.0001
160
+ links: 0 problem(s)
161
+ ```
162
+
163
+ Details and what each check catches: [docs/08-validation.md](docs/08-validation.md).
164
+
165
+ ## Reading it as a wiki
166
+
167
+ The Markdown in `docs/` is the source of truth — it versions with the data, diffs
168
+ cleanly, and renders on GitHub. If you would rather click through it, render the
169
+ HTML copy and serve it:
170
+
171
+ ```console
172
+ $ .venv/bin/python tools/render_wiki.py # writes wiki/*.html
173
+ ```
174
+
175
+ Twelve self-contained pages, a sticky sidebar, no JavaScript, no CDN, no build
176
+ step beyond that one command — which is why it drops straight into the existing
177
+ Caddy static-site setup if you want it online.
178
+
179
+ ## Status of the tables
180
+
181
+ The 2025 road tables were approved by the USATF MLDR Council on **2025-01-10**;
182
+ the published workbooks are dated **2025-07-27**. The tables are re-fitted
183
+ roughly every five years (2010 → 2015 → 2020 → 2025), so treat 2025 as current
184
+ and expect a 2030 revision. Everything in this repo is pinned to the published
185
+ 2025 artefacts by SHA-256 — see [docs/05-provenance.md](docs/05-provenance.md).
186
+
187
+ One caveat worth knowing up front: the *working* spreadsheets shipped in the
188
+ upstream repo (`maleRoad2025.xlsx`, dated 2025-07-25) do **not** reproduce the
189
+ published male 5 km / 10 km curves. The published factor workbook, the
190
+ calculator and the `…Std2025.xlsx` standards files all agree with each other;
191
+ the male working file is one revision behind. [docs/05-provenance.md](docs/05-provenance.md)
192
+ has the numbers.
193
+
194
+ ## Licence / attribution
195
+
196
+ Tables compiled by **Alan Jones** (alanjones@grests.com) with single-age bests
197
+ by **Tom Bernhard**; approved by the USATF MLDR Council. Calculator by
198
+ **Howard Grubb** (howardgrubb.co.uk). Upstream repository:
199
+ <https://github.com/AlanLyttonJones/Age-Grade-Tables>. Cite the upstream tables,
200
+ not this repo, when publishing results.