---
name: reading-plan-and-page-pace-planner
description: Planeja sua leitura dia a dia para que você termine todos os livros antes da data do clube do livro, curso ou prova - a partir de contagens de páginas ou palavras (ou duração de audiolivro), sua velocidade de leitura, os minutos que você tem em cada dia da semana, viagens e dias corridos. Distribui as páginas de forma uniforme, mantém dias de folga antes de cada prazo e dias semanais de recuperação, para nos finais de capítulos, avisa quando um prazo está fora de alcance e replaneja a partir de onde você está. Inclui um planejador em Python da biblioteca padrão.
---
# Planejador de Plano de Leitura e Ritmo de Páginas
Você ajuda leitores a parar de estudar 200 páginas na noite anterior ao clube do livro. As pessoas sabem aproximadamente quanto tempo têm (meia hora nos dias de semana, mais no fim de semana), mas não o que isso significa em páginas para um romance de 245 páginas com prazo de duas semanas, um livro de trabalho denso e um clássico de 880 páginas no mesmo mês. Você transforma livros, velocidade de leitura e minutos livres em um plano diário concreto ("Ter: p. 1-27"), mantém folga para a vida real e diz cedo e claramente quando um prazo não pode ser cumprido e o que resolveria isso.
## Arquivos nesta habilidade
- `scripts/reading_plan.py` - lê um JSON de plano (livros com páginas, palavras ou minutos de audiolivro, página inicial, dificuldade, prazo, início de capítulos; velocidade de leitura em palavras por minuto ou páginas por hora; minutos por dia da semana; viagens e exceções; dias de recuperação; dias de folga); agenda o prazo mais próximo primeiro, distribui a carga uniformemente (ou o mais rápido possível), ajusta as paradas diárias aos finais de capítulos e imprime um resumo dos livros, descobertas RP01-RP06, uma tabela dia a dia com intervalos de páginas, marcos semanais e conselhos de recuperação; `--from` + `--at "Título=página"` replaneja no meio do caminho (somente biblioteca padrão Python 3.8+)
- `references/reading-speed-and-estimates.md` - medindo sua velocidade, palavras por página por tipo de livro, fatores de dificuldade, audiolivros
- `references/pacing-and-catch-up.md` - folgas, dias de recuperação, o que fazer quando você fica para trás ou um prazo é impossível
- `templates/reading-plan.md` - o plano para entregar (amigável para calendário)
- `examples/book-club.json` - Novembro-Dezembro: um romance para um clube do livro de quinta-feira, um livro de trabalho já começado na p. 23, Middlemarch para um clube de clássicos, um audiolivro sem prazo, domingos de recuperação, uma viagem de fim de semana e dois dias incomuns
- `examples/book-club-plan.md` - a saída real do planejador nesse arquivo
## Fluxo de trabalho
### 1. Colete as entradas
Pergunte por: cada livro com sua contagem de páginas (ou contagem de palavras; a edição importa), onde você está agora, o prazo e o que é (reunião, prova), se é fácil ou denso; sua velocidade de leitura (ou cronometre um teste de 10 minutos com `references/reading-speed-and-estimates.md`); minutos realistas por dia da semana, não os ideais; viagens conhecidas, turnos tardios e dias livres; qual dia da semana deve ficar livre como dia de recuperação; e quantos dias antes de cada reunião você quer ter terminado (folga, padrão 1-2).
### 2. Execute o planejador
```
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
```
Saída 0 = todos os prazos cumpridos, 1 = pelo menos um livro não pode ser terminado a tempo, 2 = entrada inválida. Sem shell: minutos necessários = páginas x palavras por página / palavras por minuto x dificuldade; compare com os minutos disponíveis até cada data limite (prazo menos folga, dias de recuperação excluídos), prazo mais próximo primeiro.
### 3. Leia o resultado
- RP01 (alto): um livro não cabe antes do prazo. A mensagem dá o déficit em páginas e os minutos extras por dia de leitura que fechariam a lacuna. Quando o plano é inviável, o planejador lê em ritmo total antes disso, então os livros anteriores terminam cedo para liberar tempo.
- RP02 / RP04 (médio): o plano só funciona consumindo a folga, ou usa mais de 90% do seu tempo no trecho mais movimentado - uma semana ruim quebra tudo.
- RP03 (baixo): dias mais longos que a sessão máxima - divida-os. RP05/RP06 (info): livros terminados, prazos apertados, audiolivros.
- Verifique a tabela de dias para fins de semana pesados e os marcos semanais para "onde devo estar até domingo".
### 4. Corrija um plano impossível
Ofereça opções nesta ordem: comece o livro longo agora em paralelo (mova sua lógica de prazo para antes), adicione 10-20 minutos em alguns dias, use os dias de recuperação, mude parte para o audiolivro nos deslocamentos, passe o olho ou pule seções claramente marcadas, ou aceite chegar à reunião pela metade e diga isso. Execute novamente após cada mudança. Veja `references/pacing-and-catch-up.md`.
### 5. Entregue
Preencha `templates/reading-plan.md`: resumo por livro, o plano semana a semana (datas e intervalos de páginas, pronto para um calendário ou uma nota de marcador), a regra de recuperação e o comando de replanejamento. Ofereça um replanejamento rápido sempre que relatarem a página atual.
## Exemplo de resultado
Em `examples/book-club.json` (240 ppm, 20-60 min por dia, domingos livres para recuperação, 2 dias de folga) *The Remains of the Day* é terminado em 12 de novembro para o clube do livro de 19 de novembro, parando em finais de capítulos, e *Thinking in Systems* (denso, fator 1.3, da p. 23) em 27 de novembro para o grupo de 1º de dezembro. *Middlemarch* (316.000 palavras, cerca de 22 horas) não pode ser terminado até o clube de clássicos de 17 de dezembro: cerca de 452 páginas a menos, término projetado para 11 de janeiro (RP01, alto). Correção: cerca de +17 minutos em cada um dos 42 dias de leitura, ou comece junto com os outros dois agora. O sábado tranquilo de 120 minutos é sinalizado como duas sessões (RP03), e o audiolivro Piranesi (6 h 40 a 1,25x) segue depois de Middlemarch.
## Regras
- Planeje com o tempo que as pessoas realmente têm e mantenha pelo menos um dia de folga antes de cada reunião.
- Nunca encha os dias silenciosamente: se não couber, diga qual prazo falha, por quantas páginas e o que resolveria.
- Use a contagem de páginas da edição; quando apenas contagens de palavras forem conhecidas, diga que as páginas são estimativas.
- Deixe os dias de recuperação vazios no plano - eles são para ficar para trás, não para ler mais.
- A leitura deve continuar agradável: sugira pular ou mudar de formato antes de sugerir sessões de 2 horas todos os dias.
- Replaneje a partir da página atual sempre que o leitor estiver atrasado ou adiantado; não o faça "alcançar" um cronograma antigo.
FILE:references/reading-speed-and-estimates.md
# Velocidade de leitura e estimativas
## Meça sua velocidade (10 minutos)
1. Pegue o livro real (ou um parecido), comece no início de um capítulo, ajuste um cronômetro de 10 minutos e leia normalmente.
2. Conte as palavras em três linhas completas, divida por 3 = palavras por linha; conte as linhas em uma página completa = linhas por página.
3. palavras por página = palavras por linha x linhas por página; palavras por minuto = páginas lidas x palavras por página / 10.
Ou simplesmente cronometre algumas páginas e use `"pages_per_hour"` em vez de palavras por minuto.
## Valores típicos
| Leitor / material | Palavras por minuto |
|---|---|
| Leitura cuidadosa, não-ficção densa, segunda língua | 150-200 |
| Adulto médio, ficção | 200-260 |
| Leitor rápido de lazer | 300-400 |
Passar o olho não é ler: não planeje com velocidades acima de 450.
| Tipo de livro | Palavras por página |
|---|---|
| Livro de bolso de mercado de massa | 300-350 |
| Ficção de bolso comercial | 250-300 |
| Não-ficção de capa dura com notas | 350-450 |
| Letras grandes, livros infantis com capítulos | 150-200 |
| E-book | use a contagem de palavras (armazenamento da loja ou ferramentas que mostram) e o padrão de 275 palavras por página |
## Fator de dificuldade
`"difficulty"` multiplica o tempo de leitura: 1.0 ficção fácil, 1.2-1.4 não-ficção densa, clássicos com frases longas, livros técnicos com diagramas, 1.5+ livros didáticos com anotações, poesia, leitura em segunda língua.
## Contagem de páginas vs contagem de palavras
- Use a contagem de páginas da edição que o leitor tem; edições do mesmo livro podem diferir em 30%.
- Com ambos `"pages"` e `"words"`, o planejador deriva as palavras por página dessa edição (Middlemarch 316.000 / 880 = 359).
- Com apenas palavras, as páginas são estimativas (palavras / palavras por página); diga isso no plano.
## Audiolivros
- `"audio_minutes"` é a duração, `"speed"` a velocidade de reprodução: tempo necessário = duração / velocidade (400 min a 1,25x = 320 min).
- Áudio se encaixa em deslocamentos, tarefas e caminhadas: diga se esses minutos são extras ou já contados nos minutos diários.
- As posições são mostradas como h:mm do áudio.
FILE:references/pacing-and-catch-up.md
# Ritmo e recuperação
## Como o planejador agenda
- Prazo mais próximo primeiro: o livro com a data limite mais próxima (prazo menos dias de folga) é lido primeiro; livros sem prazo vêm por último.
- Modo uniforme (padrão): para cada trecho até o prazo mais apertado, usa a menor parcela igual dos seus minutos diários que ainda termina a tempo, então dias de 30 minutos podem virar dias de 20 minutos quando há espaço. Após esse prazo, o próximo trecho é calculado novamente.
- Modo ASAP: usa todos os minutos todos os dias; os livros terminam o mais cedo possível.
- Se um prazo não pode ser cumprido mesmo em ritmo total, todos os dias antes dele são planejados em ritmo total e o livro é sinalizado RP01.
- As paradas diárias se movem para um final de capítulo quando um está dentro de `chapter_snap_pages` (padrão 8) e um quarto das páginas do dia.
## Folgas e dias de recuperação
- Dias de folga (`buffer_days`, padrão 1) ficam entre o término planejado e a reunião: tempo para reler anotações ou absorver uma semana ruim.
- Dias de recuperação da semana (`catch_up_days`, ex. domingo) não recebem leitura planejada. Use-os apenas quando estiver atrasado; se estiver em dia, são tempo livre.
- Exceções (viagens, turnos tardios, um sábado tranquilo) substituem os minutos do dia da semana para essas datas.
## Quando você fica para trás
1. Perdeu um dia: leia as páginas desse dia no próximo dia de recuperação, ou adicione um terço delas a cada um dos próximos três dias.
2. Atrasado por mais de dois dias: replaneje a partir de hoje com `--from <hoje> --at "<Título>=<página atual>"` em vez de perseguir o plano antigo.
3. Ainda sem tempo (RP01): escolha um - comece o livro longo em paralelo agora, adicione 10-20 minutos em alguns dias, use o audiolivro nos deslocamentos, passe o olho em seções marcadas (prefácios, apêndices, notas), ou decida chegar pela metade e avise o grupo.
## Sinais de que o plano está apertado demais
- RP04: mais de 90% do seu tempo usado em um trecho; qualquer doença ou semana atrasada quebra.
- RP02: o livro só cabe usando os dias de folga.
- Muitos dias marcados como "2 sessões" (RP03): considere espalhar a leitura entre manhã e noite de propósito.
## Dicas para clube do livro
- Anote a página ou capítulo que o grupo discutirá se não for o livro inteiro e use isso como `pages` do livro.
- Para clássicos longos, pergunte ao clube se a reunião cobre a primeira metade; planeje dois prazos.
- Mantenha um post-it no livro com os marcos semanais do plano.
FILE:templates/reading-plan.md
# Plano de leitura: <nome> (<início> até <último prazo>)
**Ritmo:** <ppm> palavras/min, cerca de <páginas> páginas em uma sessão de 30 minutos
**Tempo:** <minutos por dia da semana>; dia(s) de recuperação: <dia>; folga: <n> dia(s) antes de cada reunião
**Veredito:** <todos os prazos são cumpridos | LIVRO não pode ser terminado até DATA - veja opções>
## Livros
| Livro | Páginas restantes | Tempo necessário | Reunião / prazo | Terminar até | Término planejado |
|---|---|---|---|---|---|
| <título> | <pp> | <h:mm> | <data - evento> | <data> | <data> |
## Semana a semana
### Semana de <data da segunda>
- Seg: <Livro p. a-b>
- Ter: <...>
- Dom: recuperação / livre
**Marco domingo à noite:** <Livro p. X>
## Se um prazo não couber
- Déficit: <páginas> páginas de <livro> até <data>
- Opções: <+N min nos dias de leitura | comece agora em paralelo | audiolivro nos deslocamentos | passe o olho nos capítulos x-y | chegue pela metade>
- Escolhido: <...>
## Ficando para trás
- Perdeu um dia? Use <dia de recuperação> ou adicione <n> páginas aos próximos três dias.
- Mais de dois dias atrasado: replaneje a partir de hoje (`--from <data> --at "<título>=<página>"`).
FILE:examples/book-club.json
{
"name": "Plano de leitura: Nov-Dez 2026 (dois clubes do livro e um livro de trabalho)",
"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": "viagem de fim de semana"},
{"dates": "2026-11-26", "minutes": 15, "note": "turno tardio"},
{"dates": "2026-12-05", "minutes": 120, "note": "sábado tranquilo"}
],
"books": [
{"title": "The Remains of the Day", "pages": 245, "words_per_page": 265, "deadline": "2026-11-19", "event": "clube do livro de quinta-feira",
"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": "grupo de leitura da equipe"},
{"title": "Middlemarch", "words": 316000, "pages": 880, "deadline": "2026-12-17", "event": "clube de clássicos"},
{"title": "Piranesi (audiolivro)", "audio_minutes": 400, "speed": 1.25}
]
}
FILE:examples/book-club-plan.md
# Plano de leitura: Nov-Dez 2026 (dois clubes do livro e um livro de trabalho)
Início 2026-11-03, modo uniforme, 2 dia(s) de folga antes de cada prazo, dias de recuperação: dom. Leitura planejada: 36 h 44 min ao longo de 81 dias.
**Veredito: 1 livro(s) não pode(m) ser terminado(s) até o prazo - mude o plano**
## Livros
| Livro | Restante | Tempo est. | Prazo | Terminar até | Término planejado | Dias de sobra | Média por dia de leitura |
|---|---|---|---|---|---|---|---|
| The Remains of the Day | 245 pp | 4:31 h | 2026-11-19 (clube do livro de quinta-feira) | 2026-11-17 | 2026-11-12 | 7 | 27 pp |
| Thinking in Systems | 196 pp | 4:57 h | 2026-12-01 (grupo de leitura da equipe) | 2026-11-29 | 2026-11-27 | 4 | 16 pp |
| Middlemarch | 880 pp | 21:57 h | 2026-12-17 (clube de clássicos) | 2026-12-15 | 2027-01-11 | -25 | 23 pp |
| Piranesi (audiolivro) | 6:40 áudio | 5:20 h | - | - | 2027-01-22 | - | 0:36 |
## Descobertas
- **ALTO RP01** Middlemarch não pode ser terminado até 2026-12-17 (clube de clássicos): cerca de 452 páginas a menos no seu ritmo; término projetado para 2027-01-11. Opções: +17 min em cada um dos 42 dias de leitura até lá, leia mais rápido, passe o olho ou pule cerca de 452 páginas, ou comece antes
- **BAIXO RP03** 1 dia(s) planejam mais de 60 minutos (ex. 2026-12-05: 120 min) - divida em duas sessões
- **INFO RP06** Piranesi (audiolivro) é um audiolivro: o plano conta o tempo de audição a 1,25x de velocidade; deslocamentos e tarefas podem cobrir isso
## Dia a dia
| Data | Dia | Minutos | Leitura | Nota |
|---|---|---|---|---|
| 2026-11-03 | Ter | 30 | The Remains of the Day p. 1-27 (27 pp) | |
| 2026-11-04 | Qua | 19 | The Remains of the Day p. 28-44 (17 pp) | |
| 2026-11-05 | Qui | 30 | The Remains of the Day p. 45-71 (27 pp) | |
| 2026-11-06 | Sex | 25 | The Remains of the Day p. 72-94 (23 pp) | |
| 2026-11-07 | Sáb | 60 | The Remains of the Day p. 95-148 (54 pp) | |
| 2026-11-08 | Dom | - | - | recuperação / sobra (45 min disponíveis) |
| 2026-11-09 | Seg | 30 | The Remains of the Day p. 149-175 (27 pp) | |
| 2026-11-10 | Ter | 30 | The Remains of the Day p. 176-202 (27 pp) | |
| 2026-11-11 | Qua | 20 | The Remains of the Day p. 203-221 (19 pp) | |
| 2026-11-12 | Qui | 30 | The Remains of the Day p. 222-245 (24 pp); Thinking in Systems p. 23-24 (2 pp) | |
| 2026-11-13 | Sex | - | - | viagem de fim de semana |
| 2026-11-14 | Sáb | - | - | viagem de fim de semana |
| 2026-11-15 | Dom | - | - | viagem de fim de semana |
| 2026-11-16 | Seg | 30 | Thinking in Systems p. 25-44 (20 pp) | |
| 2026-11-17 | Ter | 30 | Thinking in Systems p. 45-64 (20 pp) | terminar até The Remains of the Day |
| 2026-11-18 | Qua | 20 | Thinking in Systems p. 65-77 (13 pp) | |
| 2026-11-19 | Qui | 30 | Thinking in Systems p. 78-97 (20 pp) | PRAZO The Remains of the Day - clube do livro de quinta-feira |
| 2026-11-20 | Sex | 25 | Thinking in Systems p. 98-113 (16 pp) | |
| 2026-11-21 | Sáb | 60 | Thinking in Systems p. 114-153 (40 pp) | |
| 2026-11-22 | Dom | - | - | recuperação / sobra (45 min disponíveis) |
| 2026-11-23 | Seg | 30 | Thinking in Systems p. 154-172 (19 pp) | |
| 2026-11-24 | Ter | 30 | Thinking in Systems p. 173-192 (20 pp) | |
| 2026-11-25 | Qua | 20 | Thinking in Systems p. 193-205 (13 pp) | |
| 2026-11-26 | Qui | 15 | Thinking in Systems p. 206-215 (10 pp) | turno tardio |
| 2026-11-27 | Sex | 25 | Thinking in Systems p. 216-218 (3 pp); Middlemarch p. 1-14 (14 pp) | |
| 2026-11-28 | Sáb | 60 | Middlemarch p. 15-54 (40 pp) | |
| 2026-11-29 | Dom | - | - | recuperação / sobra (45 min disponíveis); terminar até Thinking in Systems |
| 2026-11-30 | Seg | 30 | Middlemarch p. 55-74 (20 pp) | |
| 2026-12-01 | Ter | 30 | Middlemarch p. 75-94 (20 pp) | PRAZO Thinking in Systems - grupo de leitura da equipe |
| 2026-12-02 | Qua | 20 | Middlemarch p. 95-107 (13 pp) | |
| 2026-12-03 | Qui | 30 | Middlemarch p. 108-128 (21 pp) | |
| 2026-12-04 | Sex | 25 | Middlemarch p. 129-144 (16 pp) | |
| 2026-12-05 | Sáb | 120 | Middlemarch p. 145-224 (80 pp) | sábado tranquilo; 2 sessões |
| 2026-12-06 | Dom | - | - | recuperação / sobra (45 min disponíveis) |
| 2026-12-07 | Seg | 30 | Middlemarch p. 225-244 (20 pp) | |
| 2026-12-08 | Ter | 30 | Middlemarch p. 245-265 (21 pp) | |
| 2026-12-09 | Qua | 20 | Middlemarch p. 266-278 (13 pp) | |
| 2026-12-10 | Qui | 30 | Middlemarch p. 279-298 (20 pp) | |
| 2026-12-11 | Sex | 25 | Middlemarch p. 299-315 (17 pp) | |
| 2026-12-12 | Sáb | 60 | Middlemarch p. 316-355 (40 pp) | |
| 2026-12-13 | Dom | - | - | recuperação / sobra (45 min disponíveis) |
| 2026-12-14 | Seg | 30 | Middlemarch p. 356-375 (20 pp) | |
| 2026-12-15 | Ter | 30 | Middlemarch p. 376-395 (20 pp) | terminar até Middlemarch |
| 2026-12-16 | Qua | 20 | Middlemarch p. 396-408 (13 pp) | |
| 2026-12-17 | Qui | 30 | Middlemarch p. 409-428 (20 pp) | PRAZO Middlemarch - clube de clássicos |
| 2026-12-18 | Sex | 25 | Middlemarch p. 429-445 (17 pp) | |
| 2026-12-19 | Sáb | 60 | Middlemarch p. 446-485 (40 pp) | |
| 2026-12-20 | Dom | - | - | recuperação / sobra (45 min disponíveis) |
| 2026-12-21 | Seg | 30 | Middlemarch p. 486-505 (20 pp) | |
| 2026-12-22 | Ter | 30 | Middlemarch p. 506-525 (20 pp) | |
| 2026-12-23 | Qua | 20 | Middlemarch p. 526-539 (14 pp) | |
| 2026-12-24 | Qui | 30 | Middlemarch p. 540-559 (20 pp) | |
| 2026-12-25 | Sex | 25 | Middlemarch p. 560-575 (16 pp) | |
| 2026-12-26 | Sáb | 60 | Middlemarch p. 576-615 (40 pp) | |
| 2026-12-27 | Dom | - | - | recuperação / sobra (45 min disponíveis) |
| 2026-12-28 | Seg | 30 | Middlemarch p. 616-635 (20 pp) | |
| 2026-12-29 | Ter | 30 | Middlemarch p. 636-656 (21 pp) | |
| 2026-12-30 | Qua | 20 | Middlemarch p. 657-669 (13 pp) | |
| 2026-12-31 | Qui | 30 | Middlemarch p. 670-689 (20 pp) | |
| 2027-01-01 | Sex | 25 | Middlemarch p. 690-706 (17 pp) | |
| 2027-01-02 | Sáb | 60 | Middlemarch p. 707-746 (40 pp) | |
| 2027-01-03 | Dom | - | - | recuperação / sobra (45 min disponíveis) |
| 2027-01-04 | Seg | 30 | Middlemarch p. 747-766 (20 pp) | |
| 2027-01-05 | Ter | 30 | Middlemarch p. 767-786 (20 pp) | |
| 2027-01-06 | Qua | 20 | Middlemarch p. 787-799 (13 pp) | |
| 2027-01-07 | Qui | 30 | Middlemarch p. 800-819 (20 pp) | |
| 2027-01-08 | Sex | 25 | Middlemarch p. 820-836 (17 pp) | |
| 2027-01-09 | Sáb | 60 | Middlemarch p. 837-876 (40 pp) | |
| 2027-01-10 | Dom | - | - | recuperação / sobra (45 min disponíveis) |
| 2027-01-11 | Seg | 30 | Middlemarch p. 877-880 (4 pp); Piranesi (audiolivro) 0:00-0:30 | |
| 2027-01-12 | Ter | 30 | Piranesi (audiolivro) 0:30-1:08 | |
| 2027-01-13 | Qua | 20 | Piranesi (audiolivro) 1:08-1:33 | |
| 2027-01-14 | Qui | 30 | Piranesi (audiolivro) 1:33-2:10 | |
| 2027-01-15 | Sex | 25 | Piranesi (audiolivro) 2:10-2:41 | |
| 2027-01-16 | Sáb | 60 | Piranesi (audiolivro) 2:41-3:56 | |
| 2027-01-17 | Dom | - | - | recuperação / sobra (45 min disponíveis) |
| 2027-01-18 | Seg | 30 | Piranesi (audiolivro) 3:56-4:34 | |
| 2027-01-19 | Ter | 30 | Piranesi (audiolivro) 4:34-5:11 | |
| 2027-01-20 | Qua | 20 | Piranesi (audiolivro) 5:11-5:36 | |
| 2027-01-21 | Qui | 30 | Piranesi (audiolivro) 5:36-6:14 | |
| 2027-01-22 | Sex | 21 | Piranesi (audiolivro) 6:14-6:40 | |
## Marcos semanais (onde você deve estar até domingo à noite)
- 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 (audiolivro) 3:56
## Se você ficar para trás
- The Remains of the Day: um dia perdido custa cerca de 27 páginas. Compense no(s) dia(s) de sobra 2026-11-08, ou adicione 10 páginas a cada um dos próximos 3 dias de leitura. Replaneje a qualquer momento com `--from <data> --at "The Remains of the Day=<página>"`.
- Thinking in Systems: um dia perdido custa cerca de 16 páginas. Compense no(s) dia(s) de sobra 2026-11-22, 2026-11-29, ou adicione 6 páginas a cada um dos próximos 3 dias de leitura. Replaneje a qualquer momento com `--from <data> --at "Thinking in Systems=<página>"`.
- Middlemarch: um dia perdido custa cerca de 23 páginas. Compense no(s) dia(s) de sobra 2026-11-29, 2026-12-06, 2026-12-13, ou adicione 8 páginas a cada um dos próximos 3 dias de leitura. Replaneje a qualquer momento com `--from <data> --at "Middlemarch=<página>"`.
FILE:scripts/reading_plan.py
#!/usr/bin/env python3
"""Planejador de plano de leitura e ritmo de páginas (somente biblioteca padrão, Python 3.8+).
Transforma uma lista de livros (páginas, contagens de palavras ou duração de audiolivro), sua velocidade de leitura e os
minutos que você tem a cada dia em um plano dia a dia com intervalos de páginas que cumpre todos os prazos
(clube do livro, curso, prova), mantém dias de folga antes de cada prazo e dias de recuperação na
semana, distribui a carga uniformemente e diz o que fazer se você ficar para trás.
Uso:
reading_plan.py PLAN.json [--mode even|asap] [--from YYYY-MM-DD] [--at "Título=página"]... [--json]
reading_plan.py --self-test
Saída: 0 = todos os prazos são cumpridos, 1 = pelo menos um livro não pode ser terminado até o prazo,
2 = entrada inválida.
"""
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 deve ser YYYY-MM-DD, recebido %r" % (what, s))
def expand_dates(spec):
spec = str(spec)
if ".." in spec:
a, b = spec.split("..", 1)
a, b = pdate(a.strip(), "intervalo de datas"), pdate(b.strip(), "intervalo de datas")
if b < a:
raise InputError("intervalo de datas %s termina antes de começar" % spec)
return [a + datetime.timedelta(days=i) for i in range((b - a).days + 1)]
return [pdate(spec.strip(), "data")]
# ---------------------------------------------------------------- modelo
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("o plano precisa de uma lista 'books' não vazia")
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 e words_per_page devem ser positivos")
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' deve ser um número ou {mon: .., tue: ..}")
mins = {k.lower()[:3]: float(v) for k, v in mins.items()}
bad = set(mins) - set(DAYS)
if bad:
raise InputError("chave(s) de dia da semana desconhecida(s) em 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 "Livro %d" % i
if "audio_minutes" in b:
speed = float(b.get("speed", 1.0))
total = float(b["audio_minutes"])
unit_min = 1.0 / speed # minutos do seu tempo por minuto de áudio
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("o livro %r precisa de pages, words ou 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"], "prazo 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", "Plano de leitura"), "wpm": wpm}
def capacity(m, d):
"""(minutos planejados, minutos disponíveis incl. recuperação, nota)."""
if d in m["exc"]:
v, note = m["exc"][d]
return v, v, note or ("sem leitura" if v == 0 else "")
wd = DAYS[d.weekday()]
v = m["mins"].get(wd, 0.0)
if wd in m["catch"]:
return 0.0, v, "dia de recuperação"
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 já está terminado - deixado de fora" % b["title"]))
b["target"] = None
if b["deadline"] and b["deadline"] < m["start"]:
if b["need"] > 0:
F.append(("medium", "RP05", "%s: o prazo %s já passou - planejado sem prazo" % (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: o prazo %s está próximo demais para %d dia(s) de folga - planejando até o prazo" % (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"]])
# horizonte: dias suficientes para tudo em capacidade total, mais folga
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)}
# fator de carga por segmento (modo uniforme): menor parcela uniforme da capacidade que cumpre cada meta
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 margem para paradas em finais de capítulo e arredondamento
end = idx[dated[bk]["target"]]
for i in range(seg_start, end + 1):
load[i] = L
seg_start, k0 = end + 1, bk + 1
# após o último prazo: livros sem prazo na carga média do plano até agora (pelo menos 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
# alocar (prazo mais próximo primeiro), rastreando a posição de cada livro
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 # parar no final do capítulo: o dia termina aqui
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)
# descobertas
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
# também conta o tempo de recuperação/folga que estaria disponível até o prazo
reading_days = sum(1 for x in avail if x["avail"] > 0) or 1
short_units = short_min / b["unit_min"]
msg = "%s não pode ser terminado até %s%s: cerca de %d %s a menos no seu ritmo; término projetado %s. Opções: +%d min em cada um dos %d dias de leitura até lá, leia mais rápido, passe o olho ou pule cerca de %d %s, ou comece antes" % (
b["title"], b["deadline"], " (%s)" % b["event"] if b["event"] else "", math.ceil(short_units), "páginas" if b["kind"] == "pages" else "minutos de áudio",
f or "além do plano", math.ceil(short_min / reading_days), reading_days, math.ceil(short_units), "páginas" if b["kind"] == "pages" else "minutos de áudio")
F.append(("high", "RP01", msg))
elif f > b["target"]:
F.append(("medium", "RP02", "%s termina em %s, dentro do(s) %d dia(s) de folga antes de %s - sem espaço para um dia perdido" % (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", "o plano usa %d%% do seu tempo de leitura planejado no trecho mais movimentado - uma semana ruim vai empurrar você para a folga" % 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 dia(s) planejam mais de %d minutos (ex. %s: %d min) - divida em duas sessões" % (
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 é um audiolivro: o plano conta o tempo de audição a %sx de velocidade; deslocamentos e tarefas podem cobrir isso" % (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):
"""Move o ponto de parada do dia para um final de capítulo se um estiver próximo (dentro de snap_pages e 25% do dia)."""
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
# ---------------------------------------------------------------- renderização
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: # domingos
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 "todos os livros atuais terminados"))
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("Início %s, modo %s, %d dia(s) de folga antes de cada prazo, dias de recuperação: %s. Leitura planejada: %d h %02d min ao longo de %d dias." % (
m["start"], mode, m["buffer"], ", ".join(sorted(m["catch"], key=DAYS.index)) or "nenhum", tot_min // 60, tot_min % 60, len(r["days"])))
out += ["", "**Veredito: %s**" % ("%d livro(s) não pode(m) ser terminado(s) até o prazo - mude o plano" % len(hi) if hi else "todos os prazos são cumpridos"), ""]
out += ["## Livros", "", "| Livro | Restante | Tempo est. | Prazo | Terminar até | Término planejado | Dias de sobra | Média por dia de leitura |", "|---|---|---|---|---|---|---|---|"]
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) + " áudio", 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 ("terminado" if b["need"] <= 0 else "FORA DO TEMPO"),
"-" if spare is None else spare, ("%.0f pp" % avg) if b["kind"] == "pages" else hm(avg)))
out += ["", "## Descobertas", ""]
out += ["- **%s %s** %s" % (s.upper(), c, msg) for s, c, msg in r["findings"]] or ["- nenhuma"]
out += ["", "## Dia a dia", "", "| Data | Dia | Minutos | Leitura | Nota |", "|---|---|---|---|---|"]
events = {}
for b in r["all_books"]:
if b["deadline"]:
events.setdefault(b["deadline"], []).append("PRAZO %s%s" % (b["title"], " - " + b["event"] if b["event"] else ""))
if b["target"] and b["target"] != b["deadline"]:
events.setdefault(b["target"], []).append("terminar até %s" % b["title"])
for x in r["days"]:
note = [x["note"]] if x["note"] else []
if x["note"] == "dia de recuperação" or (x["planned"] < 0.5 and x["avail"] > 0):
note = ["recuperação / sobra (%d min disponíveis)" % 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 sessões")
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 += ["", "## Marcos semanais (onde você deve estar até domingo à noite)", ""]
out += ["- %s: %s" % (d, s) for d, s in cps]
out += ["", "## Se você ficar para trás", ""]
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: um dia perdido custa cerca de %d páginas. Compense em %s, ou adicione %d páginas a cada um dos próximos 3 dias de leitura. Replaneje a qualquer momento com `--from <data> --at \"%s=<página>\"`." % (
b["title"], round(day_pp), ("o(s) dia(s) de sobra " + ", ".join(str(x["date"]) for x in spare[:3])) if spare else "nenhum dia de sobra antes do prazo", math.ceil(day_pp / 3), b["title"]))
out.append("")
return "\n".join(out)
# ---------------------------------------------------------------- autoteste
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 páginas/dia a 1 página/min termina no dia 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: distribuir pela semana (~10 min/dia + 3% de margem)", 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("inviável -> RP01 alto", any(f[1] == "RP01" and f[0] == "high" for f in r["findings"]))
t("RP01 sugere minutos extras", 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("dias de folga: terminar 2 dias antes", 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("domingo de recuperação não recebe leitura planejada", 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": "viagem"}], books=[{"title": "A", "pages": 50, "deadline": "2026-11-08"}]))
t("intervalo de exceção sem leitura", all(x["planned"] == 0 for x in r["days"][1:3]))
two = [{"title": "Tarde", "pages": 30, "deadline": "2026-11-20"}, {"title": "Cedo", "pages": 60, "deadline": "2026-11-04"}]
r, m = run(dict(base, books=two))
t("prazo mais próximo primeiro", r["days"][0]["items"][0]["title"] == "Cedo")
t("trecho apertado depois carga mais leve", 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 retoma da página", 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("contagem de palavras -> páginas 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("dificuldade escala o tempo", 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("audiolivro a 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("ajuste de capítulo para em finais de capítulo", 11 in ends and 22 in ends and ends[-1] == 70)
t("um item por livro por dia", all(len({it["title"] for it in x["items"]}) == len(x["items"]) for x in r["days"]))
t("intervalos de páginas não se sobrepõem", 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("sessões longas -> 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("plano apertado -> 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("livro sem prazo depois dos com prazo", r["books"][-1]["title"] == "A" and r["books"][-1]["finish"])
r, m = run(dict(base, books=[{"title": "A", "pages": 20, "start_page": 21}]))
t("livro terminado -> 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("entrada inválida rejeitada: %s" % (list(bad.get("minutes", {})) if isinstance(bad.get("minutes"), dict) else (bad["books"][0] if bad["books"] else "sem livros")), okk)
r, m = run(dict(base, books=[{"title": "Antigo", "pages": 30, "deadline": "2026-10-01"}]))
t("prazo passado -> RP05, planejado sem prazo", any(f[1] == "RP05" for f in r["findings"]) and r["books"][0]["finish"])
rep = report(*run(dict(base, books=two)), "even")
t("relatório tem veredito, tabela de dias e conselho de recuperação", "Veredito" in rep and "| Data |" in rep and "Se você ficar para trás" 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 passaram" % (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="replanejar a partir desta data (YYYY-MM-DD)")
ap.add_argument("--at", action="append", default=[], help='"Título=página" (ou "Título=minuto" para audiolivros): onde você está agora')
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 do plano necessário (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 precisa de Título=página, recebido %r" % s)
k, v = s.rsplit("=", 1)
try:
at[k.strip()] = float(v)
except ValueError:
raise InputError("--at página deve ser um número, recebido %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: nenhum livro intitulado %r" % k)
m = load(cfg, a.start, at)
r = plan(m, a.mode)
except (InputError, KeyError, TypeError, ValueError) as e:
print("erro: %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())
Discussão
0 comentários