Commit ca0597b4 by 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 = 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 %}
 ... ... @@ -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!