Commit ca0597b4 authored by Sean Stangl's avatar Sean Stangl

Add the AH (Haleczko) formula to the Meet page for ParaPL

parent 47c1faf0
Pipeline #81596334 passed with stage
in 25 minutes and 39 seconds
//! Definition of the AH Formula, also called Haleczko. Used by ParaPL.
use opltypes::*;
/// Calculates the AH coefficient for men.
///
/// The full formula is defined in Excel:
/// =ROUND($AM$1/(POWER(LOG(I13),$AM$2))*M13,2)
///
/// Where:
/// I13: Bodyweight
/// M13: Lift Attempt
/// AM1: 3.2695
/// AM2: 1.95
pub fn ah_coefficient_men(bodyweightkg: f64) -> f64 {
const AM1: f64 = 3.2695;
const AM2: f64 = 1.95;
// Upper bound avoids asymptote.
// Lower bound avoids children with huge coefficients.
let adjusted = bodyweightkg.max(32.0).min(157.0);
AM1 / adjusted.log10().powf(AM2)
}
/// Calculates the AH coefficient for women.
///
/// The full formula is defined in Excel:
/// =ROUND($AG$1/(POWER(LOG(I13),$AG$10))*M13,2)
///
/// Where:
/// I13: Bodyweight
/// M13: Lift Attempt
/// AG1: 2.7566
/// AG10: 1.8
pub fn ah_coefficient_women(bodyweightkg: f64) -> f64 {
const AG1: f64 = 2.7566;
const AG10: f64 = 1.8;
// Upper bound avoids asymptote.
// Lower bound avoids children with huge coefficients.
let adjusted = bodyweightkg.max(28.0).min(112.0);
AG1 / adjusted.log10().powf(AG10)
}
/// Calculates AH points, used by ParaPL for bench-only competitions.
///
/// https://www.paralympic.org/sites/default/files/document/130801141325417_Appendix_2_AH_Haleczko_Formula.pdf
pub fn ah(sex: Sex, bodyweight: WeightKg, total: WeightKg) -> Points {
if bodyweight.is_zero() || total.is_zero() {
return Points::from_i32(0);
}
let coefficient: f64 = match sex {
Sex::M => ah_coefficient_men(f64::from(bodyweight)),
Sex::F => ah_coefficient_women(f64::from(bodyweight)),
};
Points::from(coefficient * f64::from(total))
}
#[cfg(test)]
mod tests {
use super::*;
/// Tests whether two floating-point numbers are equal to six decimal places,
/// as published in the official AH coefficient tables.
fn matches_table(a: f64, b: f64) -> bool {
const FIGS: f64 = 1000000.0;
(a * FIGS).round() == (b * FIGS).round()
}
#[test]
fn male_coefficients() {
assert!(matches_table(ah_coefficient_men(32.0), 1.472993));
assert!(matches_table(ah_coefficient_men(60.0), 1.064247));
assert!(matches_table(ah_coefficient_men(80.0), 0.932257));
assert!(matches_table(ah_coefficient_men(100.0), 0.846200));
assert!(matches_table(ah_coefficient_men(117.0), 0.792650));
assert!(matches_table(ah_coefficient_men(144.0), 0.729355));
assert!(matches_table(ah_coefficient_men(157.0), 0.705240));
}
#[test]
fn female_coefficients() {
assert!(matches_table(ah_coefficient_women(28.0), 1.417245));
assert!(matches_table(ah_coefficient_women(35.0), 1.261172));
assert!(matches_table(ah_coefficient_women(48.0), 1.082031));
assert!(matches_table(ah_coefficient_women(70.0), 0.915248));
assert!(matches_table(ah_coefficient_women(89.0), 0.829003));
assert!(matches_table(ah_coefficient_women(100.0), 0.791625));
assert!(matches_table(ah_coefficient_women(112.0), 0.757731));
}
}
extern crate opltypes;
mod ah;
pub use crate::ah::ah;
mod dots;
pub use crate::dots::dots;
......
......@@ -1996,7 +1996,7 @@ impl Federation {
Federation::OceaniaPF => PointsSystem::Wilks,
Federation::ORPF => Federation::ipf_rules_on(date),
Federation::OEVK => Federation::ipf_rules_on(date),
Federation::ParaPL => PointsSystem::Wilks,
Federation::ParaPL => PointsSystem::AH,
Federation::PA => PointsSystem::Wilks,
Federation::PAP => Federation::ipf_rules_on(date),
Federation::PHPL => PointsSystem::Reshel,
......
......@@ -21,6 +21,7 @@ pub struct Points(i32);
/// Enum of known powerlifting points systems, like Wilks and Glossbrenner.
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum PointsSystem {
AH,
Glossbrenner,
IPFPoints,
NASA,
......
......@@ -5,6 +5,7 @@
'use strict';
// These are generated inline via templates/meet.html.tera.
declare const path_if_by_ah: string;
declare const path_if_by_division: string;
declare const path_if_by_glossbrenner: string;
declare const path_if_by_ipfpoints: string;
......@@ -19,6 +20,9 @@ let selSort: HTMLSelectElement;
// change to match.
function redirect() {
switch (selSort.value) {
case "by-ah":
window.location.href = path_if_by_ah;
break;
case "by-division":
window.location.href = path_if_by_division;
break;
......
......@@ -434,6 +434,7 @@ pub struct SortSelectorTranslations {
pub by_bench: String,
pub by_deadlift: String,
pub by_total: String,
pub by_ah: String,
pub by_allometric: String,
pub by_glossbrenner: String,
pub by_ipfpoints: String,
......
......@@ -254,6 +254,30 @@ pub fn cmp_reshel(meets: &[Meet], a: &Entry, b: &Entry) -> cmp::Ordering {
.then(a.totalkg.cmp(&b.totalkg).reverse())
}
/// Defines an `Ordering` of Entries by AH (Haleczko) points.
///
/// Because AH points aren't stored on the Entry, they are recalculated
/// each comparison. The computation is not particularly expensive,
/// but does involve powf().
#[inline]
pub fn cmp_ah(meets: &[Meet], a: &Entry, b: &Entry) -> cmp::Ordering {
let a_points = coefficients::ah(a.sex, a.bodyweightkg, a.totalkg);
let b_points = coefficients::ah(b.sex, b.bodyweightkg, b.totalkg);
// First sort by AH points, higher first.
a_points
.cmp(&b_points)
.reverse()
// If equal, sort by Date, earlier first.
.then(
meets[a.meet_id as usize]
.date
.cmp(&meets[b.meet_id as usize].date),
)
// If that's equal too, sort by Total, highest first.
.then(a.totalkg.cmp(&b.totalkg).reverse())
}
/// Gets a list of all entry indices matching the given selection.
pub fn get_entry_indices_for<'db>(
selection: &Selection,
......
......@@ -26,6 +26,7 @@ pub struct Context<'db> {
// Instead of having the JS try to figure out how to access
// other sorts, just tell it what the paths are.
pub path_if_by_ah: String,
pub path_if_by_division: String,
pub path_if_by_glossbrenner: String,
pub path_if_by_ipfpoints: String,
......@@ -52,6 +53,7 @@ pub struct Table<'db> {
/// A sort selection widget just for the meet page.
#[derive(Copy, Clone, Debug, PartialEq, Serialize)]
pub enum MeetSortSelection {
ByAH,
ByDivision,
ByGlossbrenner,
ByIPFPoints,
......@@ -176,6 +178,11 @@ impl<'a> ResultsRow<'a> {
.in_format(number_format),
total: entry.totalkg.as_type(units).in_format(number_format),
points: match points_system {
PointsSystem::AH => {
let points =
coefficients::ah(entry.sex, entry.bodyweightkg, entry.totalkg);
points.in_format(number_format)
}
PointsSystem::Glossbrenner => entry.glossbrenner.in_format(number_format),
PointsSystem::IPFPoints => entry.ipfpoints.in_format(number_format),
PointsSystem::Reshel => {
......@@ -474,6 +481,9 @@ fn make_tables_by_points<'db>(
let mut display_points_system = points_system;
match points_system {
PointsSystem::AH => {
entries.sort_unstable_by(|a, b| algorithms::cmp_ah(&meets, a, b));
}
PointsSystem::Glossbrenner => {
entries.sort_unstable_by(|a, b| algorithms::cmp_glossbrenner(&meets, a, b));
}
......@@ -516,6 +526,9 @@ impl<'db> Context<'db> {
let default_points: PointsSystem = meet.federation.default_points(meet.date);
let tables: Vec<Table> = match sort {
MeetSortSelection::ByAH => {
make_tables_by_points(&opldb, &locale, PointsSystem::AH, meet_id)
}
MeetSortSelection::ByDivision => make_tables_by_division(
&opldb,
&locale,
......@@ -552,6 +565,7 @@ impl<'db> Context<'db> {
let points_column_title = match sort {
MeetSortSelection::ByDivision | MeetSortSelection::ByFederationDefault => {
match default_points {
PointsSystem::AH => "AH",
PointsSystem::Glossbrenner => &locale.strings.columns.glossbrenner,
PointsSystem::IPFPoints => &locale.strings.columns.ipfpoints,
PointsSystem::NASA => "NASA",
......@@ -561,6 +575,7 @@ impl<'db> Context<'db> {
PointsSystem::Wilks => &locale.strings.columns.wilks,
}
}
MeetSortSelection::ByAH => "AH",
MeetSortSelection::ByGlossbrenner => &locale.strings.columns.glossbrenner,
MeetSortSelection::ByIPFPoints => &locale.strings.columns.ipfpoints,
MeetSortSelection::ByNASA => "NASA",
......@@ -571,6 +586,10 @@ impl<'db> Context<'db> {
};
// Paths do not include the urlprefix, which defaults to "/".
let path_if_by_ah = match default_points {
PointsSystem::AH => format!("m/{}", meet.path),
_ => format!("m/{}/by-ah", meet.path),
};
let path_if_by_division = format!("m/{}/by-division", meet.path);
let path_if_by_glossbrenner = match default_points {
PointsSystem::Glossbrenner => format!("m/{}", meet.path),
......@@ -605,6 +624,7 @@ impl<'db> Context<'db> {
units: locale.units,
points_column_title,
sortselection: match sort {
MeetSortSelection::ByAH => MeetSortSelection::ByAH,
MeetSortSelection::ByDivision => MeetSortSelection::ByDivision,
MeetSortSelection::ByGlossbrenner => MeetSortSelection::ByGlossbrenner,
MeetSortSelection::ByIPFPoints => MeetSortSelection::ByIPFPoints,
......@@ -613,6 +633,7 @@ impl<'db> Context<'db> {
MeetSortSelection::ByTotal => MeetSortSelection::ByTotal,
MeetSortSelection::ByWilks => MeetSortSelection::ByWilks,
MeetSortSelection::ByFederationDefault => match default_points {
PointsSystem::AH => MeetSortSelection::ByAH,
PointsSystem::Glossbrenner => MeetSortSelection::ByGlossbrenner,
PointsSystem::IPFPoints => MeetSortSelection::ByIPFPoints,
PointsSystem::Reshel => MeetSortSelection::ByReshel,
......@@ -625,6 +646,7 @@ impl<'db> Context<'db> {
has_age_data: true, // TODO: Maybe use again?
tables,
use_rank_column: sort != MeetSortSelection::ByDivision,
path_if_by_ah,
path_if_by_division,
path_if_by_glossbrenner,
path_if_by_ipfpoints,
......
......@@ -9,6 +9,7 @@
{% block includes %}
<script type="text/javascript">
const path_if_by_ah = "{{urlprefix | safe}}{{path_if_by_ah | safe}}";
const path_if_by_division = "{{urlprefix | safe}}{{path_if_by_division | safe}}";
const path_if_by_glossbrenner = "{{urlprefix | safe}}{{path_if_by_glossbrenner | safe}}";
const path_if_by_ipfpoints = "{{urlprefix | safe}}{{path_if_by_ipfpoints | safe}}";
......@@ -24,6 +25,7 @@
<div id="controls">
<div id="controls-left">
<select id="sortselect">
<option value="by-ah" {% if sortselection == "ByAH" %}selected{% endif %}>{{strings.selectors.sort.by_ah}}</option>
<option value="by-division" {% if sortselection == "ByDivision" %}selected{% endif %}>{{strings.selectors.sort.by_division}}</option>
<option value="by-glossbrenner" {% if sortselection == "ByGlossbrenner" %}selected{% endif %}>{{strings.selectors.sort.by_glossbrenner}}</option>
<option value="by-ipf-points" {% if sortselection == "ByIPFPoints" %}selected{% endif %}>{{strings.selectors.sort.by_ipfpoints}}</option>
......
......@@ -257,6 +257,7 @@
"by_deadlift": "Podle mrtvého tahu",
"by_total": "Podle totalu",
"by_allometric": "Podle alometrické škály",
"by_ah": "By AH (Haleczko)",
"by_glossbrenner": "Podle Glossbrenner bodů",
"by_ipfpoints": "Podle IPF bodů",
"by_mcculloch": "Podle McCulloch bodů",
......
......@@ -257,6 +257,7 @@
"by_deadlift": "Kreuzheben",
"by_total": "Total",
"by_allometric": "Allometrische Skalierung",
"by_ah": "By AH (Haleczko)",
"by_glossbrenner": "Glossbrenner",
"by_ipfpoints": "By IPF Points",
"by_mcculloch": "McCulloch",
......
......@@ -257,6 +257,7 @@
"by_deadlift": "Βάση Άρσης Θανάτου",
"by_total": "Βάση Συνόλου",
"by_allometric": "By Allometric Scaling",
"by_ah": "By AH (Haleczko)",
"by_glossbrenner": "By Glossbrenner",
"by_ipfpoints": "By IPF Points",
"by_mcculloch": "By McCulloch",
......
......@@ -257,6 +257,7 @@
"by_deadlift": "By Deadlift",
"by_total": "By Total",
"by_allometric": "By Allometric Scaling",
"by_ah": "By AH (Haleczko)",
"by_glossbrenner": "By Glossbrenner",
"by_ipfpoints": "By IPF Points",
"by_mcculloch": "By McCulloch",
......
......@@ -257,6 +257,7 @@
"by_deadlift": "Laŭ Mortolevo",
"by_total": "Laŭ Totalo",
"by_allometric": "Laŭ Skalado Alometrika",
"by_ah": "By AH (Haleczko)",
"by_glossbrenner": "Laŭ Glosbrenero",
"by_ipfpoints": "By IPF Points",
"by_mcculloch": "Laŭ MkKuloko",
......
......@@ -257,6 +257,7 @@
"by_deadlift": "By Deadlift",
"by_total": "By Total",
"by_allometric": "By Allometric Scaling",
"by_ah": "By AH (Haleczko)",
"by_glossbrenner": "By Glossbrenner",
"by_ipfpoints": "By IPF Points",
"by_mcculloch": "By McCulloch",
......
......@@ -257,6 +257,7 @@
"by_deadlift": "By Deadlift",
"by_total": "By Total",
"by_allometric": "By Allometric Scaling",
"by_ah": "By AH (Haleczko)",
"by_glossbrenner": "By Glossbrenner",
"by_ipfpoints": "By IPF Points",
"by_mcculloch": "By McCulloch",
......
......@@ -257,6 +257,7 @@
"by_deadlift": "Par S.d.T.",
"by_total": "Par Total",
"by_allometric": "Par Echelonnage Allométrique",
"by_ah": "By AH (Haleczko)",
"by_glossbrenner": "Par Glossbrenner",
"by_ipfpoints": "By IPF Points",
"by_mcculloch": "Par McCulloch",
......
......@@ -257,6 +257,7 @@
"by_deadlift": "Po mrtvom dizanju",
"by_total": "Po totalu",
"by_allometric": "Po Alometričkoj skali",
"by_ah": "By AH (Haleczko)",
"by_glossbrenner": "Po Glossbrenner",
"by_ipfpoints": "By IPF Points",
"by_mcculloch": "Po McCulloch",
......
......@@ -257,6 +257,7 @@
"by_deadlift": "Felhúzás alapján",
"by_total": "Összetett alapján",
"by_allometric": "Allometrikus skála alapján",
"by_ah": "By AH (Haleczko)",
"by_glossbrenner": "Glossbrenner alapján",
"by_ipfpoints": "By IPF Points",
"by_mcculloch": "McCulloch alapján",
......
......@@ -257,6 +257,7 @@
"by_deadlift": "By Deadlift",
"by_total": "By Total",
"by_allometric": "By Allometric Scaling",
"by_ah": "By AH (Haleczko)",
"by_glossbrenner": "By Glossbrenner",
"by_ipfpoints": "By IPF Points",
"by_mcculloch": "By McCulloch",
......
......@@ -257,6 +257,7 @@
"by_deadlift": "デッドリフト",
"by_total": "トータル",
"by_allometric": "アロメトリック ・ スケーリング",
"by_ah": "By AH (Haleczko)",
"by_glossbrenner": "グロスブレナー",
"by_ipfpoints": "By IPF Points",
"by_mcculloch": "マカロック",
......
......@@ -257,6 +257,7 @@
"by_deadlift": "Po Martwym Ciągu",
"by_total": "Po Trójbój",
"by_allometric": "Po skali allometrycznej",
"by_ah": "By AH (Haleczko)",
"by_glossbrenner": "Po Glossbrennerze",
"by_ipfpoints": "By IPF Points",
"by_mcculloch": "Po McCullochu",
......
......@@ -257,6 +257,7 @@
"by_deadlift": "Por Peso Morto",
"by_total": "Por Total",
"by_allometric": "Por Escala Alométrica",
"by_ah": "By AH (Haleczko)",
"by_glossbrenner": "Por Glossbrenner",
"by_ipfpoints": "By IPF Points",
"by_mcculloch": "Por McCulloch",
......
......@@ -257,6 +257,7 @@
"by_deadlift": "По Тяге",
"by_total": "По Сумме",
"by_allometric": "By Allometric Scaling",
"by_ah": "By AH (Haleczko)",
"by_glossbrenner": "По Глоссбреннеру",
"by_ipfpoints": "По очкам IPF",
"by_mcculloch": "По МкКуллоку",
......
......@@ -257,6 +257,7 @@
"by_deadlift": "By Deadlift",
"by_total": "By Total",
"by_allometric": "By Allometric Scaling",
"by_ah": "By AH (Haleczko)",
"by_glossbrenner": "By Glossbrenner",
"by_ipfpoints": "By IPF Points",
"by_mcculloch": "By McCulloch",
......
......@@ -257,6 +257,7 @@
"by_deadlift": "Po mrtvom dizanju",
"by_total": "Po totalu",
"by_allometric": "Po Allometričkoj skali",
"by_ah": "By AH (Haleczko)",
"by_glossbrenner": "Po Glossbrenner",
"by_ipfpoints": "By IPF Points",
"by_mcculloch": "Po McCulloch",
......
......@@ -257,6 +257,7 @@
"by_deadlift": "Marklyft",
"by_total": "Total",
"by_allometric": "Allometrisk Skalning",
"by_ah": "By AH (Haleczko)",
"by_glossbrenner": "Glossbrenner",
"by_ipfpoints": "By IPF Points",
"by_mcculloch": "McCulloch",
......
......@@ -257,6 +257,7 @@
"by_deadlift": "Deadlifte göre",
"by_total": "Totale göre",
"by_allometric": "Allometrik sıralamaya göre",
"by_ah": "By AH (Haleczko)",
"by_glossbrenner": "Glossberner'e göre",
"by_ipfpoints": "By IPF Points",
"by_mcculloch": "McCulloch'a göre",
......
......@@ -257,6 +257,7 @@
"by_deadlift": "За тягою",
"by_total": "За сумою",
"by_allometric": "За аллометричним шкалюванням",
"by_ah": "By AH (Haleczko)",
"by_glossbrenner": "За Глосбренером",
"by_ipfpoints": "By IPF Points",
"by_mcculloch": "За МакКалохом",
......
......@@ -257,6 +257,7 @@
"by_deadlift": "Theo Deadlift",
"by_total": "Theo Total",
"by_allometric": "Theo Allometric Scaling",
"by_ah": "By AH (Haleczko)",
"by_glossbrenner": "Theo Glossbrenner",
"by_ipfpoints": "By IPF Points",
"by_mcculloch": "Theo McCulloch",
......
......@@ -257,6 +257,7 @@
"by_deadlift": "By Deadlift",
"by_total": "By Total",
"by_allometric": "By Allometric Scaling",
"by_ah": "By AH (Haleczko)",
"by_glossbrenner": "By Glossbrenner",
"by_ipfpoints": "By IPF Points",
"by_mcculloch": "By McCulloch",
......
......@@ -257,6 +257,7 @@
"by_deadlift": "By Deadlift",
"by_total": "By Total",
"by_allometric": "By Allometric Scaling",
"by_ah": "By AH (Haleczko)",
"by_glossbrenner": "By Glossbrenner",
"by_ipfpoints": "By IPF Points",
"by_mcculloch": "By McCulloch",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment