fitness-progression-calculator
Version:
Workout progression calculator for fitness applications
391 lines (390 loc) • 16.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.calculateProgression = calculateProgression;
exports.roundToClosestIncrement = roundToClosestIncrement;
const MAX_REPS = 20;
const BODYWEIGHT_MAX_REPS = 15; // Before considering adding weight
const SPECIAL_BODYWEIGHT_EXERCISES = [
"PULL UP",
"CHIN UP",
"DIP",
"PUSH UP",
"PUSH UP DEFICIT",
];
const DEFAULT_EQUIPMENT_SETTINGS = {
barbellIncrement: 2.5,
dumbbellIncrement: 2.0,
cableIncrement: 2.5,
machineIncrement: 5.0,
experienceLevel: "BEGINNER",
};
// Constants
const CYCLING_TARGET_REPS = 15; // New target reps when cycling down from MAX_REPS
const LIGHT_DUMBBELL_MAX = 10; // Maximum weight for light dumbbells
const LIGHT_DUMBBELL_INCREMENT = 1.0; // Increment for light dumbbells
// Helper functions
function isSpecialBodyweightExercise(exerciseName) {
return SPECIAL_BODYWEIGHT_EXERCISES.includes(exerciseName.toUpperCase());
}
/**
* Calculate weight change based on rating
*/
function getWeightChangeForRating(rating, baseIncrement) {
switch (rating) {
case 1: // Very easy
return baseIncrement * 3;
case 2: // Easy
return baseIncrement * 2;
case 3: // Moderate
return baseIncrement;
case 4: // Hard
return 0;
case 5: // Too hard
return -baseIncrement;
default:
return 0;
}
}
/**
* Gets base increment for the given equipment type from user settings
*/
function getBaseIncrement(equipmentType, currentWeight, userSettings = DEFAULT_EQUIPMENT_SETTINGS) {
// Always use 1kg increments for dumbbells 0-10kg
if (equipmentType === "DUMBBELL" && currentWeight <= 10) {
return LIGHT_DUMBBELL_INCREMENT;
}
switch (equipmentType) {
case "BARBELL":
return (userSettings.barbellIncrement ||
DEFAULT_EQUIPMENT_SETTINGS.barbellIncrement);
case "DUMBBELL":
return (userSettings.dumbbellIncrement ||
DEFAULT_EQUIPMENT_SETTINGS.dumbbellIncrement);
case "CABLE":
return (userSettings.cableIncrement || DEFAULT_EQUIPMENT_SETTINGS.cableIncrement);
case "MACHINE":
return (userSettings.machineIncrement ||
DEFAULT_EQUIPMENT_SETTINGS.machineIncrement);
case "BODYWEIGHT":
return (userSettings.dumbbellIncrement ||
DEFAULT_EQUIPMENT_SETTINGS.dumbbellIncrement);
default:
return 2.5; // Default fallback
}
}
/**
* Determine maximum percentage increase based on exercise properties to cap increases
*/
function getMaxPercentageIncrease(currentWeight, isCompound, equipmentType, userSettings = DEFAULT_EQUIPMENT_SETTINGS) {
// Base percentage depends on exercise type and experience level
let maxPercentage;
// Get experience level from settings
const experienceLevel = userSettings.experienceLevel || "BEGINNER";
// Set base percentage based on compound/isolation and experience level
if (isCompound) {
switch (experienceLevel) {
case "BEGINNER":
maxPercentage = 15;
break;
case "INTERMEDIATE":
maxPercentage = 10;
break;
case "ADVANCED":
maxPercentage = 7.5;
break;
default:
maxPercentage = 15; // Default to beginner if unknown
}
}
else {
// Isolation exercises
switch (experienceLevel) {
case "BEGINNER":
maxPercentage = 12.5;
break;
case "INTERMEDIATE":
maxPercentage = 10;
break;
case "ADVANCED":
maxPercentage = 7.5;
break;
default:
maxPercentage = 12.5; // Default to beginner if unknown
}
}
// For lighter weights, allow even higher percentage increases
if (currentWeight < 10) {
maxPercentage *= 1.5; // e.g., 22.5% for beginner compounds
}
else if (currentWeight < 20) {
maxPercentage *= 1.25; // e.g., 18.75% for beginner compounds
}
// Special case for very light dumbbell exercises (like lateral raises)
// These can handle larger percentage increases when the weights are very light
if (equipmentType === "DUMBBELL" && currentWeight < 5) {
// Allow up to 25% increases for very light dumbbells (e.g., 2kg to 2.5kg)
maxPercentage = Math.max(maxPercentage, 25);
}
return maxPercentage;
}
/**
* Apply percentage-based caps to weight increases
*/
function applyPercentageCap(currentWeight, proposedWeight, isCompound, equipmentType, userSettings = DEFAULT_EQUIPMENT_SETTINGS) {
// Get max percentage increase for this exercise
const maxPercent = getMaxPercentageIncrease(currentWeight, isCompound, equipmentType, userSettings);
// Calculate the maximum allowed weight
const maxAllowedWeight = currentWeight * (1 + maxPercent / 100);
// Return the smaller of the proposed weight and maximum allowed weight
return Math.min(proposedWeight, maxAllowedWeight);
}
/**
* Rounds a weight to the nearest multiple of the increment
*/
function roundToIncrementMultiple(weight, equipmentType, userSettings = DEFAULT_EQUIPMENT_SETTINGS) {
const increment = getBaseIncrement(equipmentType, weight, userSettings);
return Math.round(weight / increment) * increment;
}
/**
* Calculate the total volume for a given exercise configuration
*/
function calculateVolume(sets, reps, weight) {
return sets * reps * weight;
}
/**
* Handle bodyweight exercises specially
*/
function handleBodyweightExercise(data) {
switch (data.rating) {
case 1: // Very easy
return {
newWeight: data.weight,
newReps: Math.min(data.reps + 2, MAX_REPS),
};
case 2: // Easy
case 3: // Moderate
return {
newWeight: data.weight,
newReps: Math.min(data.reps + 1, MAX_REPS),
};
case 4: // Hard
return {
newWeight: data.weight,
newReps: data.reps,
};
case 5: // Too hard
return {
newWeight: data.weight,
newReps: Math.max(1, data.reps - 2),
};
default:
return {
newWeight: data.weight,
newReps: data.reps,
};
}
}
/**
* Main function to calculate progression based on exercise data and program type
*/
function calculateProgression(data, programType, userSettings = DEFAULT_EQUIPMENT_SETTINGS) {
console.log(`Calculate progression for ${data.exercise_name}:`, {
currentWeight: data.weight,
currentReps: data.reps,
rating: data.rating,
equipmentType: data.equipment_type,
programType,
});
// Special handling for bodyweight exercises
if (data.equipment_type === "BODYWEIGHT" &&
isSpecialBodyweightExercise(data.exercise_name)) {
return handleBodyweightExercise(data);
}
// Get the base increment for this equipment type
const baseIncrement = getBaseIncrement(data.equipment_type, data.weight, userSettings);
// Calculate weight change based on rating
const weightChange = getWeightChangeForRating(data.rating, baseIncrement);
// For strength programs, focus on weight increases
if (programType === "STRENGTH") {
const rawNewWeight = Math.max(0, data.weight + weightChange);
// Apply percentage cap to prevent excessive increases
const cappedWeight = applyPercentageCap(data.weight, rawNewWeight, data.is_compound, data.equipment_type, userSettings);
// Round the weight to the nearest multiple of the increment
const roundedWeight = roundToIncrementMultiple(cappedWeight, data.equipment_type, userSettings);
return {
newWeight: roundedWeight,
newReps: data.reps,
};
}
// HYPERTROPHY program logic
// If we've reached max reps, cycle back to target reps with increased weight
if (data.reps >= MAX_REPS) {
// Use a more conservative approach to weight increases when cycling reps
// The heavier the weight, the more conservative we become
// Base volume ratio (would be 20/15 = 1.33 if we maintained exact volume)
// Instead, we use a slightly reduced ratio to be more conservative
let volumeRatio;
// Adjust conservativeness based on weight range
if (data.weight >= 100) {
// For heavy weights (100kg+), be very conservative (25% increase instead of 33%)
volumeRatio = 1.25;
}
else if (data.weight >= 50) {
// For medium weights (50-100kg), be moderately conservative (28% increase)
volumeRatio = 1.28;
}
else if (data.weight >= 20) {
// For lighter weights (20-50kg), be slightly conservative (30% increase)
volumeRatio = 1.3;
}
else {
// For very light weights (<20kg), standard ratio is usually safe
volumeRatio = 1.33;
}
// For advanced users, be even more conservative
if (userSettings.experienceLevel === "ADVANCED") {
volumeRatio *= 0.95; // Reduce by 5% for advanced users
}
// Calculate ideal new weight with adjusted ratio
const idealNewWeight = data.weight * volumeRatio;
// Apply percentage-based cap
const cappedWeight = applyPercentageCap(data.weight, idealNewWeight, data.is_compound, data.equipment_type, userSettings);
// Round to the nearest increment for the equipment type
const roundedWeight = roundToIncrementMultiple(cappedWeight, data.equipment_type, userSettings);
return {
newWeight: roundedWeight,
newReps: CYCLING_TARGET_REPS,
};
}
// For ratings 4 (Hard) and 5 (Too Hard)
if (data.rating >= 4) {
const rawNewWeight = Math.max(0, data.weight + weightChange);
const roundedWeight = roundToIncrementMultiple(rawNewWeight, data.equipment_type, userSettings);
return {
newWeight: roundedWeight,
newReps: data.reps,
};
}
// Prepare options for progression
const rawNewWeight = Math.max(0, data.weight + weightChange);
// Apply percentage cap to prevent excessive increases
const cappedWeight = applyPercentageCap(data.weight, rawNewWeight, data.is_compound, data.equipment_type, userSettings);
const roundedNewWeight = roundToIncrementMultiple(cappedWeight, data.equipment_type, userSettings);
const newReps = Math.min(data.reps + 1, MAX_REPS);
// Calculate current volume
const currentVolume = calculateVolume(data.sets, data.reps, data.weight);
// Calculate volumes for different progression options
const volumeWithWeightIncrease = calculateVolume(data.sets, data.reps, roundedNewWeight);
const volumeWithRepIncrease = calculateVolume(data.sets, newReps, data.weight);
// Use per-unit efficiency calculations
const weightDelta = roundedNewWeight - data.weight;
const repDelta = newReps - data.reps;
// Efficiency is volume increase per unit of change (per kg or per rep)
const weightEfficiency = weightDelta > 0
? (volumeWithWeightIncrease - currentVolume) / weightDelta
: 0;
const repEfficiency = repDelta > 0 ? (volumeWithRepIncrease - currentVolume) / repDelta : 0;
// Option objects for cleaner code
const increaseBoth = {
newWeight: roundedNewWeight,
newReps: newReps,
};
const increaseWeightOnly = {
newWeight: roundedNewWeight,
newReps: data.reps,
};
const increaseRepsOnly = {
newWeight: data.weight,
newReps: newReps,
};
const noChange = {
newWeight: data.weight,
newReps: data.reps,
};
// For ratings 1-3 with guaranteed different outcomes
// Final solution that ensures distinct outcomes while respecting percentage caps
switch (data.rating) {
case 1: {
// Very Easy - Weight gain with rep increase fallback
// For "Very Easy", attempt significant weight increase (2 increments)
const veryEasyTargetWeight = data.weight + baseIncrement * 2;
// Apply percentage cap to prevent excessive increases
const cappedVeryEasyWeight = applyPercentageCap(data.weight, veryEasyTargetWeight, data.is_compound, data.equipment_type, userSettings);
// Round to valid equipment increments
const roundedVeryEasyWeight = roundToIncrementMultiple(cappedVeryEasyWeight, data.equipment_type, userSettings);
// Check if this results in a meaningful weight increase
if (roundedVeryEasyWeight > data.weight) {
// If we got a weight increase, use it
return {
newWeight: roundedVeryEasyWeight,
newReps: data.reps, // Keep reps the same
};
}
// If weight couldn't increase due to caps, increase reps AND weight
// This ensures Rating 1 is always different from Rating 2 & 3
const easyTargetWeight = data.weight + baseIncrement;
const cappedEasyWeight = applyPercentageCap(data.weight, easyTargetWeight, data.is_compound, data.equipment_type, userSettings);
const roundedEasyWeight = roundToIncrementMultiple(cappedEasyWeight, data.equipment_type, userSettings);
return {
newWeight: roundedEasyWeight,
newReps: Math.min(data.reps + 1, MAX_REPS),
};
}
case 2: {
// Easy - Weight increase only
// For "Easy", add 1 increment of weight
const easyTargetWeight = data.weight + baseIncrement;
// Apply percentage cap
const cappedEasyWeight = applyPercentageCap(data.weight, easyTargetWeight, data.is_compound, data.equipment_type, userSettings);
// Round to valid equipment increments
const roundedEasyWeight = roundToIncrementMultiple(cappedEasyWeight, data.equipment_type, userSettings);
// Check if this results in a meaningful weight increase
if (roundedEasyWeight > data.weight) {
// If we got a weight increase, use it
return {
newWeight: roundedEasyWeight,
newReps: data.reps, // Keep reps the same
};
}
// If weight couldn't increase, keep weight the same
// This ensures Rating 2 is always different from Rating 3
return {
newWeight: data.weight,
newReps: data.reps, // No change
};
}
case 3: // Moderate - Rep increase only
// For "Moderate", keep the weight the same but increase reps
return {
newWeight: data.weight,
newReps: Math.min(data.reps + 1, MAX_REPS),
};
case 4: // Hard - No change
return {
newWeight: data.weight,
newReps: data.reps,
};
case 5: {
// Too Hard - Decrease weight
// For "Too Hard", decrease by one increment
const tooHardWeight = Math.max(0, data.weight - baseIncrement);
const roundedTooHardWeight = roundToIncrementMultiple(tooHardWeight, data.equipment_type, userSettings);
return {
newWeight: roundedTooHardWeight,
newReps: data.reps,
};
}
default:
return {
newWeight: data.weight,
newReps: data.reps,
};
}
}
/**
* Helper function to round weight to closest available increment
* Useful for client-side adjustments
*/
function roundToClosestIncrement(weight, equipmentType, userSettings = DEFAULT_EQUIPMENT_SETTINGS) {
return roundToIncrementMultiple(weight, equipmentType, userSettings);
}