---
name: reading-plan-and-page-pace-planner
description: Planifie votre lecture jour par jour pour terminer chaque livre avant sa date de club de lecture, de cours ou d'examen - à partir du nombre de pages ou de mots (ou de la durée du livre audio), de votre vitesse de lecture, des minutes dont vous disposez chaque jour de semaine, des voyages et des jours chargés. Répartit les pages uniformément, garde des jours de marge avant chaque échéance et des jours de rattrapage hebdomadaires, s'arrête en fin de chapitre, avertit lorsqu'une échéance est hors de portée et replanifie à partir de votre position actuelle. Inclut un planificateur Python de la bibliothèque standard.
---
# Plan de lecture et rythme des pages
Vous aidez les lecteurs à arrêter de bachoter 200 pages la veille du club de lecture. Les gens savent à peu près combien de temps ils ont (une demi-heure en semaine, plus le week-end) mais pas ce que cela signifie en pages pour un roman de 245 pages à rendre dans deux semaines, un livre de travail dense et un classique de 880 pages dans le même mois. Vous transformez les livres, la vitesse de lecture et les minutes libres en un plan quotidien concret ("Mar : p. 1-27"), gardez de la marge pour la vie réelle, et dites tôt et clairement quand une échéance ne peut pas être tenue et ce qui la rendrait réalisable.
## Fichiers de cette compétence
- `scripts/reading_plan.py` - lit un JSON de plan (livres avec pages, mots ou minutes audio, page de départ, difficulté, échéance, débuts de chapitres ; vitesse de lecture en mots par minute ou pages par heure ; minutes par jour de semaine ; voyages et exceptions ; jours de rattrapage en semaine ; jours de marge) ; planifie par échéance la plus proche d'abord, répartit la charge uniformément (ou dès que possible), arrête les arrêts quotidiens en fin de chapitre, et imprime un résumé des livres, les constats RP01-RP06, un tableau jour par jour avec les plages de pages, des points de contrôle hebdomadaires et des conseils de rattrapage ; `--from` + `--at "Titre=page"` replanifie en cours de route (bibliothèque standard Python 3.8+ uniquement)
- `references/reading-speed-and-estimates.md` - mesurer votre vitesse, mots par page selon le type de livre, facteurs de difficulté, livres audio
- `references/pacing-and-catch-up.md` - marges, jours de rattrapage, que faire quand on prend du retard ou qu'une échéance est impossible
- `templates/reading-plan.md` - le plan à remettre (compatible calendrier)
- `examples/book-club.json` - novembre-décembre : un roman pour un club de lecture du jeudi, un livre de travail déjà commencé à la p. 23, Middlemarch pour un club de classiques, un livre audio sans échéance, des jours de rattrapage le dimanche, un voyage le week-end et deux jours inhabituels
- `examples/book-club-plan.md` - la sortie réelle du planificateur sur ce fichier
## Flux de travail
### 1. Collecter les entrées
Demandez : chaque livre avec son nombre de pages (ou son nombre de mots ; l'édition compte), où vous en êtes, l'échéance et ce que c'est (réunion, examen), si c'est facile ou dense ; votre vitesse de lecture (ou chronométrez un test de 10 minutes avec `references/reading-speed-and-estimates.md`) ; les minutes réalistes par jour de semaine, pas les minutes idéales ; les voyages connus, les fins de service tardives et les jours libres ; quel jour de semaine doit rester libre comme jour de rattrapage ; et combien de jours avant chaque réunion vous voulez avoir terminé (marge, défaut 1-2).
### 2. Exécuter le planificateur
```
python3 scripts/reading_plan.py examples/book-club.json
python3 scripts/reading_plan.py examples/book-club.json --mode asap
python3 scripts/reading_plan.py examples/book-club.json --from 2026-11-20 --at "Thinking in Systems=110"
python3 scripts/reading_plan.py plan.json --json
python3 scripts/reading_plan.py --self-test
```
Code de sortie 0 = chaque échéance est tenue, 1 = au moins un livre ne peut pas être terminé à temps, 2 = mauvaise entrée. Sans shell : minutes nécessaires = pages x mots par page / mots par minute x difficulté ; comparez avec les minutes disponibles jusqu'à chaque date de fin (échéance moins marge, jours de rattrapage exclus), échéance la plus proche d'abord.
### 3. Lire le résultat
- RP01 (élevé) : un livre ne rentre pas avant son échéance. Le message donne le manque en pages et les minutes supplémentaires par jour de lecture qui le combleraient. Quand le plan est irréalisable, le planificateur lit à plein rythme avant cela, donc les livres précédents se terminent plus tôt pour libérer du temps.
- RP02 / RP04 (moyen) : le plan ne fonctionne qu'en grignotant la marge, ou utilise plus de 90 % de votre temps dans sa période la plus chargée - une mauvaise semaine le casse.
- RP03 (faible) : des jours plus longs que la session maximale - divisez-les. RP05/RP06 (info) : livres terminés, échéances proches, livres audio.
- Vérifiez le tableau des jours pour les week-ends chargés et les points de contrôle hebdomadaires pour "où devrais-je être dimanche".
### 4. Corriger un plan impossible
Proposez des options dans cet ordre : commencez le livre long maintenant en parallèle (déplacez sa logique d'échéance plus tôt), ajoutez 10-20 minutes certains jours, utilisez les jours de rattrapage, passez une partie au livre audio pour les trajets, survolez ou sautez des sections clairement marquées, ou acceptez d'arriver à la réunion à moitié et dites-le. Relancez après chaque changement. Voir `references/pacing-and-catch-up.md`.
### 5. Livrer
Remplissez `templates/reading-plan.md` : résumé par livre, le plan semaine par semaine (dates et plages de pages, prêt pour un calendrier ou une note de marque-page), la règle de rattrapage, et la commande de replanification. Proposez une replanification rapide à chaque fois qu'ils signalent leur page actuelle.
## Exemple de résultat
Sur `examples/book-club.json` (240 mots/min, 20-60 min par jour, dimanches libres pour le rattrapage, 2 jours de marge) *The Remains of the Day* est terminé le 12 nov. pour le club de lecture du 19 nov., en s'arrêtant en fin de chapitre, et *Thinking in Systems* (dense, facteur 1.3, à partir de la p. 23) le 27 nov. pour le groupe du 1er déc. *Middlemarch* (316 000 mots, environ 22 heures) ne peut pas être terminé pour le club de classiques du 17 déc. : environ 452 pages manquantes, fin prévue le 11 janv. (RP01, élevé). Correction : environ +17 min sur chacun des 42 jours de lecture, ou commencez-le en parallèle avec les deux autres maintenant. Le samedi calme de 120 minutes est signalé comme deux sessions (RP03), et le livre audio Piranesi (6 h 40 à 1,25x) suit après Middlemarch.
## Règles
- Planifiez avec le temps que les gens ont vraiment, et gardez au moins un jour de marge avant chaque réunion.
- Ne remplissez jamais silencieusement les jours : si cela ne rentre pas, dites quelle échéance échoue, de combien de pages, et ce qui la rendrait réalisable.
- Utilisez le nombre de pages de l'édition ; quand seuls les nombres de mots sont connus, dites que les pages sont des estimations.
- Laissez les jours de rattrapage vides dans le plan - ils servent à rattraper le retard, pas à lire plus.
- La lecture doit rester agréable : suggérez de sauter ou de changer de format avant de suggérer des sessions de 2 heures chaque jour.
- Replanifiez à partir de la page actuelle chaque fois que le lecteur est en retard ou en avance ; ne les forcez pas à "rattraper" un ancien planning.
FILE:references/reading-speed-and-estimates.md
# Vitesse de lecture et estimations
## Mesurez votre vitesse (10 minutes)
1. Prenez le livre réel (ou un livre similaire), commencez au début d'un chapitre, réglez un minuteur de 10 minutes et lisez normalement.
2. Comptez les mots sur trois lignes complètes, divisez par 3 = mots par ligne ; comptez les lignes sur une page complète = lignes par page.
3. mots par page = mots par ligne x lignes par page ; mots par minute = pages lues x mots par page / 10.
Ou simplement chronométrez quelques pages et utilisez `"pages_per_hour"` au lieu de mots par minute.
## Valeurs typiques
| Lecteur / matériel | Mots par minute |
|---|---|
| Lecture attentive, non-fiction dense, deuxième langue | 150-200 |
| Adulte moyen, fiction | 200-260 |
| Lecteur rapide de loisir | 300-400 |
Survoler n'est pas lire : ne planifiez pas avec des vitesses supérieures à 450.
| Type de livre | Mots par page |
|---|---|
| Livre de poche grand public | 300-350 |
| Fiction de poche commerciale | 250-300 |
| Non-fiction reliée avec notes | 350-450 |
| Gros caractères, livres de chapitres pour enfants | 150-200 |
| Livre électronique | utilisez le nombre de mots (page de stockage ou outils qui l'affichent) et la valeur par défaut de 275 mots par page |
## Facteur de difficulté
`"difficulty"` multiplie le temps de lecture : 1.0 fiction facile, 1.2-1.4 non-fiction dense, classiques avec phrases longues, livres techniques avec schémas, 1.5+ manuels où l'on prend des notes, poésie, lecture en deuxième langue.
## Nombre de pages vs nombre de mots
- Utilisez le nombre de pages de l'édition que le lecteur possède ; les éditions du même livre peuvent différer de 30 %.
- Avec à la fois `"pages"` et `"words"`, le planificateur dérive les mots par page de cette édition (Middlemarch 316 000 / 880 = 359).
- Avec seulement des mots, les pages sont des estimations (mots / mots par page) ; dites-le dans le plan.
## Livres audio
- `"audio_minutes"` est la durée, `"speed"` la vitesse de lecture : temps nécessaire = durée / vitesse (400 min à 1,25x = 320 min).
- L'audio convient aux trajets, aux tâches ménagères et aux promenades : dites si ces minutes sont supplémentaires ou déjà comptées dans les minutes quotidiennes.
- Les positions sont affichées en h:mm de l'audio.
FILE:references/pacing-and-catch-up.md
# Rythme et rattrapage
## Comment le planificateur planifie
- Échéance la plus proche d'abord : le livre avec la date de fin la plus proche (échéance moins jours de marge) est lu en premier ; les livres sans échéance viennent en dernier.
- Mode uniforme (défaut) : pour chaque période jusqu'à l'échéance la plus serrée, il utilise la plus petite part égale de vos minutes quotidiennes qui termine encore à temps, donc des jours de 30 minutes peuvent devenir des jours de 20 minutes quand il y a de la place. Après cette échéance, la période suivante est recalculée.
- Mode ASAP : utilise toutes les minutes chaque jour ; les livres se terminent dès que possible.
- Si une échéance ne peut pas être tenue même à plein rythme, chaque jour avant elle est planifié à plein rythme et le livre est signalé RP01.
- Les arrêts quotidiens se déplacent en fin de chapitre quand un chapitre est à moins de `chapter_snap_pages` (défaut 8) et d'un quart des pages du jour.
## Marges et jours de rattrapage
- Les jours de marge (`buffer_days`, défaut 1) se situent entre la fin planifiée et la réunion : du temps pour relire des notes ou absorber une mauvaise semaine.
- Les jours de rattrapage en semaine (`catch_up_days`, ex. dimanche) n'ont pas de lecture planifiée. Utilisez-les seulement si vous êtes en retard ; si vous êtes dans les temps, c'est du temps libre.
- Les exceptions (voyages, fins de service tardives, un samedi calme) remplacent les minutes du jour de semaine pour ces dates.
## Quand vous prenez du retard
1. Un jour manqué : lisez les pages de ce jour le prochain jour de rattrapage, ou ajoutez un tiers d'entre elles à chacun des trois jours suivants.
2. Retard de plus de deux jours : replanifiez à partir d'aujourd'hui avec `--from <date> --at "<Titre>=<page actuelle>"` au lieu de courir après l'ancien plan.
3. Toujours pas à temps (RP01) : choisissez une option - commencez le livre long en parallèle maintenant, ajoutez 10-20 minutes sur quelques jours, utilisez le livre audio pour les trajets, survolez des sections marquées (préfaces, annexes, notes), ou décidez d'arriver à moitié et prévenez le groupe.
## Signes que le plan est trop serré
- RP04 : plus de 90 % de votre temps utilisé sur une période ; toute maladie ou mauvaise semaine le casse.
- RP02 : le livre ne rentre qu'en utilisant les jours de marge.
- Beaucoup de jours marqués "2 sessions" (RP03) : envisagez de répartir la lecture entre matin et soir délibérément.
## Conseils pour les clubs de lecture
- Notez la page ou le chapitre que le groupe discutera si ce n'est pas tout le livre, et utilisez-le comme `pages` du livre.
- Pour les longs classiques, demandez au club si la réunion couvre la première moitié ; planifiez deux échéances.
- Gardez un pense-bête dans le livre avec les points de contrôle hebdomadaires du plan.
FILE:templates/reading-plan.md
# Plan de lecture : <nom> (<début> à <dernière échéance>)
**Rythme :** <mots/min> mots/min, environ <pages> pages dans une session de 30 minutes
**Temps :** <minutes par jour de semaine> ; jour(s) de rattrapage : <jour> ; marge : <n> jour(s) avant chaque réunion
**Verdict :** <chaque échéance est tenue | LIVRE ne peut pas être terminé avant DATE - voir options>
## Livres
| Livre | Pages restantes | Temps nécessaire | Réunion / échéance | Fin prévue | Fin planifiée |
|---|---|---|---|---|---|
| <titre> | <pp> | <h:mm> | <date - événement> | <date> | <date> |
## Semaine par semaine
### Semaine du <lundi date>
- Lun : <Livre p. a-b>
- Mar : <...>
- Dim : rattrapage / libre
**Point de contrôle dimanche soir :** <Livre p. X>
## Si une échéance ne rentre pas
- Manque : <pages> pages de <livre> avant <date>
- Options : <+N min sur les jours de lecture | commencer maintenant en parallèle | livre audio pour les trajets | survoler les chapitres x-y | arriver à moitié>
- Choisi : <...>
## En cas de retard
- Un jour manqué ? Utilisez <jour de rattrapage> ou ajoutez <n> pages aux trois prochains jours.
- Plus de deux jours de retard : replanifiez à partir d'aujourd'hui (`--from <date> --at "<titre>=<page>"`).
FILE:examples/book-club.json
{
"name": "Plan de lecture : nov-déc 2026 (deux clubs de lecture et un livre de travail)",
"start": "2026-11-03",
"reader": {"words_per_minute": 240, "words_per_page": 280},
"minutes": {"mon": 30, "tue": 30, "wed": 20, "thu": 30, "fri": 25, "sat": 60, "sun": 45},
"catch_up_days": ["sun"],
"buffer_days": 2,
"max_session_minutes": 60,
"chapter_snap_pages": 8,
"exceptions": [
{"dates": "2026-11-13..2026-11-15", "minutes": 0, "note": "voyage le week-end"},
{"dates": "2026-11-26", "minutes": 15, "note": "fin de service tardive"},
{"dates": "2026-12-05", "minutes": 120, "note": "samedi calme"}
],
"books": [
{"title": "The Remains of the Day", "pages": 245, "words_per_page": 265, "deadline": "2026-11-19", "event": "club de lecture du jeudi",
"chapters": [1, 9, 45, 101, 125, 165, 212, 239]},
{"title": "Thinking in Systems", "pages": 218, "start_page": 23, "difficulty": 1.3, "deadline": "2026-12-01", "event": "groupe de lecture d'équipe"},
{"title": "Middlemarch", "words": 316000, "pages": 880, "deadline": "2026-12-17", "event": "club de classiques"},
{"title": "Piranesi (livre audio)", "audio_minutes": 400, "speed": 1.25}
]
}
FILE:examples/book-club-plan.md
# Plan de lecture : nov-déc 2026 (deux clubs de lecture et un livre de travail)
Début 2026-11-03, mode uniforme, 2 jour(s) de marge avant chaque échéance, jours de rattrapage : dim. Lecture planifiée : 36 h 44 min sur 81 jours.
**Verdict : 1 livre(s) ne peut/peuvent pas être terminé(s) avant leur échéance - changez le plan**
## Livres
| Livre | Restant | Temps est. | Échéance | Fin prévue | Fin planifiée | Jours de marge | Moyenne par jour de lecture |
|---|---|---|---|---|---|---|---|
| The Remains of the Day | 245 pp | 4:31 h | 2026-11-19 (club de lecture du jeudi) | 2026-11-17 | 2026-11-12 | 7 | 27 pp |
| Thinking in Systems | 196 pp | 4:57 h | 2026-12-01 (groupe de lecture d'équipe) | 2026-11-29 | 2026-11-27 | 4 | 16 pp |
| Middlemarch | 880 pp | 21:57 h | 2026-12-17 (club de classiques) | 2026-12-15 | 2027-01-11 | -25 | 23 pp |
| Piranesi (livre audio) | 6:40 audio | 5:20 h | - | - | 2027-01-22 | - | 0:36 |
## Constats
- **ÉLEVÉ RP01** Middlemarch ne peut pas être terminé avant 2026-12-17 (club de classiques) : environ 452 pages manquantes à votre rythme ; fin prévue 2027-01-11. Options : +17 min sur chacun des 42 jours de lecture jusqu'à cette date, lire plus vite, survoler ou sauter environ 452 pages, ou commencer plus tôt
- **FAIBLE RP03** 1 jour(s) planifie(nt) plus de 60 minutes (ex. 2026-12-05 : 120 min) - divisez en deux sessions
- **INFO RP06** Piranesi (livre audio) est un livre audio : le plan compte le temps d'écoute à 1,25x ; les trajets et les tâches ménagères peuvent le couvrir
## Jour par jour
| Date | Jour | Minutes | Lecture | Note |
|---|---|---|---|---|
| 2026-11-03 | Mar | 30 | The Remains of the Day p. 1-27 (27 pp) | |
| 2026-11-04 | Mer | 19 | The Remains of the Day p. 28-44 (17 pp) | |
| 2026-11-05 | Jeu | 30 | The Remains of the Day p. 45-71 (27 pp) | |
| 2026-11-06 | Ven | 25 | The Remains of the Day p. 72-94 (23 pp) | |
| 2026-11-07 | Sam | 60 | The Remains of the Day p. 95-148 (54 pp) | |
| 2026-11-08 | Dim | - | - | rattrapage / libre (45 min disponibles) |
| 2026-11-09 | Lun | 30 | The Remains of the Day p. 149-175 (27 pp) | |
| 2026-11-10 | Mar | 30 | The Remains of the Day p. 176-202 (27 pp) | |
| 2026-11-11 | Mer | 20 | The Remains of the Day p. 203-221 (19 pp) | |
| 2026-11-12 | Jeu | 30 | The Remains of the Day p. 222-245 (24 pp) ; Thinking in Systems p. 23-24 (2 pp) | |
| 2026-11-13 | Ven | - | - | voyage le week-end |
| 2026-11-14 | Sam | - | - | voyage le week-end |
| 2026-11-15 | Dim | - | - | voyage le week-end |
| 2026-11-16 | Lun | 30 | Thinking in Systems p. 25-44 (20 pp) | |
| 2026-11-17 | Mar | 30 | Thinking in Systems p. 45-64 (20 pp) | fin prévue The Remains of the Day |
| 2026-11-18 | Mer | 20 | Thinking in Systems p. 65-77 (13 pp) | |
| 2026-11-19 | Jeu | 30 | Thinking in Systems p. 78-97 (20 pp) | ÉCHÉANCE The Remains of the Day - club de lecture du jeudi |
| 2026-11-20 | Ven | 25 | Thinking in Systems p. 98-113 (16 pp) | |
| 2026-11-21 | Sam | 60 | Thinking in Systems p. 114-153 (40 pp) | |
| 2026-11-22 | Dim | - | - | rattrapage / libre (45 min disponibles) |
| 2026-11-23 | Lun | 30 | Thinking in Systems p. 154-172 (19 pp) | |
| 2026-11-24 | Mar | 30 | Thinking in Systems p. 173-192 (20 pp) | |
| 2026-11-25 | Mer | 20 | Thinking in Systems p. 193-205 (13 pp) | |
| 2026-11-26 | Jeu | 15 | Thinking in Systems p. 206-215 (10 pp) | fin de service tardive |
| 2026-11-27 | Ven | 25 | Thinking in Systems p. 216-218 (3 pp) ; Middlemarch p. 1-14 (14 pp) | |
| 2026-11-28 | Sam | 60 | Middlemarch p. 15-54 (40 pp) | |
| 2026-11-29 | Dim | - | - | rattrapage / libre (45 min disponibles) ; fin prévue Thinking in Systems |
| 2026-11-30 | Lun | 30 | Middlemarch p. 55-74 (20 pp) | |
| 2026-12-01 | Mar | 30 | Middlemarch p. 75-94 (20 pp) | ÉCHÉANCE Thinking in Systems - groupe de lecture d'équipe |
| 2026-12-02 | Mer | 20 | Middlemarch p. 95-107 (13 pp) | |
| 2026-12-03 | Jeu | 30 | Middlemarch p. 108-128 (21 pp) | |
| 2026-12-04 | Ven | 25 | Middlemarch p. 129-144 (16 pp) | |
| 2026-12-05 | Sam | 120 | Middlemarch p. 145-224 (80 pp) | samedi calme ; 2 sessions |
| 2026-12-06 | Dim | - | - | rattrapage / libre (45 min disponibles) |
| 2026-12-07 | Lun | 30 | Middlemarch p. 225-244 (20 pp) | |
| 2026-12-08 | Mar | 30 | Middlemarch p. 245-265 (21 pp) | |
| 2026-12-09 | Mer | 20 | Middlemarch p. 266-278 (13 pp) | |
| 2026-12-10 | Jeu | 30 | Middlemarch p. 279-298 (20 pp) | |
| 2026-12-11 | Ven | 25 | Middlemarch p. 299-315 (17 pp) | |
| 2026-12-12 | Sam | 60 | Middlemarch p. 316-355 (40 pp) | |
| 2026-12-13 | Dim | - | - | rattrapage / libre (45 min disponibles) |
| 2026-12-14 | Lun | 30 | Middlemarch p. 356-375 (20 pp) | |
| 2026-12-15 | Mar | 30 | Middlemarch p. 376-395 (20 pp) | fin prévue Middlemarch |
| 2026-12-16 | Mer | 20 | Middlemarch p. 396-408 (13 pp) | |
| 2026-12-17 | Jeu | 30 | Middlemarch p. 409-428 (20 pp) | ÉCHÉANCE Middlemarch - club de classiques |
| 2026-12-18 | Ven | 25 | Middlemarch p. 429-445 (17 pp) | |
| 2026-12-19 | Sam | 60 | Middlemarch p. 446-485 (40 pp) | |
| 2026-12-20 | Dim | - | - | rattrapage / libre (45 min disponibles) |
| 2026-12-21 | Lun | 30 | Middlemarch p. 486-505 (20 pp) | |
| 2026-12-22 | Mar | 30 | Middlemarch p. 506-525 (20 pp) | |
| 2026-12-23 | Mer | 20 | Middlemarch p. 526-539 (14 pp) | |
| 2026-12-24 | Jeu | 30 | Middlemarch p. 540-559 (20 pp) | |
| 2026-12-25 | Ven | 25 | Middlemarch p. 560-575 (16 pp) | |
| 2026-12-26 | Sam | 60 | Middlemarch p. 576-615 (40 pp) | |
| 2026-12-27 | Dim | - | - | rattrapage / libre (45 min disponibles) |
| 2026-12-28 | Lun | 30 | Middlemarch p. 616-635 (20 pp) | |
| 2026-12-29 | Mar | 30 | Middlemarch p. 636-656 (21 pp) | |
| 2026-12-30 | Mer | 20 | Middlemarch p. 657-669 (13 pp) | |
| 2026-12-31 | Jeu | 30 | Middlemarch p. 670-689 (20 pp) | |
| 2027-01-01 | Ven | 25 | Middlemarch p. 690-706 (17 pp) | |
| 2027-01-02 | Sam | 60 | Middlemarch p. 707-746 (40 pp) | |
| 2027-01-03 | Dim | - | - | rattrapage / libre (45 min disponibles) |
| 2027-01-04 | Lun | 30 | Middlemarch p. 747-766 (20 pp) | |
| 2027-01-05 | Mar | 30 | Middlemarch p. 767-786 (20 pp) | |
| 2027-01-06 | Mer | 20 | Middlemarch p. 787-799 (13 pp) | |
| 2027-01-07 | Jeu | 30 | Middlemarch p. 800-819 (20 pp) | |
| 2027-01-08 | Ven | 25 | Middlemarch p. 820-836 (17 pp) | |
| 2027-01-09 | Sam | 60 | Middlemarch p. 837-876 (40 pp) | |
| 2027-01-10 | Dim | - | - | rattrapage / libre (45 min disponibles) |
| 2027-01-11 | Lun | 30 | Middlemarch p. 877-880 (4 pp) ; Piranesi (livre audio) 0:00-0:30 | |
| 2027-01-12 | Mar | 30 | Piranesi (livre audio) 0:30-1:08 | |
| 2027-01-13 | Mer | 20 | Piranesi (livre audio) 1:08-1:33 | |
| 2027-01-14 | Jeu | 30 | Piranesi (livre audio) 1:33-2:10 | |
| 2027-01-15 | Ven | 25 | Piranesi (livre audio) 2:10-2:41 | |
| 2027-01-16 | Sam | 60 | Piranesi (livre audio) 2:41-3:56 | |
| 2027-01-17 | Dim | - | - | rattrapage / libre (45 min disponibles) |
| 2027-01-18 | Lun | 30 | Piranesi (livre audio) 3:56-4:34 | |
| 2027-01-19 | Mar | 30 | Piranesi (livre audio) 4:34-5:11 | |
| 2027-01-20 | Mer | 20 | Piranesi (livre audio) 5:11-5:36 | |
| 2027-01-21 | Jeu | 30 | Piranesi (livre audio) 5:36-6:14 | |
| 2027-01-22 | Ven | 21 | Piranesi (livre audio) 6:14-6:40 | |
## Points de contrôle hebdomadaires (où vous devriez être dimanche soir)
- 2026-11-08 : The Remains of the Day p. 148
- 2026-11-15 : Thinking in Systems p. 24
- 2026-11-22 : Thinking in Systems p. 153
- 2026-11-29 : Middlemarch p. 54
- 2026-12-06 : Middlemarch p. 224
- 2026-12-13 : Middlemarch p. 355
- 2026-12-20 : Middlemarch p. 485
- 2026-12-27 : Middlemarch p. 615
- 2027-01-03 : Middlemarch p. 746
- 2027-01-10 : Middlemarch p. 876
- 2027-01-17 : Piranesi (livre audio) 3:56
## Si vous prenez du retard
- The Remains of the Day : un jour manqué coûte environ 27 pages. Rattrapez-les le(s) jour(s) libre(s) 2026-11-08, ou ajoutez 10 pages à chacun des 3 prochains jours de lecture. Replanifiez à tout moment avec `--from <date> --at "The Remains of the Day=<page>"`.
- Thinking in Systems : un jour manqué coûte environ 16 pages. Rattrapez-les le(s) jour(s) libre(s) 2026-11-22, 2026-11-29, ou ajoutez 6 pages à chacun des 3 prochains jours de lecture. Replanifiez à tout moment avec `--from <date> --at "Thinking in Systems=<page>"`.
- Middlemarch : un jour manqué coûte environ 23 pages. Rattrapez-les le(s) jour(s) libre(s) 2026-11-29, 2026-12-06, 2026-12-13, ou ajoutez 8 pages à chacun des 3 prochains jours de lecture. Replanifiez à tout moment avec `--from <date> --at "Middlemarch=<page>"`.
FILE:scripts/reading_plan.py
#!/usr/bin/env python3
"""Planificateur de plan de lecture et de rythme des pages (bibliothèque standard uniquement, Python 3.8+).
Transforme une liste de livres (pages, nombres de mots ou durée de livre audio), votre vitesse de lecture et les
minutes dont vous disposez chaque jour en un plan jour par jour avec des plages de pages qui respecte chaque échéance
(club de lecture, cours, examen), garde des jours de marge avant chaque échéance et des jours de rattrapage dans la
semaine, répartit la charge uniformément et vous dit quoi faire si vous prenez du retard.
Utilisation :
reading_plan.py PLAN.json [--mode even|asap] [--from AAAA-MM-JJ] [--at "Titre=page"]... [--json]
reading_plan.py --self-test
Code de sortie : 0 = chaque échéance est tenue, 1 = au moins un livre ne peut pas être terminé avant son échéance,
2 = mauvaise entrée.
"""
import argparse, datetime, json, math, re, sys
DAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
SEV = {"high": 0, "medium": 1, "low": 2, "info": 3}
class InputError(Exception):
pass
def pdate(s, what):
try:
return datetime.date.fromisoformat(str(s))
except ValueError:
raise InputError("%s doit être AAAA-MM-JJ, reçu %r" % (what, s))
def expand_dates(spec):
spec = str(spec)
if ".." in spec:
a, b = spec.split("..", 1)
a, b = pdate(a.strip(), "plage de dates"), pdate(b.strip(), "plage de dates")
if b < a:
raise InputError("la plage de dates %s se termine avant de commencer" % spec)
return [a + datetime.timedelta(days=i) for i in range((b - a).days + 1)]
return [pdate(spec.strip(), "date")]
# ---------------------------------------------------------------- modèle
def load(cfg, start_override=None, at=None):
if not isinstance(cfg, dict) or not isinstance(cfg.get("books"), list) or not cfg["books"]:
raise InputError("le plan a besoin d'une liste 'books' non vide")
reader = cfg.get("reader") or {}
wpm = float(reader.get("words_per_minute", 230))
wpp_default = float(reader.get("words_per_page", 275))
pph = reader.get("pages_per_hour")
if wpm <= 0 or wpp_default <= 0:
raise InputError("words_per_minute et words_per_page doivent être positifs")
start = pdate(start_override or cfg.get("start") or datetime.date.today().isoformat(), "start")
mins = cfg.get("minutes")
if isinstance(mins, (int, float)):
mins = {d: mins for d in DAYS}
if not isinstance(mins, dict):
raise InputError("'minutes' doit être un nombre ou {mon: .., tue: ..}")
mins = {k.lower()[:3]: float(v) for k, v in mins.items()}
bad = set(mins) - set(DAYS)
if bad:
raise InputError("clé(s) de jour de semaine inconnue(s) dans minutes : %s" % ", ".join(sorted(bad)))
catch = {d.lower()[:3] for d in cfg.get("catch_up_days", [])}
exc = {}
for e in cfg.get("exceptions", []):
for d in expand_dates(e.get("dates") or e.get("date")):
exc[d] = (float(e.get("minutes", 0)), e.get("note", ""))
books = []
at = at or {}
for i, b in enumerate(cfg["books"], 1):
t = b.get("title") or "Livre %d" % i
if "audio_minutes" in b:
speed = float(b.get("speed", 1.0))
total = float(b["audio_minutes"])
unit_min = 1.0 / speed # minutes de votre temps par minute d'audio
units, kind = total, "audio"
done = float(at.get(t, b.get("start_minute", 0)))
else:
wpp = float(b.get("words_per_page") or (float(b["words"]) / float(b["pages"]) if b.get("words") and b.get("pages") else wpp_default))
pages = float(b["pages"]) if b.get("pages") else (float(b["words"]) / wpp if b.get("words") else None)
if not pages or pages <= 0:
raise InputError("le livre %r a besoin de pages, de mots ou d'audio_minutes" % t)
diff = float(b.get("difficulty", 1.0))
if pph and not b.get("words"):
unit_min = 60.0 / float(pph) * diff
else:
unit_min = wpp / wpm * diff
units, kind = round(pages), "pages"
sp = at.get(t, b.get("start_page", 1))
done = max(0.0, float(sp) - 1)
dl = pdate(b["deadline"], "échéance de %r" % t) if b.get("deadline") else None
chapters = sorted(int(c) for c in b.get("chapters", []))
books.append({"title": t, "units": float(units), "kind": kind, "unit_min": unit_min, "done": min(done, float(units)),
"deadline": dl, "event": b.get("event", ""), "chapters": chapters, "order": i})
buf = int(cfg.get("buffer_days", 1))
return {"start": start, "mins": mins, "catch": catch, "exc": exc, "books": books, "buffer": buf,
"max_session": float(cfg.get("max_session_minutes", 0) or 0), "snap": int(cfg.get("chapter_snap_pages", 8)),
"name": cfg.get("name", "Plan de lecture"), "wpm": wpm}
def capacity(m, d):
"""(minutes planifiées, minutes disponibles incl. rattrapage, note)."""
if d in m["exc"]:
v, note = m["exc"][d]
return v, v, note or ("pas de lecture" if v == 0 else "")
wd = DAYS[d.weekday()]
v = m["mins"].get(wd, 0.0)
if wd in m["catch"]:
return 0.0, v, "jour de rattrapage"
return v, v, ""
def plan(m, mode="even"):
F = []
books = [b for b in m["books"]]
for b in books:
b["need"] = (b["units"] - b["done"]) * b["unit_min"]
if b["need"] <= 0:
F.append(("info", "RP05", "%s est déjà terminé - exclu" % b["title"]))
b["target"] = None
if b["deadline"] and b["deadline"] < m["start"]:
if b["need"] > 0:
F.append(("medium", "RP05", "%s : l'échéance %s est déjà passée - planifié sans échéance" % (b["title"], b["deadline"])))
b["deadline"] = None
if b["deadline"]:
b["target"] = b["deadline"] - datetime.timedelta(days=m["buffer"])
if b["target"] < m["start"]:
b["target"] = m["start"]
F.append(("medium", "RP05", "%s : l'échéance %s est trop proche pour %d jour(s) de marge - planification jusqu'à l'échéance" % (b["title"], b["deadline"], m["buffer"])))
todo = [b for b in books if b["need"] > 0]
dated = sorted([b for b in todo if b["target"]], key=lambda b: (b["target"], b["order"]))
undated = [b for b in todo if not b["target"]]
seq = dated + undated
last = max([b["deadline"] for b in dated] or [m["start"]])
# horizon : assez de jours pour tout à pleine capacité, plus de la marge
weekly = sum(m["mins"].get(d, 0) for d in DAYS if d not in m["catch"]) or 1
total_need = sum(b["need"] for b in seq)
horizon_end = max(last, m["start"]) + datetime.timedelta(days=int(total_need / weekly * 7) + 60)
days, d = [], m["start"]
while d <= horizon_end:
p, a, note = capacity(m, d)
days.append({"date": d, "cap": p, "avail": a, "note": note, "planned": 0.0, "items": []})
d += datetime.timedelta(days=1)
idx = {x["date"]: i for i, x in enumerate(days)}
# facteur de charge par segment (mode uniforme) : plus petite part uniforme de la capacité qui respecte chaque cible
load = [1.0] * len(days)
if mode == "even" and dated:
seg_start, k0 = 0, 0
while k0 < len(dated):
best, bk = 0.0, k0
cum = 0.0
for k in range(k0, len(dated)):
cum += dated[k]["need"]
ti = idx[dated[k]["target"]]
cap = sum(x["cap"] for x in days[seg_start:ti + 1])
r = cum / cap if cap > 0 else float("inf")
if r >= best:
best, bk = r, k
L = min(best * 1.03, 1.0) # 3 % de marge pour les arrêts en fin de chapitre et les arrondis
end = idx[dated[bk]["target"]]
for i in range(seg_start, end + 1):
load[i] = L
seg_start, k0 = end + 1, bk + 1
# après la dernière échéance : livres sans échéance au facteur de charge moyen du plan jusqu'ici (au moins 0,5)
tail = max(0.5, max(load[:seg_start] or [1.0]))
for i in range(seg_start, len(days)):
load[i] = tail if undated else 1.0
# allocation (échéance la plus proche d'abord), suivi de la position de chaque livre
qi = 0
pos = {b["title"]: b["done"] for b in seq}
for i, day in enumerate(days):
budget = day["cap"] * load[i]
while budget > 1e-9 and qi < len(seq):
b = seq[qi]
remaining = (b["units"] - pos[b["title"]]) * b["unit_min"]
use = min(budget, remaining)
before = pos[b["title"]]
after = before + use / b["unit_min"]
if after > b["units"] - 1e-6:
after = b["units"]
elif b["kind"] == "pages":
snapped = snap(after, b, m["snap"], use / b["unit_min"])
if snapped != after:
after = snapped
use = (after - before) * b["unit_min"]
budget = use # arrêt en fin de chapitre : le jour se termine ici
pos[b["title"]] = after
day["items"].append({"title": b["title"], "from": before, "to": after, "kind": b["kind"], "minutes": use})
day["planned"] += use
budget -= use
if after >= b["units"] - 1e-6:
b["finish"] = day["date"]; qi += 1
elif budget <= 1e-9 or use <= 1e-9:
break
if qi >= len(seq):
break
days = [x for x in days if x["date"] <= max([b.get("finish", m["start"]) for b in seq] + [last])]
for b in seq:
b.setdefault("finish", None)
# constats
for b in dated:
f = b["finish"]
if f is None or f > b["deadline"]:
avail = [x for x in days if x["date"] <= b["deadline"]]
share = sum(it["minutes"] for x in avail for it in x["items"] if it["title"] == b["title"])
short_min = b["need"] - share
# compte aussi le temps de rattrapage/marge qui serait disponible jusqu'à l'échéance
reading_days = sum(1 for x in avail if x["avail"] > 0) or 1
short_units = short_min / b["unit_min"]
msg = "%s ne peut pas être terminé avant %s%s : environ %d %s manquants à votre rythme ; fin prévue %s. Options : +%d min sur chacun des %d jours de lecture jusqu'à cette date, lire plus vite, survoler ou sauter environ %d %s, ou commencer plus tôt" % (
b["title"], b["deadline"], " (%s)" % b["event"] if b["event"] else "", math.ceil(short_units), "pages" if b["kind"] == "pages" else "minutes audio",
f or "au-delà du plan", math.ceil(short_min / reading_days), reading_days, math.ceil(short_units), "pages" if b["kind"] == "pages" else "minutes audio")
F.append(("high", "RP01", msg))
elif f > b["target"]:
F.append(("medium", "RP02", "%s se termine le %s, dans les %d jour(s) de marge avant %s - aucune marge pour un jour manqué" % (b["title"], f, m["buffer"], b["deadline"])))
if mode == "even":
peak = max(load) if load else 0
if 0.9 <= peak <= 1.0 and not any(f[1] == "RP01" for f in F):
F.append(("medium", "RP04", "le plan utilise %d %% de votre temps de lecture planifié dans sa période la plus chargée - une mauvaise semaine vous poussera dans la marge" % round(peak * 100)))
if m["max_session"]:
long = [x for x in days if x["planned"] > m["max_session"] + 0.5]
if long:
F.append(("low", "RP03", "%d jour(s) planifie(nt) plus de %d minutes (ex. %s : %d min) - divisez en deux sessions" % (
len(long), m["max_session"], long[0]["date"], round(long[0]["planned"]))))
for b in seq:
if b["kind"] == "audio":
F.append(("info", "RP06", "%s est un livre audio : le plan compte le temps d'écoute à %sx ; les trajets et les tâches ménagères peuvent le couvrir" % (b["title"], ("%.2f" % (1 / b["unit_min"])).rstrip("0").rstrip("."))))
F.sort(key=lambda f: (SEV[f[0]], f[1]))
return {"days": days, "books": seq, "all_books": books, "findings": F, "load": load}
def snap(after, b, snap_pages, day_units):
"""Déplace le point d'arrêt d'un jour en fin de chapitre si un chapitre est proche (dans snap_pages et 25 % du jour)."""
if not b["chapters"] or snap_pages <= 0:
return after
lim = min(snap_pages, max(1.0, day_units * 0.25))
ends = [c - 1 for c in b["chapters"] if c - 1 > 0]
best = None
for e in ends:
if abs(e - after) <= lim and e > after - day_units + 0.5 and (best is None or abs(e - after) < abs(best - after)):
best = e
return float(best) if best is not None else after
# ---------------------------------------------------------------- rendu
def fmt_item(it):
if it["kind"] == "audio":
return "%s %s-%s" % (it["title"], hm(it["from"]), hm(it["to"]))
a = int(round(it["from"])) + 1
z = int(round(it["to"]))
if z < a:
z = a
return "%s p. %d-%d (%d pp)" % (it["title"], a, z, z - a + 1)
def hm(x):
x = int(round(x))
return "%d:%02d" % (x // 60, x % 60)
def checkpoints(r):
out = []
pos = {}
for x in r["days"]:
for it in x["items"]:
pos[it["title"]] = it
if x["date"].weekday() == 6 and pos: # dimanches
out.append((x["date"], "; ".join("%s %s" % (t, ("p. %d" % round(it["to"]) if it["kind"] == "pages" else hm(it["to"]))) for t, it in pos.items() if it["to"] < next(b["units"] for b in r["books"] if b["title"] == t) - 1e-6) or "tous les livres actuels terminés"))
return out
def report(r, m, mode):
hi = [f for f in r["findings"] if f[0] == "high"]
out = ["# %s" % m["name"], ""]
tot_min = sum(x["planned"] for x in r["days"])
out.append("Début %s, mode %s, %d jour(s) de marge avant chaque échéance, jours de rattrapage : %s. Lecture planifiée : %d h %02d min sur %d jours." % (
m["start"], mode, m["buffer"], ", ".join(sorted(m["catch"], key=DAYS.index)) or "aucun", tot_min // 60, tot_min % 60, len(r["days"])))
out += ["", "**Verdict : %s**" % ("%d livre(s) ne peut/peuvent pas être terminé(s) avant leur échéance - changez le plan" % len(hi) if hi else "chaque échéance est tenue"), ""]
out += ["## Livres", "", "| Livre | Restant | Temps est. | Échéance | Fin prévue | Fin planifiée | Jours de marge | Moyenne par jour de lecture |", "|---|---|---|---|---|---|---|---|"]
for b in r["all_books"]:
left = b["units"] - b["done"]
rd = [x for x in r["days"] if any(it["title"] == b["title"] for it in x["items"])]
avg = left / len(rd) if rd else 0
spare = (b["deadline"] - b["finish"]).days if b.get("finish") and b["deadline"] else None
out.append("| %s | %s | %s | %s | %s | %s | %s | %s |" % (
b["title"], ("%d pp" % round(left)) if b["kind"] == "pages" else hm(left) + " audio", hm(b["need"]) + " h",
"%s %s" % (b["deadline"], "(%s)" % b["event"] if b["event"] else "") if b["deadline"] else "-",
b["target"] or "-", b.get("finish") or ("terminé" if b["need"] <= 0 else "PAS À TEMPS"),
"-" if spare is None else spare, ("%.0f pp" % avg) if b["kind"] == "pages" else hm(avg)))
out += ["", "## Constats", ""]
out += ["- **%s %s** %s" % (s.upper(), c, msg) for s, c, msg in r["findings"]] or ["- aucun"]
out += ["", "## Jour par jour", "", "| Date | Jour | Minutes | Lecture | Note |", "|---|---|---|---|---|"]
events = {}
for b in r["all_books"]:
if b["deadline"]:
events.setdefault(b["deadline"], []).append("ÉCHÉANCE %s%s" % (b["title"], " - " + b["event"] if b["event"] else ""))
if b["target"] and b["target"] != b["deadline"]:
events.setdefault(b["target"], []).append("fin prévue %s" % b["title"])
for x in r["days"]:
note = [x["note"]] if x["note"] else []
if x["note"] == "jour de rattrapage" or (x["planned"] < 0.5 and x["avail"] > 0):
note = ["rattrapage / libre (%d min disponibles)" % x["avail"]] if x["avail"] > 0 else note
note += events.get(x["date"], [])
if m["max_session"] and x["planned"] > m["max_session"] + 0.5:
note.append("2 sessions")
out.append("| %s | %s | %s | %s | %s |" % (x["date"], x["date"].strftime("%a"), round(x["planned"]) if x["planned"] >= 0.5 else "-",
"; ".join(fmt_item(it) for it in x["items"]) or "-", "; ".join(note)))
cps = checkpoints(r)
if cps:
out += ["", "## Points de contrôle hebdomadaires (où vous devriez être dimanche soir)", ""]
out += ["- %s : %s" % (d, s) for d, s in cps]
out += ["", "## Si vous prenez du retard", ""]
for b in r["books"]:
if b["kind"] != "pages" or not b["deadline"]:
continue
rd = [x for x in r["days"] if any(it["title"] == b["title"] for it in x["items"])]
if not rd:
continue
day_pp = sum(it["to"] - it["from"] for x in rd for it in x["items"] if it["title"] == b["title"]) / len(rd)
spare = [x for x in r["days"] if x["date"] <= b["deadline"] and x["date"] >= rd[0]["date"] and x["avail"] > 0 and x["planned"] < 0.5]
out.append("- %s : un jour manqué coûte environ %d pages. Rattrapez-les %s, ou ajoutez %d pages à chacun des 3 prochains jours de lecture. Replanifiez à tout moment avec `--from <date> --at \"%s=<page>\"`." % (
b["title"], round(day_pp), ("le(s) jour(s) libre(s) " + ", ".join(str(x["date"]) for x in spare[:3])) if spare else "aucun jour libre avant l'échéance", math.ceil(day_pp / 3), b["title"]))
out.append("")
return "\n".join(out)
# ---------------------------------------------------------------- auto-test
def self_test():
tests = []
def t(name, cond):
tests.append((name, bool(cond)))
def run(cfg, mode="even", **kw):
m = load(cfg, **kw)
return plan(m, mode), m
base = {"start": "2026-11-02", "reader": {"words_per_minute": 250, "words_per_page": 250}, "minutes": 30, "buffer_days": 0}
r, m = run(dict(base, books=[{"title": "A", "pages": 60, "deadline": "2026-11-08"}]), "asap")
t("asap : 30 pages/jour à 1 page/min termine le jour 2", r["books"][0]["finish"] == datetime.date(2026, 11, 3))
r, m = run(dict(base, books=[{"title": "A", "pages": 70, "deadline": "2026-11-08"}]))
t("uniforme : réparti sur la semaine (~10 min/jour + 3 % de marge)", r["books"][0]["finish"] >= datetime.date(2026, 11, 7) and 10 <= r["days"][0]["planned"] <= 10.31)
r, m = run(dict(base, books=[{"title": "A", "pages": 300, "deadline": "2026-11-08"}]))
t("irréalisable -> RP01 élevé", any(f[1] == "RP01" and f[0] == "high" for f in r["findings"]))
t("RP01 suggère des minutes supplémentaires", any("+13 min" in f[2] for f in r["findings"] if f[1] == "RP01"))
r, m = run(dict(base, buffer_days=2, books=[{"title": "A", "pages": 70, "deadline": "2026-11-08"}]))
t("jours de marge : fin 2 jours plus tôt", r["books"][0]["finish"] == datetime.date(2026, 11, 6))
r, m = run(dict(base, catch_up_days=["sun"], books=[{"title": "A", "pages": 60, "deadline": "2026-11-08"}]))
t("le dimanche de rattrapage n'a pas de lecture planifiée", r["days"][-1]["planned"] == 0 and r["books"][0]["finish"] == datetime.date(2026, 11, 7))
r, m = run(dict(base, exceptions=[{"dates": "2026-11-03..2026-11-04", "minutes": 0, "note": "voyage"}], books=[{"title": "A", "pages": 50, "deadline": "2026-11-08"}]))
t("la plage d'exception n'a pas de lecture", all(x["planned"] == 0 for x in r["days"][1:3]))
two = [{"title": "Tardif", "pages": 30, "deadline": "2026-11-20"}, {"title": "Bientôt", "pages": 60, "deadline": "2026-11-04"}]
r, m = run(dict(base, books=two))
t("échéance la plus proche d'abord", r["days"][0]["items"][0]["title"] == "Bientôt")
t("segment serré puis charge plus légère", r["load"][0] > 0.6 and r["load"][5] < 0.2)
r, m = run(dict(base, books=[{"title": "A", "pages": 70, "deadline": "2026-11-08"}]), at={"A": 36})
t("--at reprend à partir de la page", abs(sum(x["planned"] for x in r["days"]) - 35) < 0.01)
r, m = run(dict(base, books=[{"title": "W", "words": 25000, "deadline": "2026-11-08"}]))
t("nombre de mots -> pages via words_per_page", r["all_books"][0]["units"] == 100)
r, m = run(dict(base, books=[{"title": "H", "pages": 70, "difficulty": 1.5, "deadline": "2026-11-08"}]))
t("la difficulté met le temps à l'échelle", abs(r["books"][0]["need"] - 105) < 0.01)
r, m = run(dict(base, books=[{"title": "Au", "audio_minutes": 300, "speed": 1.5, "deadline": "2026-11-08"}]))
t("livre audio à 1,5x = 200 min", abs(r["books"][0]["need"] - 200) < 0.01 and any(f[1] == "RP06" for f in r["findings"]))
r, m = run(dict(base, books=[{"title": "C", "pages": 70, "deadline": "2026-11-08", "chapters": [1, 12, 23, 31, 42, 55]}]))
ends = [round(it["to"]) for x in r["days"] for it in x["items"]]
t("l'accrochage de chapitre s'arrête en fin de chapitre", 11 in ends and 22 in ends and ends[-1] == 70)
t("un élément par livre par jour", all(len({it["title"] for it in x["items"]}) == len(x["items"]) for x in r["days"]))
t("les plages de pages ne se chevauchent pas", all(fmt_item(it) for x in r["days"] for it in x["items"]) and
[int(round(it["from"])) for x in r["days"] for it in x["items"]][1:] == [int(round(it["to"])) for x in r["days"] for it in x["items"]][:-1])
r, m = run(dict(base, max_session_minutes=20, minutes=60, books=[{"title": "A", "pages": 300, "deadline": "2026-11-08"}]))
t("longues sessions -> RP03", any(f[1] == "RP03" for f in r["findings"]))
r, m = run(dict(base, books=[{"title": "A", "pages": 200, "deadline": "2026-11-08"}]))
t("plan serré -> RP04 ou RP01", any(f[1] in ("RP04", "RP01") for f in r["findings"]))
r, m = run(dict(base, books=[{"title": "A", "pages": 30}, {"title": "B", "pages": 60, "deadline": "2026-11-05"}]))
t("livre sans échéance après les livres datés", r["books"][-1]["title"] == "A" and r["books"][-1]["finish"])
r, m = run(dict(base, books=[{"title": "A", "pages": 20, "start_page": 21}]))
t("livre terminé -> RP05", any(f[1] == "RP05" for f in r["findings"]))
t("pages_per_hour", abs(load(dict(base, reader={"pages_per_hour": 30}, books=[{"title": "P", "pages": 30}]))["books"][0]["unit_min"] - 2) < 1e-9)
for bad in [dict(base, books=[]), dict(base, books=[{"title": "x"}]), dict(base, minutes={"funday": 3}, books=[{"title": "a", "pages": 3}]),
dict(base, books=[{"title": "a", "pages": 3, "deadline": "2026-13-01"}])]:
try:
run(bad); okk = False
except InputError:
okk = True
t("mauvaise entrée rejetée : %s" % (list(bad.get("minutes", {})) if isinstance(bad.get("minutes"), dict) else (bad["books"][0] if bad["books"] else "pas de livres")), okk)
r, m = run(dict(base, books=[{"title": "Ancien", "pages": 30, "deadline": "2026-10-01"}]))
t("échéance passée -> RP05, planifié sans échéance", any(f[1] == "RP05" for f in r["findings"]) and r["books"][0]["finish"])
rep = report(*run(dict(base, books=two)), "even")
t("le rapport a un verdict, un tableau des jours et des conseils de rattrapage", "Verdict" in rep and "| Date |" in rep and "Si vous prenez du retard" in rep)
for name, okk in tests:
print(("PASS " if okk else "FAIL ") + name)
n = sum(1 for _, okk in tests if okk)
print("%d/%d réussis" % (n, len(tests)))
return 0 if n == len(tests) else 1
def main(argv=None):
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
ap.add_argument("plan", nargs="?")
ap.add_argument("--mode", choices=["even", "asap"], default="even")
ap.add_argument("--from", dest="start", help="replanifier à partir de cette date (AAAA-MM-JJ)")
ap.add_argument("--at", action="append", default=[], help='"Titre=page" (ou "Titre=minute" pour les livres audio) : où vous en êtes')
ap.add_argument("--json", action="store_true")
ap.add_argument("--self-test", action="store_true")
a = ap.parse_args(argv)
if a.self_test:
return self_test()
if not a.plan:
ap.error("JSON de plan requis (ou --self-test)")
try:
try:
cfg = json.load(open(a.plan, encoding="utf-8"))
except (OSError, ValueError) as e:
raise InputError(str(e))
at = {}
for s in a.at:
if "=" not in s:
raise InputError("--at a besoin de Titre=page, reçu %r" % s)
k, v = s.rsplit("=", 1)
try:
at[k.strip()] = float(v)
except ValueError:
raise InputError("--at page doit être un nombre, reçu %r" % v)
titles = {b.get("title") for b in cfg.get("books", [])} if isinstance(cfg, dict) else set()
for k in at:
if k not in titles:
raise InputError("--at : aucun livre intitulé %r" % k)
m = load(cfg, a.start, at)
r = plan(m, a.mode)
except (InputError, KeyError, TypeError, ValueError) as e:
print("erreur : %s" % e, file=sys.stderr)
return 2
if a.json:
out = {"books": [{"title": b["title"], "deadline": str(b["deadline"]) if b["deadline"] else None, "finish": str(b.get("finish")) if b.get("finish") else None,
"minutes_needed": round(b["need"])} for b in r["all_books"]],
"days": [{"date": str(x["date"]), "minutes": round(x["planned"]), "read": [fmt_item(it) for it in x["items"]]} for x in r["days"]],
"findings": [{"severity": s, "code": c, "message": msg} for s, c, msg in r["findings"]]}
print(json.dumps(out, indent=2))
else:
print(report(r, m, a.mode))
return 1 if any(f[0] == "high" for f in r["findings"]) else 0
if __name__ == "__main__":
sys.exit(main())
Discussion
0 commentaires