v1.9.6 : la carte Accueil liste TOUTES les prochaines doses (triées)

Remontée : le rappel quotidien (CPA) masquait le rappel hebdomadaire
(EEn) — « je ne le vois quasiment pas ».

- NextDoseCard : UNE ligne par traitement, TRIÉES par prochaine prise ;
  1ʳᵉ ligne (la plus proche) en avant, suivantes en style secondaire ;
  titre pluriel « Prochaines doses » dès 2 lignes (next_doses FR/EN)
- Helper PUR nextDoseLine (locale/zone paramétrables, template jours
  localisé next_dose_days) + 3 tests NextDoseLineTest
- Notifications inchangées (une alarme par traitement)
- versionCode 43 / 1.9.6 + CHANGELOG/§2/README/GUIDE
This commit is contained in:
Siphonight 2026-09-21 21:56:32 +02:00
parent 0f0ecc3d23
commit a40d9bc076
9 changed files with 133 additions and 32 deletions

View File

@ -16,7 +16,7 @@ sauvegarde JSON. **100 % local, aucun compte, aucun serveur.**
> à titre informatif — ce ne sont pas des mesures. Fie-toi toujours à tes analyses de > à titre informatif — ce ne sont pas des mesures. Fie-toi toujours à tes analyses de
> sang et aux consignes de ton endocrinologue. > sang et aux consignes de ton endocrinologue.
- **Statut** : v1.9.5 — build Android ✅, **lint vert** ✅, **244 tests unitaires** ✅ (214 sans les données de test locales ; régressions épinglées sur données réelles **non versionnées**), intégration montre = notifications ✅, **dépôt Gitea privé + releases avec APK** ✅ - **Statut** : v1.9.6 — build Android ✅, **lint vert** ✅, **247 tests unitaires** ✅ (217 sans les données de test locales ; régressions épinglées sur données réelles **non versionnées**), intégration montre = notifications ✅, **dépôt Gitea privé + releases avec APK** ✅
- **Journal des versions** : [docs/CHANGELOG.md](docs/CHANGELOG.md) - **Journal des versions** : [docs/CHANGELOG.md](docs/CHANGELOG.md)
- **Guide utilisateur** : [docs/GUIDE_INSTALLATION.md](docs/GUIDE_INSTALLATION.md) - **Guide utilisateur** : [docs/GUIDE_INSTALLATION.md](docs/GUIDE_INSTALLATION.md)
- **Doc de développement** (architecture, maths, décisions, bugs) : [docs/DEVELOPPEMENT.md](docs/DEVELOPPEMENT.md) - **Doc de développement** (architecture, maths, décisions, bugs) : [docs/DEVELOPPEMENT.md](docs/DEVELOPPEMENT.md)
@ -123,7 +123,7 @@ par AGP si les licences sont signées). Le wrapper télécharge Gradle 9.7.1.
git clone <repo> && cd HormoneTrack git clone <repo> && cd HormoneTrack
echo "sdk.dir=/chemin/vers/android-sdk" > local.properties # ou ANDROID_HOME echo "sdk.dir=/chemin/vers/android-sdk" > local.properties # ou ANDROID_HOME
./gradlew assembleDebug # APK : app/build/outputs/apk/debug/app-debug.apk ./gradlew assembleDebug # APK : app/build/outputs/apk/debug/app-debug.apk
./gradlew testDebugUnitTest # 244 tests (214 sans les données locales) ./gradlew testDebugUnitTest # 247 tests (217 sans les données locales)
./gradlew lint # lint vert obligatoire avant release ./gradlew lint # lint vert obligatoire avant release
``` ```

View File

