fitness-progression-calculator 1.1.0 → 1.3.0
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.
- package/dist/calculator.d.ts +0 -1
- package/dist/calculator.js +133 -187
- package/package.json +6 -2
package/dist/calculator.d.ts
CHANGED
@@ -21,7 +21,6 @@ interface UserEquipmentSettings {
|
|
21
21
|
}
|
22
22
|
/**
|
23
23
|
* Main function to calculate progression based on exercise data and program type
|
24
|
-
* Now accepts optional userSettings for equipment-specific increments
|
25
24
|
*/
|
26
25
|
export declare function calculateProgression(data: ExerciseData, programType: "STRENGTH" | "HYPERTROPHY", userSettings?: UserEquipmentSettings): ProgressionResult;
|
27
26
|
/**
|
package/dist/calculator.js
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
"use strict";
|
2
|
-
// services/progressionCalculator.ts
|
3
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
4
3
|
exports.calculateProgression = calculateProgression;
|
5
4
|
exports.roundToClosestIncrement = roundToClosestIncrement;
|
@@ -19,13 +18,22 @@ const DEFAULT_EQUIPMENT_SETTINGS = {
|
|
19
18
|
machineIncrement: 5.0,
|
20
19
|
experienceLevel: "BEGINNER",
|
21
20
|
};
|
21
|
+
// Constants
|
22
|
+
const STARTING_HYPERTROPHY_REPS = 10; // Rep count to cycle back to after reaching MAX_REPS
|
23
|
+
const LIGHT_DUMBBELL_MAX = 10; // Maximum weight for light dumbbells
|
24
|
+
const LIGHT_DUMBBELL_INCREMENT = 1.0; // Increment for light dumbbells
|
25
|
+
// Helper functions
|
22
26
|
function isSpecialBodyweightExercise(exerciseName) {
|
23
27
|
return SPECIAL_BODYWEIGHT_EXERCISES.includes(exerciseName.toUpperCase());
|
24
28
|
}
|
25
29
|
/**
|
26
|
-
* Gets
|
30
|
+
* Gets base increment for the given equipment type from user settings
|
27
31
|
*/
|
28
|
-
function
|
32
|
+
function getBaseIncrement(equipmentType, currentWeight, userSettings = DEFAULT_EQUIPMENT_SETTINGS) {
|
33
|
+
// Special case for light dumbbells
|
34
|
+
if (equipmentType === "DUMBBELL" && currentWeight < LIGHT_DUMBBELL_MAX) {
|
35
|
+
return LIGHT_DUMBBELL_INCREMENT;
|
36
|
+
}
|
29
37
|
switch (equipmentType) {
|
30
38
|
case "BARBELL":
|
31
39
|
return (userSettings.barbellIncrement ||
|
@@ -40,238 +48,176 @@ function getPreferredIncrement(equipmentType, userSettings = DEFAULT_EQUIPMENT_S
|
|
40
48
|
DEFAULT_EQUIPMENT_SETTINGS.machineIncrement);
|
41
49
|
case "BODYWEIGHT":
|
42
50
|
return (userSettings.dumbbellIncrement ||
|
43
|
-
DEFAULT_EQUIPMENT_SETTINGS.dumbbellIncrement);
|
51
|
+
DEFAULT_EQUIPMENT_SETTINGS.dumbbellIncrement);
|
44
52
|
default:
|
45
|
-
return 2.5; //
|
53
|
+
return 2.5; // Default fallback
|
46
54
|
}
|
47
55
|
}
|
48
56
|
/**
|
49
|
-
*
|
57
|
+
* Rounds a weight to the nearest multiple of the increment
|
50
58
|
*/
|
51
|
-
function
|
52
|
-
|
53
|
-
|
54
|
-
BARBELL: [1.25, 2.5, 5.0, 10.0],
|
55
|
-
DUMBBELL: [1.0, 2.0, 2.5, 5.0],
|
56
|
-
CABLE: [2.0, 2.5, 5.0, 10.0],
|
57
|
-
MACHINE: [2.5, 5.0, 10.0],
|
58
|
-
BODYWEIGHT: [1.0, 2.5, 5.0], // For weighted bodyweight exercises
|
59
|
-
};
|
60
|
-
// Get user's preferred increment for this equipment type
|
61
|
-
const preferredIncrement = getPreferredIncrement(equipmentType, userSettings);
|
62
|
-
// Return equipment's standard increments with preferred increment first
|
63
|
-
return [
|
64
|
-
preferredIncrement,
|
65
|
-
...standardIncrements[equipmentType].filter((inc) => inc !== preferredIncrement),
|
66
|
-
];
|
59
|
+
function roundToIncrementMultiple(weight, equipmentType, userSettings = DEFAULT_EQUIPMENT_SETTINGS) {
|
60
|
+
const increment = getBaseIncrement(equipmentType, weight, userSettings);
|
61
|
+
return Math.round(weight / increment) * increment;
|
67
62
|
}
|
68
63
|
/**
|
69
|
-
*
|
64
|
+
* Calculate the total volume for a given exercise configuration
|
70
65
|
*/
|
71
|
-
function
|
72
|
-
|
73
|
-
if (rawIncrement <= availableIncrements[0]) {
|
74
|
-
return availableIncrements[0];
|
75
|
-
}
|
76
|
-
// Find closest increment by minimizing absolute difference
|
77
|
-
return availableIncrements.reduce((prev, curr) => Math.abs(curr - rawIncrement) < Math.abs(prev - rawIncrement) ? curr : prev);
|
66
|
+
function calculateVolume(sets, reps, weight) {
|
67
|
+
return sets * reps * weight;
|
78
68
|
}
|
79
69
|
/**
|
80
|
-
*
|
70
|
+
* Handle bodyweight exercises specially
|
81
71
|
*/
|
82
|
-
function
|
83
|
-
// Get available increments for this equipment type
|
84
|
-
const availableIncrements = getEquipmentIncrements(equipmentType, userSettings);
|
85
|
-
const sortedIncrements = [...availableIncrements].sort((a, b) => a - b);
|
86
|
-
const smallestIncrement = sortedIncrements[0] || 1.25;
|
87
|
-
// Base increment size on exercise type and current weight
|
88
|
-
let rawIncrement;
|
89
|
-
if (isCompound) {
|
90
|
-
// Compound movements (bench, squat, deadlift):
|
91
|
-
if (currentWeight < 40) {
|
92
|
-
rawIncrement = 5.0; // Under 40kg: 5kg increments
|
93
|
-
}
|
94
|
-
else if (currentWeight < 100) {
|
95
|
-
rawIncrement = 2.5; // 40-100kg: 2.5kg increments
|
96
|
-
}
|
97
|
-
else {
|
98
|
-
rawIncrement = 1.25; // Over 100kg: 1.25kg increments
|
99
|
-
}
|
100
|
-
}
|
101
|
-
else {
|
102
|
-
// Isolation movements (curls, lateral raises):
|
103
|
-
if (currentWeight < 15) {
|
104
|
-
rawIncrement = 2.5; // Under 15kg: 2.5kg increments
|
105
|
-
}
|
106
|
-
else {
|
107
|
-
rawIncrement = 1.25; // Over 15kg: 1.25kg increments
|
108
|
-
}
|
109
|
-
}
|
110
|
-
// Modify based on experience level
|
111
|
-
const experienceLevel = (userSettings === null || userSettings === void 0 ? void 0 : userSettings.experienceLevel) || "BEGINNER";
|
112
|
-
if (experienceLevel === "INTERMEDIATE" && currentWeight > 60) {
|
113
|
-
// Intermediate lifters progress slower at higher weights
|
114
|
-
rawIncrement = Math.max(smallestIncrement, rawIncrement * 0.5);
|
115
|
-
}
|
116
|
-
else if (experienceLevel === "ADVANCED") {
|
117
|
-
// Advanced lifters progress slower overall
|
118
|
-
rawIncrement = Math.max(smallestIncrement, rawIncrement * 0.5);
|
119
|
-
}
|
120
|
-
// Find the closest available increment
|
121
|
-
return findClosestIncrement(rawIncrement, availableIncrements);
|
122
|
-
}
|
123
|
-
/**
|
124
|
-
* Gets weight increment based on difficulty rating, equipment, weight and experience level
|
125
|
-
*/
|
126
|
-
function getWeightIncrement(currentWeight, equipment, isCompound, rating, userSettings) {
|
127
|
-
// Get the standard increment for this exercise/weight
|
128
|
-
const standardIncrement = getStandardIncrement(currentWeight, equipment, isCompound, userSettings);
|
129
|
-
// Modify based on rating
|
130
|
-
switch (rating) {
|
131
|
-
case 1: // Very Easy - double the increment
|
132
|
-
return standardIncrement * 2;
|
133
|
-
case 2: // Easy - standard increment
|
134
|
-
case 3: // Moderate - standard increment
|
135
|
-
return standardIncrement;
|
136
|
-
case 4: // Hard - no increase
|
137
|
-
return 0;
|
138
|
-
case 5: // Too Hard - decrease
|
139
|
-
return -standardIncrement;
|
140
|
-
default:
|
141
|
-
return standardIncrement;
|
142
|
-
}
|
143
|
-
}
|
144
|
-
function calculateBodyweightProgression(data) {
|
72
|
+
function handleBodyweightExercise(data) {
|
145
73
|
switch (data.rating) {
|
146
74
|
case 1: // Very easy
|
147
75
|
return {
|
148
|
-
newWeight:
|
76
|
+
newWeight: data.weight,
|
149
77
|
newReps: Math.min(data.reps + 2, MAX_REPS),
|
150
78
|
};
|
151
79
|
case 2: // Easy
|
152
|
-
return {
|
153
|
-
newWeight: 0,
|
154
|
-
newReps: Math.min(data.reps + 1, MAX_REPS),
|
155
|
-
};
|
156
80
|
case 3: // Moderate
|
157
81
|
return {
|
158
|
-
newWeight:
|
82
|
+
newWeight: data.weight,
|
159
83
|
newReps: Math.min(data.reps + 1, MAX_REPS),
|
160
84
|
};
|
161
85
|
case 4: // Hard
|
162
86
|
return {
|
163
|
-
newWeight:
|
87
|
+
newWeight: data.weight,
|
164
88
|
newReps: data.reps,
|
165
89
|
};
|
166
90
|
case 5: // Too hard
|
167
91
|
return {
|
168
|
-
newWeight:
|
92
|
+
newWeight: data.weight,
|
169
93
|
newReps: Math.max(1, data.reps - 2),
|
170
94
|
};
|
171
95
|
default:
|
172
96
|
return {
|
173
|
-
newWeight:
|
97
|
+
newWeight: data.weight,
|
174
98
|
newReps: data.reps,
|
175
99
|
};
|
176
100
|
}
|
177
101
|
}
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
};
|
190
|
-
}
|
191
|
-
function calculateVolumeIncrease(data, type, userSettings) {
|
192
|
-
const increment = getWeightIncrement(data.weight, data.equipment_type, data.is_compound, Math.min(data.rating, 3), // Cap at 3 to always get a positive increment for comparison
|
193
|
-
userSettings);
|
194
|
-
const currentVolume = data.sets * data.reps * data.weight;
|
195
|
-
if (type === "weight") {
|
196
|
-
return data.sets * data.reps * (data.weight + increment) - currentVolume;
|
197
|
-
}
|
198
|
-
// Only calculate rep increase if we're not at max reps
|
199
|
-
if (data.reps >= MAX_REPS) {
|
200
|
-
return Number.POSITIVE_INFINITY; // Force weight increase by making rep increase unfavorable
|
201
|
-
}
|
202
|
-
return data.sets * (data.reps + 1) * data.weight - currentVolume;
|
203
|
-
}
|
204
|
-
function calculateHypertrophyProgression(data, userSettings) {
|
205
|
-
// Handle special bodyweight exercises differently
|
206
|
-
if (data.equipment_type === "BODYWEIGHT" &&
|
207
|
-
isSpecialBodyweightExercise(data.exercise_name)) {
|
208
|
-
return calculateBodyweightProgression(data);
|
209
|
-
}
|
210
|
-
const increment = getWeightIncrement(data.weight, data.equipment_type, data.is_compound, data.rating, userSettings);
|
211
|
-
// For compound movements (except special bodyweight exercises), prioritize weight increases
|
212
|
-
if (data.is_compound) {
|
213
|
-
return calculateStrengthProgression(data, userSettings);
|
214
|
-
}
|
215
|
-
// For isolation exercises, implement progressive overload based on rating
|
216
|
-
switch (data.rating) {
|
217
|
-
case 1: {
|
218
|
-
// Very easy
|
219
|
-
const newReps = data.reps >= MAX_REPS ? data.reps : data.reps + 1;
|
220
|
-
return {
|
221
|
-
newWeight: data.weight + increment,
|
222
|
-
newReps: newReps,
|
223
|
-
};
|
224
|
-
}
|
225
|
-
case 2: {
|
226
|
-
// Easy - increase either weight or reps (larger increase)
|
227
|
-
if (data.reps >= MAX_REPS) {
|
228
|
-
return { newWeight: data.weight + increment, newReps: data.reps };
|
229
|
-
}
|
230
|
-
const volumeIncreaseWeight = calculateVolumeIncrease(data, "weight", userSettings);
|
231
|
-
const volumeIncreaseReps = calculateVolumeIncrease(data, "reps", userSettings);
|
232
|
-
return volumeIncreaseWeight > volumeIncreaseReps
|
233
|
-
? { newWeight: data.weight + increment, newReps: data.reps }
|
234
|
-
: { newWeight: data.weight, newReps: data.reps + 1 };
|
235
|
-
}
|
236
|
-
case 3: {
|
237
|
-
// Moderate - choose smallest increase between weight and reps
|
238
|
-
if (data.reps >= MAX_REPS) {
|
239
|
-
return { newWeight: data.weight + increment, newReps: data.reps };
|
240
|
-
}
|
241
|
-
const weightIncrease = calculateVolumeIncrease(data, "weight", userSettings);
|
242
|
-
const repIncrease = calculateVolumeIncrease(data, "reps", userSettings);
|
243
|
-
return weightIncrease < repIncrease
|
244
|
-
? { newWeight: data.weight + increment, newReps: data.reps }
|
245
|
-
: { newWeight: data.weight, newReps: data.reps + 1 };
|
246
|
-
}
|
102
|
+
/**
|
103
|
+
* Calculate weight change based on rating
|
104
|
+
*/
|
105
|
+
function getWeightChangeForRating(rating, baseIncrement) {
|
106
|
+
switch (rating) {
|
107
|
+
case 1: // Very easy
|
108
|
+
return baseIncrement * 3;
|
109
|
+
case 2: // Easy
|
110
|
+
return baseIncrement * 2;
|
111
|
+
case 3: // Moderate
|
112
|
+
return baseIncrement;
|
247
113
|
case 4: // Hard
|
248
|
-
return
|
114
|
+
return 0;
|
249
115
|
case 5: // Too hard
|
250
|
-
return
|
251
|
-
newWeight: Math.max(0, data.weight + increment),
|
252
|
-
newReps: data.reps,
|
253
|
-
};
|
116
|
+
return -baseIncrement;
|
254
117
|
default:
|
255
|
-
return
|
118
|
+
return 0;
|
256
119
|
}
|
257
120
|
}
|
258
121
|
/**
|
259
122
|
* Main function to calculate progression based on exercise data and program type
|
260
|
-
* Now accepts optional userSettings for equipment-specific increments
|
261
123
|
*/
|
262
|
-
function calculateProgression(data, programType, userSettings) {
|
124
|
+
function calculateProgression(data, programType, userSettings = DEFAULT_EQUIPMENT_SETTINGS) {
|
125
|
+
console.log(`Calculate progression for ${data.exercise_name}:`, {
|
126
|
+
currentWeight: data.weight,
|
127
|
+
currentReps: data.reps,
|
128
|
+
rating: data.rating,
|
129
|
+
equipmentType: data.equipment_type,
|
130
|
+
programType,
|
131
|
+
});
|
132
|
+
// Special handling for bodyweight exercises
|
133
|
+
if (data.equipment_type === "BODYWEIGHT" &&
|
134
|
+
isSpecialBodyweightExercise(data.exercise_name)) {
|
135
|
+
return handleBodyweightExercise(data);
|
136
|
+
}
|
137
|
+
// Get the base increment for this equipment type
|
138
|
+
const baseIncrement = getBaseIncrement(data.equipment_type, data.weight, userSettings);
|
139
|
+
console.log(`Using ${data.equipment_type} increment: ${baseIncrement}kg`);
|
140
|
+
// Calculate weight change based on rating
|
141
|
+
const weightChange = getWeightChangeForRating(data.rating, baseIncrement);
|
142
|
+
// For strength programs, focus on weight increases
|
263
143
|
if (programType === "STRENGTH") {
|
264
|
-
|
144
|
+
const rawNewWeight = Math.max(0, data.weight + weightChange);
|
145
|
+
// Round the weight to the nearest multiple of the increment
|
146
|
+
const roundedWeight = roundToIncrementMultiple(rawNewWeight, data.equipment_type, userSettings);
|
147
|
+
return {
|
148
|
+
newWeight: roundedWeight,
|
149
|
+
newReps: data.reps,
|
150
|
+
};
|
265
151
|
}
|
266
|
-
|
152
|
+
// HYPERTROPHY program logic
|
153
|
+
// For ratings 4 (Hard) and 5 (Too Hard), no rep increase, only potential weight decrease
|
154
|
+
if (data.rating >= 4) {
|
155
|
+
const rawNewWeight = Math.max(0, data.weight + weightChange);
|
156
|
+
const roundedWeight = roundToIncrementMultiple(rawNewWeight, data.equipment_type, userSettings);
|
157
|
+
return {
|
158
|
+
newWeight: roundedWeight,
|
159
|
+
newReps: data.reps,
|
160
|
+
};
|
161
|
+
}
|
162
|
+
// If we've reached max reps, cycle back to starting reps with increased weight
|
163
|
+
if (data.reps >= MAX_REPS) {
|
164
|
+
// Calculate how much weight to increase to maintain volume
|
165
|
+
// Current volume: sets * maxReps * currentWeight
|
166
|
+
// New volume: sets * startingReps * newWeight
|
167
|
+
// So, newWeight = currentWeight * (maxReps / startingReps)
|
168
|
+
// Ensure the weight increases by at least the rating-based increment
|
169
|
+
const volumeBasedIncrement = data.weight * (MAX_REPS / STARTING_HYPERTROPHY_REPS) - data.weight;
|
170
|
+
const actualIncrement = Math.max(weightChange, volumeBasedIncrement);
|
171
|
+
const rawNewWeight = Math.max(0, data.weight + actualIncrement);
|
172
|
+
const roundedWeight = roundToIncrementMultiple(rawNewWeight, data.equipment_type, userSettings);
|
173
|
+
return {
|
174
|
+
newWeight: roundedWeight,
|
175
|
+
newReps: STARTING_HYPERTROPHY_REPS,
|
176
|
+
};
|
177
|
+
}
|
178
|
+
// For ratings 1-3 with non-maxed reps, compare volume increases
|
179
|
+
// Option 1: Increase weight
|
180
|
+
const rawNewWeight = Math.max(0, data.weight + weightChange);
|
181
|
+
const roundedNewWeight = roundToIncrementMultiple(rawNewWeight, data.equipment_type, userSettings);
|
182
|
+
const volumeWithWeightIncrease = calculateVolume(data.sets, data.reps, roundedNewWeight);
|
183
|
+
// Option 2: Increase reps
|
184
|
+
const newReps = data.reps + 1;
|
185
|
+
const volumeWithRepIncrease = calculateVolume(data.sets, newReps, data.weight);
|
186
|
+
// Calculate volume change for each option
|
187
|
+
const currentVolume = calculateVolume(data.sets, data.reps, data.weight);
|
188
|
+
const volumeChangeWithWeight = volumeWithWeightIncrease - currentVolume;
|
189
|
+
const volumeChangeWithReps = volumeWithRepIncrease - currentVolume;
|
190
|
+
console.log("Volume comparison:", {
|
191
|
+
currentVolume,
|
192
|
+
volumeWithWeightIncrease,
|
193
|
+
volumeWithRepIncrease,
|
194
|
+
volumeChangeWithWeight,
|
195
|
+
volumeChangeWithReps,
|
196
|
+
});
|
197
|
+
// For very easy (rating 1), always increase both weight and reps if compound
|
198
|
+
if (data.rating === 1 && data.is_compound) {
|
199
|
+
return {
|
200
|
+
newWeight: roundedNewWeight,
|
201
|
+
newReps: Math.min(newReps, MAX_REPS),
|
202
|
+
};
|
203
|
+
}
|
204
|
+
// For other cases, choose the option with the smaller volume increase
|
205
|
+
// This helps make sure progression is steady and not too aggressive
|
206
|
+
if (volumeChangeWithWeight <= volumeChangeWithReps) {
|
207
|
+
return {
|
208
|
+
newWeight: roundedNewWeight,
|
209
|
+
newReps: data.reps,
|
210
|
+
};
|
211
|
+
}
|
212
|
+
return {
|
213
|
+
newWeight: data.weight,
|
214
|
+
newReps: newReps,
|
215
|
+
};
|
267
216
|
}
|
268
217
|
/**
|
269
218
|
* Helper function to round weight to closest available increment
|
270
219
|
* Useful for client-side adjustments
|
271
220
|
*/
|
272
|
-
function roundToClosestIncrement(weight, equipmentType, userSettings) {
|
273
|
-
|
274
|
-
const preferredIncrement = availableIncrements[0];
|
275
|
-
// Round to nearest multiple of preferred increment
|
276
|
-
return Math.round(weight / preferredIncrement) * preferredIncrement;
|
221
|
+
function roundToClosestIncrement(weight, equipmentType, userSettings = DEFAULT_EQUIPMENT_SETTINGS) {
|
222
|
+
return roundToIncrementMultiple(weight, equipmentType, userSettings);
|
277
223
|
}
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "fitness-progression-calculator",
|
3
|
-
"version": "1.
|
3
|
+
"version": "1.3.0",
|
4
4
|
"description": "Workout progression calculator for fitness applications",
|
5
5
|
"main": "dist/index.js",
|
6
6
|
"types": "dist/index.d.ts",
|
@@ -10,8 +10,12 @@
|
|
10
10
|
"build": "tsc",
|
11
11
|
"prepare": "npm run build"
|
12
12
|
},
|
13
|
+
"repository": {
|
14
|
+
"type": "git",
|
15
|
+
"url": "git+https://github.com/VictorNorm/fitness-progression-calculator.git"
|
16
|
+
},
|
13
17
|
"keywords": ["fitness", "workout", "progression"],
|
14
|
-
"author": "
|
18
|
+
"author": "Victor Norman",
|
15
19
|
"license": "MIT",
|
16
20
|
"files": ["dist/**/*"],
|
17
21
|
"devDependencies": {
|