Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Mini set comparison and volume inclusion #113

Merged
merged 6 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 45 additions & 14 deletions src/lib/utils/workoutUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,21 @@ export function getExerciseVolume(workoutExercise: WorkoutExercise, userBodyweig
);
}

export function cleanupInProgressMiniSets(miniSets: WorkoutExerciseInProgress['sets'][number]['miniSets']) {
return miniSets.map((miniSet) => {
return {
reps: miniSet.reps ?? 0,
load: miniSet.load ?? 0,
RIR: miniSet.RIR ?? 0
};
});
}

export type SetDetails = {
reps: number;
load: number;
RIR: number;
miniSets?: {
miniSets: {
reps: number;
load: number;
RIR: number;
Expand All @@ -34,13 +44,13 @@ type CommonBergerType = {
bodyweightFraction: number | null;
oldUserBodyweight?: number;
newUserBodyweight?: number;
overloadPercentage?: number;
oldSet: SetDetails;
};

type BergerNewReps = {
variableToSolve: 'NewReps';
knownValues: CommonBergerType & {
overloadPercentage: number;
newSet: Omit<SetDetails, 'reps'> & { reps?: number };
};
};
Expand All @@ -56,14 +66,7 @@ type BergerInput = BergerNewReps | BergerOverloadPercentage;

export function solveBergerFormula(input: BergerInput) {
const { variableToSolve, knownValues } = input;
const {
oldSet,
newSet,
bodyweightFraction = null,
oldUserBodyweight = 0,
newUserBodyweight = 0,
overloadPercentage = 0
} = knownValues;
const { oldSet, newSet, bodyweightFraction = null, oldUserBodyweight = 0, newUserBodyweight = 0 } = knownValues;

const oldLoad = oldSet.load + (bodyweightFraction ?? 0) * oldUserBodyweight;
const newLoad = newSet.load + (bodyweightFraction ?? 0) * newUserBodyweight;
Expand All @@ -72,7 +75,8 @@ export function solveBergerFormula(input: BergerInput) {

switch (variableToSolve) {
case 'NewReps': {
const numerator = (1 + overloadPercentage / 100) * (9745640 * oldLoad - 423641) * exponentialMultiplier;
const numerator =
(1 + knownValues.overloadPercentage / 100) * (9745640 * oldLoad - 423641) * exponentialMultiplier;
const denominator = 9745640 * newLoad - 423641;
return 38.1679 * Math.log(numerator / denominator) - newSet.RIR;
}
Expand All @@ -81,7 +85,33 @@ export function solveBergerFormula(input: BergerInput) {
const numeratorMultiplier = Math.pow(Math.E, (knownValues.newSet.reps + newSet.RIR) / 38.1679);
const numerator = numeratorMultiplier * (9745640 * newLoad - 423641);
const denominator = exponentialMultiplier * (9745640 * oldLoad - 423641);
return (numerator / denominator - 1) * 100;
const overloadPercentage = (numerator / denominator - 1) * 100;

let miniSetsCompared = 0;
let totalMiniSetsOverload = 0;
for (let i = 0; i < Math.max(newSet.miniSets.length, oldSet.miniSets.length); i++) {
const prevMiniSet = oldSet.miniSets[i];
const newMiniSet = newSet.miniSets[i];
if (!prevMiniSet || !newMiniSet) break;

totalMiniSetsOverload += solveBergerFormula({
variableToSolve: 'OverloadPercentage',
knownValues: {
newSet: { ...newMiniSet, miniSets: [] },
oldSet: { ...prevMiniSet, miniSets: [] },
bodyweightFraction,
newUserBodyweight,
oldUserBodyweight
}
});
miniSetsCompared++;
}

if (miniSetsCompared === 0) {
return overloadPercentage;
}

return (overloadPercentage + totalMiniSetsOverload) / (miniSetsCompared + 1);
}
}
}
Expand Down Expand Up @@ -283,11 +313,12 @@ function increaseLoadOfSets(ex: WorkoutExerciseInProgress, userBodyweight: numbe
newLoad += ex.minimumWeightChange ?? 5;
}

const cleanedMiniSets = cleanupInProgressMiniSets(set.miniSets);
const newReps = solveBergerFormula({
variableToSolve: 'NewReps',
knownValues: {
oldSet: { reps: set.reps, load: set.load, RIR: set.RIR },
newSet: { load: newLoad, RIR: set.RIR },
oldSet: { reps: set.reps, load: set.load, RIR: set.RIR, miniSets: cleanedMiniSets },
newSet: { load: newLoad, RIR: set.RIR, miniSets: cleanedMiniSets },
bodyweightFraction: ex.bodyweightFraction ?? null,
newUserBodyweight: userBodyweight,
oldUserBodyweight: userBodyweight,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
<script lang="ts">
import { solveBergerFormula, type WorkoutExerciseInProgress } from '$lib/utils/workoutUtils';
import * as Popover from '$lib/components/ui/popover';
import { workoutRunes } from '../../workoutRunes.svelte';
import TrendUpIcon from 'virtual:icons/lucide/trending-up';
import UpIcon from 'virtual:icons/lucide/chevron-up';
import TrendDownIcon from 'virtual:icons/lucide/trending-down';
import DownIcon from 'virtual:icons/lucide/chevron-down';
import Minus from 'virtual:icons/lucide/minus';
import Separator from '$lib/components/ui/separator/separator.svelte';
import { arrayAverage } from '$lib/utils';
import {
cleanupInProgressMiniSets,
solveBergerFormula,
type WorkoutExerciseInProgress
} from '$lib/utils/workoutUtils';
import DownIcon from 'virtual:icons/lucide/chevron-down';
import UpIcon from 'virtual:icons/lucide/chevron-up';
import Minus from 'virtual:icons/lucide/minus';
import TrendDownIcon from 'virtual:icons/lucide/trending-down';
import TrendUpIcon from 'virtual:icons/lucide/trending-up';
import { workoutRunes } from '../../workoutRunes.svelte';

type PropsType = { exercise: WorkoutExerciseInProgress };
let { exercise }: PropsType = $props();
Expand All @@ -17,23 +21,26 @@

function getTheoreticalVolumeChange(setIdx: number) {
const prevSet = prevExercise?.sets[setIdx];
const currentSet = exercise.sets[setIdx];

if (!prevSet) return;
if (!exercise.sets[setIdx]) return;
if (exercise.sets[setIdx].skipped || prevSet.skipped) return;
if (!currentSet) return;
if (currentSet.skipped || prevSet.skipped) return;

let { reps, load, RIR } = exercise.sets[setIdx];
if (reps === undefined || load === undefined || RIR === undefined) return;
if (!isSetCompleted(currentSet)) return;
let { reps, load, RIR, miniSets } = currentSet;

const actualOverload = solveBergerFormula({
variableToSolve: 'OverloadPercentage',
knownValues: {
oldSet: prevSet,
newSet: { reps, load, RIR },
newSet: { reps, load, RIR, miniSets: cleanupInProgressMiniSets(miniSets) },
newUserBodyweight: workoutRunes.workoutData?.userBodyweight as number,
oldUserBodyweight: workoutRunes.previousWorkoutData?.userBodyweight,
bodyweightFraction: exercise.bodyweightFraction ?? null
}
});
console.log(actualOverload);

return actualOverload;
}
Expand All @@ -45,6 +52,30 @@
);
}

type InProgressSet = { reps?: number; load?: number; RIR?: number; completed: boolean };
type CompletedSet = { reps: number; load: number; RIR: number; completed: boolean };

function isSetCompleted(miniSet: InProgressSet): miniSet is CompletedSet {
const { reps, load, RIR } = miniSet;
return reps !== undefined && load !== undefined && RIR !== undefined;
}

function getTheoreticalVolumeChangeOfMiniSet(prev: Omit<CompletedSet, 'completed'>, current: InProgressSet) {
if (!isSetCompleted(current)) return;

const actualOverload = solveBergerFormula({
variableToSolve: 'OverloadPercentage',
knownValues: {
oldSet: { ...prev, miniSets: [] },
newSet: { ...current, miniSets: [] },
newUserBodyweight: workoutRunes.workoutData?.userBodyweight as number,
oldUserBodyweight: workoutRunes.previousWorkoutData?.userBodyweight,
bodyweightFraction: exercise.bodyweightFraction ?? null
}
});
return actualOverload;
}

let totalVolumeChange = $derived(getAverageVolumeChangeOfAllSets());
</script>

Expand All @@ -67,7 +98,7 @@
</span>
<span class="text-sm font-medium">RIR</span>
<span class="flex w-full items-center justify-end gap-1 text-sm font-semibold">
{#if totalVolumeChange !== undefined}
{#if !isNaN(Number(totalVolumeChange)) && totalVolumeChange !== undefined}
{totalVolumeChange.toFixed(2)}%
{#if totalVolumeChange < 0}
<TrendDownIcon class="justify-self-end" />
Expand All @@ -85,26 +116,20 @@
<p>
{#if prevSet.reps !== set.reps}
<span class="text-muted-foreground">{prevSet.reps} -&gt;</span>
{set.reps}
{:else}
{set.reps}
{/if}
{set.reps}
</p>
<p>
{#if prevSet.load !== set.load}
<span class="text-muted-foreground">{prevSet.load} -&gt;</span>
{set.load}
{:else}
{set.load}
{/if}
{set.load}
</p>
<p>
{#if prevSet.RIR !== set.RIR}
<span class="text-muted-foreground">{prevSet.RIR} -&gt;</span>
{set.RIR}
{:else}
{set.RIR}
{/if}
{set.RIR}
</p>
<span class="flex w-full items-center justify-end gap-1 text-sm font-light">
{#if typeof volumeChange === 'number'}
Expand All @@ -118,7 +143,47 @@
{/if}
{/if}
</span>
<!-- TODO: #85 -->
{#each set.miniSets as miniSet, miniSetIdx}
{@const prevMiniSet = prevExercise.sets[idx].miniSets[miniSetIdx]}
{@const miniSetVolumeChange = getTheoreticalVolumeChangeOfMiniSet(prevMiniSet, miniSet)}
{#if prevMiniSet}
<p class="text-sm font-light italic">
{#if prevMiniSet.reps !== miniSet.reps}
<span class="text-muted-foreground">{prevMiniSet.reps} -&gt;</span>
{/if}
{miniSet.reps}
</p>
<p class="text-sm font-light italic">
{#if prevMiniSet.load !== miniSet.load}
<span class="text-muted-foreground">{prevMiniSet.load} -&gt;</span>
{/if}
{miniSet.load}
</p>
<p class="text-sm font-light italic">
{#if prevMiniSet.RIR !== miniSet.RIR}
<span class="text-muted-foreground">{prevMiniSet.RIR} -&gt;</span>
{/if}
{miniSet.RIR}
</p>
<span class="flex w-full items-center justify-end gap-1 text-xs font-light italic">
{#if typeof miniSetVolumeChange === 'number'}
<span>{miniSetVolumeChange?.toFixed(2)}%</span>
{#if miniSetVolumeChange < 0}
<DownIcon class="justify-self-end" />
{:else if miniSetVolumeChange > 0}
<UpIcon class="justify-self-end" />
{:else}
<Minus class="justify-self-end" />
{/if}
{/if}
</span>
{:else}
<Separator />
<span class="text-center text-sm text-muted-foreground">new mini set</span>
<Separator />
<span></span>
{/if}
{/each}
{:else}
<Separator />
<span class="text-center text-sm text-muted-foreground">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Separator } from '$lib/components/ui/separator';
import { solveBergerFormula, type WorkoutExerciseInProgress } from '$lib/utils/workoutUtils';
import {
cleanupInProgressMiniSets,
solveBergerFormula,
type WorkoutExerciseInProgress
} from '$lib/utils/workoutUtils';
import CheckIcon from 'virtual:icons/lucide/check';
import RemoveIcon from 'virtual:icons/lucide/minus';
import EditIcon from 'virtual:icons/lucide/pencil';
Expand Down Expand Up @@ -111,11 +115,17 @@
solveBergerFormula({
variableToSolve: 'NewReps',
knownValues: {
oldSet: { reps: exerciseSet.reps, load: oldLoad, RIR: exerciseSet.RIR },
newSet: { load: newLoad, RIR: exerciseSet.RIR },
oldSet: {
reps: exerciseSet.reps,
load: oldLoad,
RIR: exerciseSet.RIR,
miniSets: cleanupInProgressMiniSets(exerciseSet.miniSets)
},
newSet: { load: newLoad, RIR: exerciseSet.RIR, miniSets: cleanupInProgressMiniSets(exerciseSet.miniSets) },
oldUserBodyweight: workoutRunes.previousWorkoutData?.userBodyweight,
newUserBodyweight: workoutRunes.workoutData?.userBodyweight as number,
bodyweightFraction: exercise.bodyweightFraction ?? null
bodyweightFraction: exercise.bodyweightFraction ?? null,
overloadPercentage: 0
}
})
);
Expand All @@ -131,8 +141,8 @@
solveBergerFormula({
variableToSolve: 'NewReps',
knownValues: {
oldSet: { reps: set.reps, load: oldLoad, RIR: set.RIR },
newSet: { load: newLoad, RIR: set.RIR },
oldSet: { reps: set.reps, load: oldLoad, RIR: set.RIR, miniSets: cleanupInProgressMiniSets(set.miniSets) },
newSet: { load: newLoad, RIR: set.RIR, miniSets: cleanupInProgressMiniSets(set.miniSets) },
oldUserBodyweight: workoutRunes.previousWorkoutData?.userBodyweight,
newUserBodyweight: workoutRunes.workoutData?.userBodyweight as number,
bodyweightFraction: exercise.bodyweightFraction ?? null,
Expand All @@ -144,8 +154,8 @@
extraOverloadAchieved += solveBergerFormula({
variableToSolve: 'OverloadPercentage',
knownValues: {
oldSet: { reps: set.reps, load: oldLoad, RIR: set.RIR },
newSet: { reps: newReps, load: newLoad, RIR: set.RIR },
oldSet: { reps: set.reps, load: oldLoad, RIR: set.RIR, miniSets: cleanupInProgressMiniSets(set.miniSets) },
newSet: { reps: newReps, load: newLoad, RIR: set.RIR, miniSets: cleanupInProgressMiniSets(set.miniSets) },
oldUserBodyweight: workoutRunes.previousWorkoutData?.userBodyweight,
newUserBodyweight: workoutRunes.workoutData?.userBodyweight as number,
bodyweightFraction: exercise.bodyweightFraction ?? null
Expand Down
2 changes: 1 addition & 1 deletion src/routes/workouts/manage/overview/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@
}}
/>
{:else}
<span class="muted-textbox">No previous workout available to compare</span>
<div class="muted-text-box">No previous workout available to compare</div>
{/if}
</Tabs.Content>
<Tabs.Content class="rounded-md border bg-card p-4" value="basic">
Expand Down
Loading