@ -17,8 +17,8 @@ android {
// tag : v1.3.0→v1.3.2 contenaient tous versionCode 14 / "1.3.0" // tag : v1.3.0→v1.3.2 contenaient tous versionCode 14 / "1.3.0"
// (bump jamais commité) → BuildConfig.VERSION_NAME était faux dans // (bump jamais commité) → BuildConfig.VERSION_NAME était faux dans
// les APK publiés (Paramètres + titre du dialog « Nouveautés »). // les APK publiés (Paramètres + titre du dialog « Nouveautés »).
versionCode = 42 versionCode = 43
versionName = "1.9.5" versionName = "1.9.6"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {

View File

@ -458,48 +458,76 @@ private fun NowLevelCard(curve: List<LevelPoint>) {
} }
} }
/**
* Ligne « prochaine prise » d'un traitement (v1.9.6 — PUR, testable JVM) :
* « Xj Yh · ven. 25 10:00 (nom) » au-delà de 24 h (les heures deviennent peu
* lisibles, v1.4.1), « XhMM · HH:mm (nom) » en dessous, « N min · HH:mm
* (nom) » dans l'heure. Utilisée par chaque ligne de [NextDoseCard].
*/
internal fun nextDoseLine(
nowMs: Long,
fireMs: Long,
name: String,
/** Template localisé du cas ≥ 24 h (string `next_dose_days`, 4 args). */
daysTemplate: String,
zone: ZoneId = ZoneId.systemDefault(),
locale: Locale = Locale.getDefault()
): String {
val deltaMs = fireMs - nowMs
val h = deltaMs / HOUR_MS
val m = (deltaMs % HOUR_MS) / 60_000L
return if (h >= 24) {
val (days, remH) = com.hormonetrack.util.HrtDuration.daysAndHours(h)
val dayTimeStr = DateTimeFormatter.ofPattern("EEE d HH:mm")
.withZone(zone)
.withLocale(locale)
.format(Instant.ofEpochMilli(fireMs))
String.format(locale, daysTemplate, days, remH, dayTimeStr, name)
} else {
val timeStr = DateTimeFormatter.ofPattern("HH:mm")
.withZone(zone)
.withLocale(locale)
.format(Instant.ofEpochMilli(fireMs))
if (h > 0) {
String.format(locale, "%dh%02d · %s (%s)", h, m, timeStr, name)
} else {
String.format(locale, "%d min · %s (%s)", m, timeStr, name)
}
}
}
@Composable @Composable
private fun NextDoseCard(treatments: List<Treatment>, doseLogs: List<DoseLog>) { private fun NextDoseCard(treatments: List<Treatment>, doseLogs: List<DoseLog>) {
// v1.4.0 (fix #52) : les dates de rappel passent par le moteur — la carte // v1.4.0 (fix #52) : les dates de rappel passent par le moteur — la carte
// suit la grille Posologie (jour du créneau uniquement) comme les alarmes. // suit la grille Posologie (jour du créneau uniquement) comme les alarmes.
// v1.9.6 (demande) : la carte liste TOUTES les prochaines prises, UNE
// ligne par traitement, TRIÉES par prochaine prise — l'ancienne carte ne
// montrait que la plus proche, et une prise quotidienne (CPA) masquait
// totalement le rappel de l'injection hebdomadaire (le rappel EEn
// « n'arrivait qu'après celui de la CPA »). Le titre passe au pluriel
// dès qu'il y a au moins deux lignes.
val nowMs = System.currentTimeMillis() val nowMs = System.currentTimeMillis()
val fires = treatments val fires = treatments
.mapNotNull { tr -> PharmacokineticEngine.nextReminderFireFor(tr, doseLogs, nowMs)?.let { tr to it } } .mapNotNull { tr -> PharmacokineticEngine.nextReminderFireFor(tr, doseLogs, nowMs)?.let { tr to it } }
.sortedBy { it.second }
if (fires.isEmpty()) return if (fires.isEmpty()) return
val (nextTreatment, next) = fires.minByOrNull { it.second }!!
Card(Modifier.fillMaxWidth()) { Card(Modifier.fillMaxWidth()) {
Column(Modifier.padding(16.dp)) { Column(Modifier.padding(16.dp)) {
Text( Text(
stringResource(R.string.next_dose), stringResource(if (fires.size == 1) R.string.next_dose else R.string.next_doses),
style = MaterialTheme.typography.labelMedium style = MaterialTheme.typography.labelMedium
) )
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
val deltaMs = next - System.currentTimeMillis() // Première ligne (la plus proche) mise en avant, les suivantes en
val h = deltaMs / HOUR_MS // style secondaire — même hiérarchie visuelle que v1.4.x.
val m = (deltaMs % HOUR_MS) / 60_000L fires.forEachIndexed { idx, (treatment, next) ->
val name = nextTreatment?.name ?: ""
// v1.4.1 : au-delà de 24 h, afficher en JOURS (les heures
// deviennent peu lisibles) + le jour de la semaine dans la date.
if (h >= 24) {
val (days, remH) = com.hormonetrack.util.HrtDuration.daysAndHours(h)
val dayTimeStr = DateTimeFormatter.ofPattern("EEE d HH:mm")
.withZone(ZoneId.systemDefault())
.format(Instant.ofEpochMilli(next))
Text( Text(
stringResource(R.string.next_dose_days, days, remH, dayTimeStr, name), nextDoseLine(
style = MaterialTheme.typography.titleMedium nowMs, next, treatment?.name ?: "",
) stringResource(R.string.next_dose_days)
} else { ),
val timeStr = DateTimeFormatter.ofPattern("HH:mm") style = if (idx == 0) MaterialTheme.typography.titleMedium
.withZone(ZoneId.systemDefault()) else MaterialTheme.typography.bodyMedium
.format(Instant.ofEpochMilli(next))
Text(
if (h > 0) {
String.format(Locale.getDefault(), "%dh%02d · %s (%s)", h, m, timeStr, name)
} else {
String.format(Locale.getDefault(), "%d min · %s (%s)", m, timeStr, name)
},
style = MaterialTheme.typography.titleMedium
) )
} }
} }

View File

@ -17,6 +17,7 @@
<string name="no_data">Pas encore de données — crée un traitement et logue une dose.</string> <string name="no_data">Pas encore de données — crée un traitement et logue une dose.</string>
<string name="logged_today">Log rapide</string> <string name="logged_today">Log rapide</string>
<string name="next_dose">Prochaine dose</string> <string name="next_dose">Prochaine dose</string>
<string name="next_doses">Prochaines doses</string>
<string name="next_dose_days">%1$d j %2$d h · %3$s (%4$s)</string> <string name="next_dose_days">%1$d j %2$d h · %3$s (%4$s)</string>
<string name="delta_6h">%1$s %2$s pg/mL vs il y a 6 h</string> <string name="delta_6h">%1$s %2$s pg/mL vs il y a 6 h</string>
<string name="home_chart_title">Dernières 24 h (estimation)</string> <string name="home_chart_title">Dernières 24 h (estimation)</string>

View File

@ -17,6 +17,7 @@
<string name="no_data">No data yet — add a treatment and log a dose.</string> <string name="no_data">No data yet — add a treatment and log a dose.</string>
<string name="logged_today">Quick log</string> <string name="logged_today">Quick log</string>
<string name="next_dose">Next dose</string> <string name="next_dose">Next dose</string>
<string name="next_doses">Upcoming doses</string>
<string name="next_dose_days">%1$d d %2$d h · %3$s (%4$s)</string> <string name="next_dose_days">%1$d d %2$d h · %3$s (%4$s)</string>
<string name="delta_6h">%1$s %2$s pg/mL vs 6 h ago</string> <string name="delta_6h">%1$s %2$s pg/mL vs 6 h ago</string>
<string name="home_chart_title">Last 24 hours (estimate)</string> <string name="home_chart_title">Last 24 hours (estimate)</string>

View File

@ -0,0 +1,45 @@
package com.hormonetrack.ui
import com.hormonetrack.ui.screens.nextDoseLine
import org.junit.Assert.assertEquals
import org.junit.Test
import java.time.ZoneId
import java.time.ZonedDateTime
import java.util.Locale
/**
* Tests du helper PUR `nextDoseLine` (v1.9.6 — carte « Prochaines doses » de
* l'Accueil, une ligne par traitement triée par prochaine prise). Zone et
* locale FIXES pour un format déterministe.
*/
class NextDoseLineTest {
private val zone = ZoneId.of("Europe/Paris")
private val locale = Locale.US
// Template du cas ≥ 24 h (string EN `next_dose_days`)
private val daysTemplate = "%1\$d d %2\$d h · %3\$s (%4\$s)"
private fun at(hoursFromNow: Long): Long =
ZonedDateTime.of(2026, 9, 21, 12, 0, 0, 0, zone).toInstant().toEpochMilli() + hoursFromNow * 3_600_000L
@Test
fun `au-dela de 24 h - jours + jour de semaine`() {
// now = lundi 21/09 12:00 → fire = samedi 26/09 10:00 (118 h)
val line = nextDoseLine(at(0), at(118), "Injection EEn 5mg 7d", daysTemplate, zone, locale)
assertEquals("4 d 22 h · Sat 26 10:00 (Injection EEn 5mg 7d)", line)
}
@Test
fun `entre 1 h et 24 h - heures et minutes`() {
// now = 12:00 → fire = 14:05 (2 h 05)
val line = nextDoseLine(at(0), at(2) + 5 * 60_000L, "CPA", daysTemplate, zone, locale)
assertEquals("2h05 · 14:05 (CPA)", line)
}
@Test
fun `moins d'une heure - minutes`() {
// now = 12:00 → fire = 12:07
val line = nextDoseLine(at(0), at(0) + 7 * 60_000L, "CPA", daysTemplate, zone, locale)
assertEquals("7 min · 12:07 (CPA)", line)
}
}

View File

@ -3,6 +3,27 @@
Format : [Keep a Changelog](https://keepachangelog.com/fr-FR/1.1.0/). Format : [Keep a Changelog](https://keepachangelog.com/fr-FR/1.1.0/).
Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.4.10`, …). Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.4.10`, …).
## [1.9.6] — 2026-09-20 (versionCode 43)
### Modifié — la carte de l'Accueil liste TOUTES les prochaines prises
- **Remontée** : « si une injection et une prise d'un autre médicament ont
lieu en même temps… le rappel ne montre que la prise qui vient, non
toutes les prises du jour » — avec une prise quotidienne (CPA), le
rappel de l'injection hebdomadaire (EEn) « n'arrive qu'après celui de la
CPA, du coup je ne le vois quasiment pas ».
- **Fix** : la carte « Prochaine dose » devient « **Prochaines doses** »
(titre pluriel dès 2 lignes) — **une ligne par traitement, TRIÉES par
prochaine prise** ; la 1ʳᵉ ligne (la plus proche) reste mise en avant.
Format de ligne inchangé (`Xj Yh · ven. 25 10:00 (nom)`, `XhMM · HH:mm`,
`N min`). Le CPA se lit désormais AU-DESSUS de l'EEn du samedi, jamais à
sa place.
- Les NOTIFICATIONS de rappel sont inchangées (une alarme par traitement
— le problème était purement l'affichage de l'Accueil).
- Moteur : helper PUR `nextDoseLine` (HomeScreen.kt, locale/zone
paramétrables, template jours localisé) — 3 tests (NextDoseLineTest).
247 tests verts + lint.
## [1.9.5] — 2026-09-20 (versionCode 42) ## [1.9.5] — 2026-09-20 (versionCode 42)
### Corrigé — revue de code post-v1.9.4 (audit lecture-seule des deux dépôts) ### Corrigé — revue de code post-v1.9.4 (audit lecture-seule des deux dépôts)

View File

@ -63,6 +63,7 @@ Fonctionnalités v1 :
| Date | Événement | | Date | Événement |
|---|---| |---|---|
| 20 sept. 2026 (session v1.9.6) | **Carte Accueil « Prochaines doses » — toutes les prises, triées** (remontée : le rappel quotidien CPA masquait le rappel hebdomadaire EEn — « je ne le vois quasiment pas ») : la carte liste UNE ligne par traitement TRIÉES par prochaine prise (1ʳᵉ en avant, suivantes en style secondaire), titre pluriel dès 2 lignes (`next_doses` FR/EN) ; helper PUR `nextDoseLine` (locale/zone paramétrables, template jours localisé `next_dose_days`) — 3 tests `NextDoseLineTest`. Notifications inchangées (une alarme par traitement). Web miroir v1.9.6 : helper pur `nextDoseLineText` (format.js) + même carte + 3 tests miroirs (misc) — 247 verts Android / 178 web. |
| 20 sept. 2026 (session v1.9.5) | **Revue de code post-v1.9.4 — audit lecture-seule des deux dépôts puis corrections** (demande : « les logiques, choix techniques, commentaires, optimisations ») : (a) **#67** : ticks X alignés sur le fuseau APPAREIL pendant que les labels étaient formatés dans le fuseau CHOISI (zone manquante à `xLabelTicks`, contredisant #55) + pas fixe `+24 h` dérivant d'1 h aux transitions DST → zone passée + avancement Calendar, test DST (Europe/Paris 29/03) ; (b) `EstrannaiseCloud.compute` : `nowMs` mort supprimé (KDoc mensonger) ; (c) **perf** : `E2Context`/`prepareE2Context` — le tracé labs (720 pts) reconstruisait les maps moteur par point, diluant v1.8.2 (résultat bit-identique) ; Paints texte + `detectExtrema` + formatters mémoïsés dans le Canvas ; (d) **règles ré-appliquées** : `modelLabelRes` (anti-#59), `PKModels.*` partout (fini les literals), `scheduler2` supprimé ; (e) **doc rattrapée** : 3 commentaires périmés (ODS/lookup/t½-table), §7.2/§7.3 (asset historique, cutoff 10×t½). **Web miroir v1.9.5** : garde `hasModel` ESE dans `cutoffHours` (un backup importé ESE+ester non couvert levait TypeError → écran vide), portage des optimisations v1.8.2 (contexte préparé, cutoffs par traitement, Bateman paresseux), min/max en une passe (RangeError possible sur ~230 k pts avec le Nuage), init MCMC non bloquante (data-ready ⇒ asset chargé), `pk_profiles.json` exclu du zip (export-ignore, −548 Ko livrés), E2E version paramétrée (`data-version` — anti-bug v1.9.3), nettoyages (import mort lab-timing, JSDoc « troncature », dialogs ESE=3, COLORS accueil, JSDoc orpheline). 244 verts Android + lint · 175 verts web + E2E ✓. | | 20 sept. 2026 (session v1.9.5) | **Revue de code post-v1.9.4 — audit lecture-seule des deux dépôts puis corrections** (demande : « les logiques, choix techniques, commentaires, optimisations ») : (a) **#67** : ticks X alignés sur le fuseau APPAREIL pendant que les labels étaient formatés dans le fuseau CHOISI (zone manquante à `xLabelTicks`, contredisant #55) + pas fixe `+24 h` dérivant d'1 h aux transitions DST → zone passée + avancement Calendar, test DST (Europe/Paris 29/03) ; (b) `EstrannaiseCloud.compute` : `nowMs` mort supprimé (KDoc mensonger) ; (c) **perf** : `E2Context`/`prepareE2Context` — le tracé labs (720 pts) reconstruisait les maps moteur par point, diluant v1.8.2 (résultat bit-identique) ; Paints texte + `detectExtrema` + formatters mémoïsés dans le Canvas ; (d) **règles ré-appliquées** : `modelLabelRes` (anti-#59), `PKModels.*` partout (fini les literals), `scheduler2` supprimé ; (e) **doc rattrapée** : 3 commentaires périmés (ODS/lookup/t½-table), §7.2/§7.3 (asset historique, cutoff 10×t½). **Web miroir v1.9.5** : garde `hasModel` ESE dans `cutoffHours` (un backup importé ESE+ester non couvert levait TypeError → écran vide), portage des optimisations v1.8.2 (contexte préparé, cutoffs par traitement, Bateman paresseux), min/max en une passe (RangeError possible sur ~230 k pts avec le Nuage), init MCMC non bloquante (data-ready ⇒ asset chargé), `pk_profiles.json` exclu du zip (export-ignore, −548 Ko livrés), E2E version paramétrée (`data-version` — anti-bug v1.9.3), nettoyages (import mort lab-timing, JSDoc « troncature », dialogs ESE=3, COLORS accueil, JSDoc orpheline). 244 verts Android + lint · 175 verts web + E2E ✓. |
| 19 sept. 2026 (session v1.9.4) | **Presets du modèle ESE étendus à ses 6 esters injectables** (remontée : « depuis l'ajout du modèle source ESE, les presets n'ont pas été mis à jour ») : 3 presets ajoutés (EC huile 5 mg IM, EB 1 mg IM, EUCS suspension cristalline 10 mg SC — 25 entrées), champs Bateman informatifs = t½ des fits ; **éditeur** : la liste des esters suit `Esters.choicesForModel` (parité DoseDialog/web) et rebascule sur EV si l'ester sort de la couverture au changement de modèle ; **web** : 2 trous du portage v1.9.0 corrigés (`Esters.EUCS` manquant dans pk-engine.js — preset référençant `undefined` — et `choicesForModel` resté sur la couverture ODS à 3 esters) + `tests/presets.test.js` (6 tests, miroir `PKPresetsTest`). 243 verts / 213 sans données locales + lint + check web (175). Doc : doublon §7.12 supprimé. Web v1.9.4 sync. | | 19 sept. 2026 (session v1.9.4) | **Presets du modèle ESE étendus à ses 6 esters injectables** (remontée : « depuis l'ajout du modèle source ESE, les presets n'ont pas été mis à jour ») : 3 presets ajoutés (EC huile 5 mg IM, EB 1 mg IM, EUCS suspension cristalline 10 mg SC — 25 entrées), champs Bateman informatifs = t½ des fits ; **éditeur** : la liste des esters suit `Esters.choicesForModel` (parité DoseDialog/web) et rebascule sur EV si l'ester sort de la couverture au changement de modèle ; **web** : 2 trous du portage v1.9.0 corrigés (`Esters.EUCS` manquant dans pk-engine.js — preset référençant `undefined` — et `choicesForModel` resté sur la couverture ODS à 3 esters) + `tests/presets.test.js` (6 tests, miroir `PKPresetsTest`). 243 verts / 213 sans données locales + lint + check web (175). Doc : doublon §7.12 supprimé. Web v1.9.4 sync. |
| 19 sept. 2026 (session v1.9.2) | **Fix nuage vide sans traitement stocké ESE** (remontée : « il ne s'active que autour du tracé émulé, pas autour du modèle Estrannaise ») : le filtre du nuage testait le pkModel STOCKÉ — or la courbe ESE redessine TOUTES les doses E2 (modelOverride ESE) quel que soit le modèle stocké. FIX : le nuage couvre les doses à ester effectif couvert par ESE (peu importe le stocké) ; l'exclusivité reste portée par le chip ; l'oral Bateman reste hors nuage. 2 tests réécrits (237 verts / 207 sans). Web miroir v1.9.2. | | 19 sept. 2026 (session v1.9.2) | **Fix nuage vide sans traitement stocké ESE** (remontée : « il ne s'active que autour du tracé émulé, pas autour du modèle Estrannaise ») : le filtre du nuage testait le pkModel STOCKÉ — or la courbe ESE redessine TOUTES les doses E2 (modelOverride ESE) quel que soit le modèle stocké. FIX : le nuage couvre les doses à ester effectif couvert par ESE (peu importe le stocké) ; l'exclusivité reste portée par le chip ; l'oral Bateman reste hors nuage. 2 tests réécrits (237 verts / 207 sans). Web miroir v1.9.2. |

View File

@ -105,7 +105,11 @@ Les notifications de l'app remontent automatiquement sur la montre via **Huawei
- Ester (EV / EU / EEn) + modèle (Estrannaise / Transfem Science) = les courbes du `.ods` - Ester (EV / EU / EEn) + modèle (Estrannaise / Transfem Science) = les courbes du `.ods`
- Pour gel/patch/oral : paramètres Bateman (temps au pic, demi-vie, biodispo) - Pour gel/patch/oral : paramètres Bateman (temps au pic, demi-vie, biodispo)
2. **Doses → +** → logue tes injections passées 2. **Doses → +** → logue tes injections passées
- L'accueil affiche la **prochaine dose en jours** quand elle est à plus de 24 h - L'accueil liste **toutes les prochaines doses** (v1.9.6) : une ligne
par traitement, triées par prochaine prise (la plus proche en premier,
en gras) — une prise quotidienne ne masque plus le rappel de
l'injection hebdomadaire. Affichage en **jours** quand la prochaine
prise est à plus de 24 h
(ex. « 5 j 2 h · sam. 6 18:00 »), en heures/minutes sinon (ex. « 5 j 2 h · sam. 6 18:00 »), en heures/minutes sinon
- La carte « Niveau actuel » compare ton taux estimé **à il y a 6 h** - La carte « Niveau actuel » compare ton taux estimé **à il y a 6 h**
(flèche ↗/↘) — autour d'un ester lent comme l'énanthate, cette variation (flèche ↗/↘) — autour d'un ester lent comme l'énanthate, cette variation