diff --git a/CHANGELOG.md b/CHANGELOG.md index b156be3..af4ceba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ Numérotation `MAJEUR.MINEUR.CORRECTIF`. Le numéro vit dans `app/build.gradle.kts` (`versionName`), et `versionCode` s'incrémente de un à chaque version publiée. +## 1.1.1 (2026-11-02) + +### Corrections +- Les macros proposées étaient intenables en cas de surpoids : rapportées au poids total, protéines et lipides mangeaient presque tout l'objectif et ne laissaient qu'une cinquantaine de grammes de glucides. Les protéines suivent maintenant un poids de référence plafonné — la masse grasse n'a pas de besoin protéique — et les lipides sont une part des calories, non un montant par kilo. Pour 126 kg à 2 353 kcal : 186 g de protéines, 237 g de glucides et 73 g de lipides, contre 277 / 57 / 113 avant. + ## 1.1.0 (2026-11-02) ### Nouveautés diff --git a/README.md b/README.md index 8822838..e88f88c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # FitTrack -**Version 1.1.0** — les nouveautés de chaque version sont dans [CHANGELOG.md](CHANGELOG.md). +**Version 1.1.1** — les nouveautés de chaque version sont dans [CHANGELOG.md](CHANGELOG.md). Application Android pour **suivre ses séances de salle** et **tracker ses calories et ses macros**. Pas de compte, pas de serveur, pas de publicité : tout est stocké sur le téléphone. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 33f0993..e262a4d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,8 +13,8 @@ android { applicationId = "com.kaz229.fittrack" minSdk = 26 targetSdk = 35 - versionCode = 3 - versionName = "1.1.0" + versionCode = 4 + versionName = "1.1.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/java/com/kaz229/fittrack/data/Needs.kt b/app/src/main/java/com/kaz229/fittrack/data/Needs.kt index 06cece7..ffe1799 100644 --- a/app/src/main/java/com/kaz229/fittrack/data/Needs.kt +++ b/app/src/main/java/com/kaz229/fittrack/data/Needs.kt @@ -133,7 +133,7 @@ fun computeNeeds(profile: NeedsProfile, weightKg: Double): Needs? { trainingKcal = trainingKcal.roundToInt(), maintenance = maintenance.roundToInt(), target = target, - macros = macrosFor(profile.goal, weightKg, target), + macros = macrosFor(profile.goal, weightKg, profile.heightCm, target), weeklyDeficit = deficit * 7, achievableWeeklyLossKg = if (deficit > 0) deficit * 7 / KCAL_PER_KG else 0.0, cappedByFloor = profile.goal == Goal.LOSE && wanted < floor, @@ -144,27 +144,51 @@ private fun metKcal(intensity: Intensity, minutes: Int, weightKg: Double): Doubl intensity.met * 3.5 * weightKg / 200.0 * minutes /** - * Protéines et lipides d'abord, glucides avec ce qui reste. + * Poids sur lequel asseoir les besoins en protéines. * - * Les protéines se calent sur le poids (plus hautes en perte, pour garder le muscle), - * les lipides sur un plancher hormonal, et les glucides absorbent le solde — ce sont - * eux qui bougent quand l'objectif change. + * Les rapporter au poids total donne des quantités intenables dès qu'il y a du surpoids : + * la masse grasse n'a pas de besoin protéique. La référence est donc plafonnée au poids + * d'un IMC de 25, majoré de 15 % pour ne pas pénaliser une vraie masse musculaire. */ -private fun macrosFor(goal: Goal, weightKg: Double, target: Int): MacroGoals { +private fun referenceWeight(weightKg: Double, heightCm: Int): Double { + if (heightCm <= 0) return weightKg + val meters = heightCm / 100.0 + return minOf(weightKg, 25.0 * meters * meters * 1.15) +} + +/** + * Protéines d'abord, lipides en part des calories, glucides avec ce qui reste. + * + * Les lipides sont une **proportion** de l'objectif, pas un montant par kilo : à poids + * élevé, 0,9 g/kg dépassait 40 % des calories et ne laissait presque rien aux glucides. + * Un plancher les empêche de tomber trop bas quand l'objectif est serré. + */ +private fun macrosFor(goal: Goal, weightKg: Double, heightCm: Int, target: Int): MacroGoals { + val reference = referenceWeight(weightKg, heightCm) val proteinPerKg = when (goal) { - Goal.LOSE -> 2.2 + Goal.LOSE -> 2.0 Goal.GAIN -> 1.8 - Goal.MAINTAIN -> 1.8 + Goal.MAINTAIN -> 1.6 } - val protein = (weightKg * proteinPerKg).roundToInt() - val fat = (weightKg * 0.9).roundToInt() + val protein = (reference * proteinPerKg).roundToInt() + + // 28 % des calories en lipides : la part basse de ce qui se recommande couramment, + // sans descendre sous le seuil hormonal de 0,6 g par kilo de référence. + val fatFloor = (reference * 0.6).roundToInt() + val fat = maxOf((target * 0.28 / 9).roundToInt(), fatFloor) + val remaining = target - protein * 4 - fat * 9 - // Jamais de glucides négatifs : sur un objectif très bas, on rogne les lipides - val carbs = if (remaining >= 0) { - remaining / 4 + if (remaining >= 0) { + return MacroGoals(proteinG = protein, carbsG = remaining / 4, fatG = fat) + } + // Objectif trop serré pour les deux : on rogne les lipides jusqu'à leur plancher, + // puis les protéines, pour garder des glucides utilisables. + val reducedFat = maxOf(((target - protein * 4) / 9), fatFloor / 2).coerceAtLeast(0) + val leftover = target - protein * 4 - reducedFat * 9 + return if (leftover >= 0) { + MacroGoals(proteinG = protein, carbsG = leftover / 4, fatG = reducedFat) } else { - 0 + val reducedProtein = ((target - reducedFat * 9) / 4).coerceAtLeast(0) + MacroGoals(proteinG = reducedProtein, carbsG = 0, fatG = reducedFat) } - val adjustedFat = if (remaining >= 0) fat else ((target - protein * 4) / 9).coerceAtLeast(0) - return MacroGoals(proteinG = protein, carbsG = carbs, fatG = adjustedFat) } diff --git a/app/src/test/java/com/kaz229/fittrack/NeedsTest.kt b/app/src/test/java/com/kaz229/fittrack/NeedsTest.kt index 6ba45f7..2465fa4 100644 --- a/app/src/test/java/com/kaz229/fittrack/NeedsTest.kt +++ b/app/src/test/java/com/kaz229/fittrack/NeedsTest.kt @@ -131,8 +131,38 @@ class NeedsTest { val maintien = computeNeeds(base, 75.0)!! val perte = computeNeeds(base.copy(goal = Goal.LOSE), 75.0)!! - assertEquals(135, maintien.macros.proteinG) // 1,8 g/kg - assertEquals(165, perte.macros.proteinG) // 2,2 g/kg + assertEquals(120, maintien.macros.proteinG) // 1,6 g/kg + assertEquals(150, perte.macros.proteinG) // 2,0 g/kg + assertTrue(perte.macros.proteinG > maintien.macros.proteinG) + } + + @Test + fun `en surpoids les proteines suivent un poids de reference, pas le poids total`() { + // 126 kg pour 1,80 m : 2 g/kg donneraient 252 g, intenables et inutiles — + // la masse grasse n'a pas de besoin protéique. + val lourd = computeNeeds(base.copy(goal = Goal.LOSE), weightKg = 126.0)!! + + assertTrue("Obtenu ${lourd.macros.proteinG} g", lourd.macros.proteinG in 150..200) + } + + @Test + fun `les glucides restent utilisables meme a poids eleve`() { + // Le défaut d'origine : protéines et lipides au poids total mangeaient 2129 des + // 2353 kcal, et il ne restait que 57 g de glucides. + val lourd = computeNeeds(base.copy(goal = Goal.LOSE), weightKg = 126.0)!! + + assertTrue("Obtenu ${lourd.macros.carbsG} g", lourd.macros.carbsG > 150) + } + + @Test + fun `les lipides sont une part des calories, pas un montant par kilo`() { + val leger = computeNeeds(base, 60.0)!! + val lourd = computeNeeds(base, 126.0)!! + + // À 0,9 g/kg, le lourd aurait 113 g ; en part des calories il reste mesuré + assertTrue("Obtenu ${lourd.macros.fatG} g", lourd.macros.fatG < 110) + assertTrue(lourd.macros.fatG * 9 < lourd.target * 0.35) + assertTrue(leger.macros.fatG * 9 < leger.target * 0.35) } @Test