Refactoring calculator code

This commit is contained in:
Dominic Ferrando
2025-09-14 14:22:17 -04:00
parent e3c4a5c285
commit aacfcccb9a
5 changed files with 6033 additions and 279 deletions
+150 -187
View File
@@ -1,7 +1,5 @@
import { Activity, Attribute, attributes, BenchmarkPerformance, findNearestPoints, Gender, getActivityAttribute, getAttributeActivities, kgToLb, Levels, msToTime, Player, StandardsItem } from "./util"; import { Activity, Attribute, attributes, ActivityPerformance, findNearestPoints, getActivityAttribute, getAttributeActivities, Player, StandardsMap, BaseLevel, Standard } from "./util";
import rawStandards from "./standards.json" assert { type: "json" } import rawStandards from "./newStandards.json" assert { type: "json" }
const standards = rawStandards as StandardsItem[];
// SOURCES // SOURCES
// Squat, Bench, Dead Lift: // Squat, Bench, Dead Lift:
@@ -13,136 +11,32 @@ const standards = rawStandards as StandardsItem[];
// Broad Jump: // Broad Jump:
// //
const standards = rawStandards as StandardsMap;
type BaseLevelValues = Record<BaseLevel, number>
// ------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------
// Level calculations // Level utilities (helpers)
// ------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------
export const calcBenchmarkLevel = (performance: number, levels: Record<number, number>): number => {
let playerLevel = 1; function findLevel(levels: Record<number, number>, performance: number): number {
let playerLevelDiff = Infinity; let resultLevel = 1;
let resultLevelDiff = Infinity;
for (const level in levels) { for (const level in levels) {
const diff = Math.abs(performance - levels[level as unknown as number]); const diff = Math.abs(performance - levels[level as unknown as number]);
if (diff < playerLevelDiff) { if (diff < resultLevelDiff) {
playerLevel = +level; resultLevel = +level;
playerLevelDiff = diff; resultLevelDiff = diff;
} }
} }
return playerLevel; return resultLevel;
};
export const calcAttributeLevel = (
attribute: Attribute,
benchmarkPerformances: BenchmarkPerformance[],
): number => {
// must be activities of the given attribute
if (benchmarkPerformances.some((bp) => getActivityAttribute(bp.activity) !== attribute)) {
throw new Error("Wrong benchmark performance attribute");
} }
// must perform all of an attributes activities function compressLevels(
for (const activity of getAttributeActivities(attribute)) {
const benchmarkActivites = benchmarkPerformances.map((bp) => bp.activity);
if (!benchmarkActivites.includes(activity))
return 0;
}
// must have all benchmarks using a valid performance value (> 0)
for (const benchmarkPerformance of benchmarkPerformances) {
if (benchmarkPerformance.performance <= 0)
return 0
}
const benchmarkLevels = benchmarkPerformances.map((abp) =>
calcBenchmarkLevel(abp.performance, abp.levels),
);
const benchmarkLevelsAvg = Math.round(
benchmarkLevels.reduce((sum, curr) => sum + curr, 0) / benchmarkLevels.length,
);
return benchmarkLevelsAvg;
};
export const calcAttributeLevels = (
player: Player,
benchmarkPerformances: BenchmarkPerformance[],
): Record<Attribute, number> => {
const attrLevels: Record<Attribute, number> = {
[attributes.STRENGTH]: 0,
[attributes.POWER]: 0,
[attributes.ENDURANCE]: 0,
// [attributes.SPEED]: 0,
[attributes.AGILITY]: 0,
} as const;
for (const value of Object.values(player)) {
if (!value)
return attrLevels;
}
(Object.values(attributes) as Attribute[]).forEach((attribute) => {
const attrBenchmarkPerformances = benchmarkPerformances.filter(
(bp) => getActivityAttribute(bp.activity) === attribute,
);
attrLevels[attribute] = calcAttributeLevel(attribute, attrBenchmarkPerformances);
});
return attrLevels;
};
export const calcPlayerLevel = (player: Player, benchmarkPerformances: BenchmarkPerformance[]): number => {
for (const value of Object.values(player)) {
if (!value)
return 0;
}
const attrLevels = calcAttributeLevels(player, benchmarkPerformances);
for (const level of Object.values(attrLevels)) {
if (!level)
return 0;
}
const attrLevelsSum = Object.values(attrLevels).reduce((sum, lvl) => sum + lvl, 0);
return Math.round(attrLevelsSum / Object.keys(attributes).length);
};
// -------------------------------------------------------------------------------------------------
// Presentation helpers
// -------------------------------------------------------------------------------------------------
// export const formatLevelValues = (
// levels: Record<number, number>,
// activity: Activity,
// ): Record<number, string> => {
// const timedActivities: Activity[] = [activities.RUN, activities.DASH, activities.CONE_DRILL];
//
// const formattedLevels: Record<number, string> = {};
// for (const level in levels) {
// const lvl = +level as number;
// let displayValue = "";
// if (timedActivities.includes(activity)) {
// displayValue = msToTime(levels[lvl]);
// } else if (activity === activities.TREADMILL_DASH) {
// displayValue = `${(levels[lvl] * 1000).toFixed(2)} m/s`;
// } else if (activity === activities.BROAD_JUMP) {
// displayValue = `${(levels[lvl] * 0.0328084).toFixed(2)} ft.`;
// } else {
// displayValue = kgToLb(levels[lvl]).toFixed(0);
// }
//
// formattedLevels[lvl] = displayValue;
// }
//
// return formattedLevels;
// };
// -------------------------------------------------------------------------------------------------
// Level utilities
// -------------------------------------------------------------------------------------------------
const compressLevels = (
levels: Record<number, number>, levels: Record<number, number>,
targetLevelsAmount: number, targetLevelsAmount: number,
): Record<number, number> => { ): Record<number, number> {
const levelsAmount = Object.keys(levels).length; const levelsAmount = Object.keys(levels).length;
if (levelsAmount < targetLevelsAmount) { if (levelsAmount < targetLevelsAmount) {
throw new Error( throw new Error(
@@ -166,12 +60,12 @@ const compressLevels = (
} }
return compressedLevels; return compressedLevels;
}; }
const expandLevels = ( function expandLevels(
levels: Levels | Record<number, number>, levels: BaseLevel | Record<number, number>,
i: number i: number
): Record<number, number> => { ): Record<number, number> {
if (i == 0) { if (i == 0) {
// ensure we always use number keys // ensure we always use number keys
const newLevels = {}; const newLevels = {};
@@ -198,15 +92,70 @@ const expandLevels = (
} }
// ------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------
// Main level function // Standards interpolation (heavy math bits)
// ------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------
export const computeLevels = (
age: number, function interpolateStandardsValues(
weightKG: number, lower: BaseLevelValues,
gender: Gender, upper: BaseLevelValues,
ratio: number,
): BaseLevelValues {
function lerp(k: BaseLevel): number {
return (lower[k] as number) + ((upper[k] as number) - (lower[k] as number)) * ratio;
}
return {
physicallyActive: lerp("physicallyActive"),
beginner: lerp("beginner"),
intermediate: lerp("intermediate"),
advanced: lerp("advanced"),
elite: lerp("elite"),
};
}
function interpolateByBodyWeight(
targetAge: number,
bodyWeightKG: number,
standardsArr: Standard[],
): BaseLevelValues | null {
const ageFilteredStandards = standardsArr.filter((s) => s.metrics.age === targetAge);
if (ageFilteredStandards[0].metrics.weight === -1) return ageFilteredStandards[0].levels;
const weightArray = [...new Set(ageFilteredStandards.map((s) => s.metrics.weight))].sort((a, b) => a - b);
const { lower, upper } = findNearestPoints(bodyWeightKG, weightArray);
const weightLower = ageFilteredStandards.find((s) => s.metrics.weight === lower);
const weightUpper = ageFilteredStandards.find((s) => s.metrics.weight === upper);
if (!weightLower || !weightUpper) return null;
let weightRatio = upper === lower ? 1 : (bodyWeightKG - lower) / (upper - lower);
weightRatio = Math.max(0, Math.min(1, weightRatio));
return interpolateStandardsValues(weightLower.levels, weightUpper.levels, weightRatio);
}
function interpolateActivityLevels(
activity: Activity, activity: Activity,
) => { player: Player,
const levels = calculateLevels(age, weightKG, gender, activity, standards); ): Record<number, number> {
// Filter dataset
const filteredStandards = standards[activity]
.filter(a => a.metrics.gender === player.gender);
if (filteredStandards.length === 0) return {};
// Age interpolation
const ageArray = [...new Set(filteredStandards.map((s) => s.metrics.age))].sort((a, b) => a - b);
const { lower: ageLower, upper: ageUpper } = findNearestPoints(player.age, ageArray);
const lowerValues = interpolateByBodyWeight(ageLower, player.weightKG, filteredStandards);
const upperValues = interpolateByBodyWeight(ageUpper, player.weightKG, filteredStandards);
if (!lowerValues || !upperValues) return {};
let ageRatio = ageUpper === ageLower ? 1 : (player.age - ageLower) / (ageUpper - ageLower);
ageRatio = Math.max(0, Math.min(1, ageRatio));
const levels = interpolateStandardsValues(lowerValues, upperValues, ageRatio);
if (levels) { if (levels) {
const expandedLevels = expandLevels(levels, 5); const expandedLevels = expandLevels(levels, 5);
const compressedLevels = compressLevels(expandedLevels, 100); const compressedLevels = compressLevels(expandedLevels, 100);
@@ -217,69 +166,83 @@ export const computeLevels = (
} }
// ------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------
// Standards interpolation (heavy math bits) // Level calculations (public API)
// ------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------
const calculateLevels = (
age: number, export function calcAttributeLevel(
weightKG: number, attribute: Attribute,
gender: Gender, player: Player,
activity: Activity, activityPerformances: ActivityPerformance[],
standards: StandardsItem[], ): number {
): Levels | null => { // must be activities of the given attribute
// Filter dataset if (activityPerformances.some((bp) => getActivityAttribute(bp.activity) !== attribute)) {
const filtered = standards.filter( throw new Error("Wrong activity performance attribute");
(item) => item.gender === gender && item.activityType === activity, }
// must perform all of an attributes activities
for (const activity of getAttributeActivities(attribute)) {
const filteredActivites = activityPerformances.map((bp) => bp.activity);
if (!filteredActivites.includes(activity))
return 0;
}
// must have all activties using a valid performance value (> 0)
for (const activityPerformance of activityPerformances) {
if (activityPerformance.performance <= 0)
return 0
}
const activityLevels = activityPerformances.map((abp) =>
findLevel(interpolateActivityLevels(abp.activity, player), abp.performance),
); );
if (filtered.length === 0) return null;
// Age interpolation const activityLevelsAvg = Math.round(
const ageArray = [...new Set(filtered.map((s) => s.age))].sort((a, b) => a - b); activityLevels.reduce((sum, curr) => sum + curr, 0) / activityLevels.length,
const { lower: ageLower, upper: ageUpper } = findNearestPoints(age, ageArray); );
const lowerValues = interpolateByBodyWeight(ageLower, weightKG, filtered); return activityLevelsAvg;
const upperValues = interpolateByBodyWeight(ageUpper, weightKG, filtered); }
if (!lowerValues || !upperValues) return null;
let ageRatio = ageUpper === ageLower ? 1 : (age - ageLower) / (ageUpper - ageLower); export function calcAllAttributeLevels(
ageRatio = Math.max(0, Math.min(1, ageRatio)); player: Player,
activityPerformances: ActivityPerformance[],
): Record<Attribute, number> {
const attrLevels: Record<Attribute, number> = {
[attributes.STRENGTH]: 0,
[attributes.POWER]: 0,
[attributes.ENDURANCE]: 0,
// [attributes.SPEED]: 0,
[attributes.AGILITY]: 0,
} as const;
return interpolateStandardsValues(lowerValues, upperValues, ageRatio); for (const value of Object.values(player)) {
}; if (!value)
return attrLevels;
}
const interpolateByBodyWeight = ( (Object.values(attributes) as Attribute[]).forEach((attribute) => {
agePoint: number, const attrActivityPerformances = activityPerformances.filter(
bodyWeightKG: number, (bp) => getActivityAttribute(bp.activity) === attribute,
filtered: StandardsItem[], );
): Levels | null => { attrLevels[attribute] = calcAttributeLevel(attribute, player, attrActivityPerformances);
const ageFiltered = filtered.filter((item) => item.age === agePoint); });
if (ageFiltered[0].bodyWeight === -1) return ageFiltered[0];
const weightArray = [...new Set(ageFiltered.map((obj) => obj.bodyWeight))].sort((a, b) => a - b); return attrLevels;
const { lower, upper } = findNearestPoints(bodyWeightKG, weightArray); }
const weightLower = ageFiltered.find((i) => i.bodyWeight === lower); export function calcPlayerLevel(player: Player, activityPerformances: ActivityPerformance[]): number {
const weightUpper = ageFiltered.find((i) => i.bodyWeight === upper); for (const value of Object.values(player)) {
if (!weightLower || !weightUpper) return null; if (!value)
return 0;
}
let weightRatio = upper === lower ? 1 : (bodyWeightKG - lower) / (upper - lower); const attrLevels = calcAllAttributeLevels(player, activityPerformances);
weightRatio = Math.max(0, Math.min(1, weightRatio)); for (const level of Object.values(attrLevels)) {
if (!level)
return 0;
}
return interpolateStandardsValues(weightLower, weightUpper, weightRatio); const attrLevelsSum = Object.values(attrLevels).reduce((sum, lvl) => sum + lvl, 0);
}; return Math.round(attrLevelsSum / Object.keys(attributes).length);
}
const interpolateStandardsValues = (
lower: Levels,
upper: Levels,
ratio: number,
): Levels => {
const lerp = (k: keyof Levels): number =>
(lower[k] as number) + ((upper[k] as number) - (lower[k] as number)) * ratio;
return {
physicallyActive: lerp("physicallyActive"),
beginner: lerp("beginner"),
intermediate: lerp("intermediate"),
advanced: lerp("advanced"),
elite: lerp("elite"),
};
};
File diff suppressed because it is too large Load Diff
+38 -51
View File
@@ -1,14 +1,5 @@
import { activities, BenchmarkPerformance, genders, Player } from "./util"; import { activities, ActivityPerformance, feetToCm, genders, inchesToCm, lbToKg, minToMs, Player, secToMs } from "./util";
import { calcAttributeLevels, calcPlayerLevel, computeLevels } from "./calc.ts"; import { calcAllAttributeLevels, calcPlayerLevel } from "./calc.ts";
const lbToKg = (lb) => +lb * 0.453592;
const kgToLb = (kg) => +kg * 2.20462;
const minToMs = (min) => min * 60000;
const secToMs = (sec) => sec * 1000;
const feetToCm = (feet) => feet * 30.48;
const inchesToCm = (inches) => inches * 2.54;
const player: Player = { const player: Player = {
age: 18, age: 18,
@@ -16,77 +7,73 @@ const player: Player = {
gender: genders.MALE gender: genders.MALE
}; };
const computedPerformances: BenchmarkPerformance[] = [ const computedPerformances: ActivityPerformance[] = [
// STRENGTH // STRENGTH
{ {
activity: activities.BENCH_PRESS, activity: activities.BENCH_PRESS,
performance: lbToKg(200), performance: lbToKg(200)
levels: computeLevels(
player.age,
player.weightKG,
player.gender,
activities.BENCH_PRESS
)
}, },
{ {
activity: activities.DEADLIFT, activity: activities.DEADLIFT,
performance: lbToKg(200), performance: lbToKg(200),
levels: computeLevels(
player.age,
player.weightKG,
player.gender,
activities.DEADLIFT
)
}, },
{ {
activity: activities.BACK_SQUAT, activity: activities.BACK_SQUAT,
performance: lbToKg(200), performance: lbToKg(200),
levels: computeLevels(
player.age,
player.weightKG,
player.gender,
activities.BACK_SQUAT
)
}, },
// POWER // POWER
{ {
activity: activities.BROAD_JUMP, activity: activities.BROAD_JUMP,
performance: feetToCm(5) + inchesToCm(2), performance: feetToCm(10) + inchesToCm(2),
levels: computeLevels(
player.age,
player.weightKG,
player.gender,
activities.BROAD_JUMP
)
}, },
// ENDURANCE // ENDURANCE
{ {
activity: activities.RUN, activity: activities.RUN,
performance: minToMs(5) + secToMs(20), performance: minToMs(5) + secToMs(20),
levels: computeLevels(
player.age,
player.weightKG,
player.gender,
activities.RUN
)
}, },
// AGILITY // AGILITY
{ {
activity: activities.CONE_DRILL, activity: activities.CONE_DRILL,
performance: secToMs(5), performance: secToMs(5),
levels: computeLevels(
player.age,
player.weightKG,
player.gender,
activities.CONE_DRILL
)
}, },
]; ];
const levels = { const levels = {
attributes: calcAttributeLevels(player, computedPerformances), attributes: calcAllAttributeLevels(player, computedPerformances),
player: calcPlayerLevel(player, computedPerformances) player: calcPlayerLevel(player, computedPerformances)
}; };
console.log(levels); console.log(levels);
// const newStandards: Standards = {
// "Back Squat": [],
// "Deadlift": [],
// "Bench Press": [],
// "Run": [],
// "Dash": [],
// "Treadmill Dash": [],
// "Broad Jump": [],
// "Cone Drill": [],
// };
//
// for (const item of standards) {
// if (!newStandards[item.activityType])
// continue;
//
// newStandards[item.activityType].push({
// metrics: {
// age: item.age,
// weight: item.bodyWeight,
// gender: item.gender
// },
// levels: {
// physicallyActive: item.physicallyActive,
// beginner: item.beginner,
// intermediate: item.intermediate,
// advanced: item.advanced,
// elite: item.elite
// }
// });
// }
//
// Bun.write("./newStandards.json", JSON.stringify(newStandards, null, 2));
+25 -18
View File
@@ -70,38 +70,45 @@ export type Gender = typeof genders[keyof typeof genders];
// ------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------
// Data model interfaces // Data model interfaces
// ------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------
export interface BenchmarkPerformance { export interface ActivityPerformance {
activity: Activity; activity: Activity;
performance: number; performance: number;
/**
* Key: level number, Value: performance value (kg, ms, m/s, etc.)
*/
levels: Record<number, number>;
} }
export interface Levels { export type BaseLevel =
/** KG or -1 for bodyweightagnostic entries */ | "physicallyActive"
physicallyActive: number; | "beginner"
beginner: number; | "intermediate"
intermediate: number; | "advanced"
advanced: number; | "elite";
elite: number;
}
export interface StandardsItem extends Levels { interface Metrics {
bodyWeight: number;
gender: Gender;
activityType: Activity;
age: number; age: number;
weight: number;
gender: Gender;
};
export interface Standard {
metrics: Metrics;
levels: Record<BaseLevel, number>;
} }
export type StandardsMap = Record<
Activity,
Standard[]
>;
// ------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------
// Conversion utilities // Conversion utilities
// ------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------
export const lbToKg = (lb: number | string): number => +lb * 0.453592; export const lbToKg = (lb: number | string): number => +lb * 0.453592;
export const kgToLb = (kg: number | string): number => +kg * 2.20462; export const kgToLb = (kg: number | string): number => +kg * 2.20462;
export const feetToCm = (feet: number): number => feet * 30.48; export const minToMs = (min) => min * 60000;
export const secToMs = (sec) => sec * 1000;
export const feetToCm = (feet) => feet * 30.48;
export const inchesToCm = (inches) => inches * 2.54;
export const mphToMtrsPerMs = (mph: number): number => (mph * 0.44704) / 1000; export const mphToMtrsPerMs = (mph: number): number => (mph * 0.44704) / 1000;
+5 -22
View File
@@ -1,14 +1,11 @@
import { calcAttributeLevels, calcPlayerLevel, computeLevels } from "./calculator/calc.ts"; import { calcAllAttributeLevels, calcPlayerLevel } from "./calculator/calc.ts";
import { Activity, BenchmarkPerformance, Player } from "./calculator/util.ts"; import { ActivityPerformance, Player } from "./calculator/util.ts";
import { Printful } from "./commerce/printful.ts" import { Printful } from "./commerce/printful.ts"
import { Webflow } from "./commerce/webflow.ts"; import { Webflow } from "./commerce/webflow.ts";
interface CalcRequest { interface CalcRequest {
player: Player, player: Player,
performances: { performances: ActivityPerformance[]
activity: Activity,
performance: number,
}[]
} }
export class ClientResponse extends Response { export class ClientResponse extends Response {
@@ -39,24 +36,10 @@ const server = Bun.serve({
const calcReq: CalcRequest = await req.json(); const calcReq: CalcRequest = await req.json();
const { player, performances } = calcReq; const { player, performances } = calcReq;
const computedPerformances: BenchmarkPerformance[] = [];
for (const p of performances) {
computedPerformances.push({
activity: p.activity,
performance: p.performance,
levels: computeLevels(
player.age,
player.weightKG,
player.gender,
p.activity
)
})
}
return ClientResponse.json({ return ClientResponse.json({
levels: { levels: {
attributes: calcAttributeLevels(player, computedPerformances), attributes: calcAllAttributeLevels(player, performances),
player: calcPlayerLevel(player, computedPerformances) player: calcPlayerLevel(player, performances)
} }
}) })
} }