31 de janeiro + 1 mês: o bug que encontrei comparando JavaScript e Python
Enquanto eu desenvolvia uma calculadora de datas para o meu site, notei uma coisa estranha nos testes: a mesma conta dava resultados diferentes em JavaScript e em Python.
A conta era simples: 31 de janeiro de 2026 + 1 mês.
- Python (
relativedelta): 28/02/2026 - JavaScript (
setMonth): 03/03/2026
Fevereiro não tem dia 31, e cada linguagem resolve isso de um jeito. Fui olhar outras ferramentas:
| Resposta | Quem dá |
|---|---|
| 28/02 (último dia do mês) | Excel, Python, Java, C#, date-fns, PostgreSQL, MySQL |
| 03/03 (o dia "transborda") | JavaScript, PHP |
| 01/03 (dia seguinte) | Código Civil, art. 132, para prazos |
Imagine o sistema gerando a cobrança com uma regra e a planilha do financeiro conferindo com outra. O vencimento não bate, e ninguém entende por quê.
Por que o JavaScript dá 3 de março
O setMonth muda o mês e mantém o dia 31. Como 31 de fevereiro não existe, o Date conta os dias que sobram para março:
const d = new Date(Date.UTC(2026, 0, 31));
d.setUTCMonth(1);
console.log(d.toISOString().slice(0, 10)); // 2026-03-03
O modify('+1 month') do PHP faz o mesmo. Não é bug: está na documentação. Só não é o que a maioria espera.
Como resolvi
Escolhi a regra mais comum, a do último dia do mês, e escrevi a função sem depender do setMonth:
function somarMeses(data, meses) {
const [a, m, d] = data.split('-').map(Number);
const total = a * 12 + (m - 1) + meses;
const ano = Math.floor(total / 12);
const mes = (total % 12) + 1;
const ultimoDia = new Date(Date.UTC(ano, mes, 0)).getUTCDate();
const dia = Math.min(d, ultimoDia);
return `${ano}-${String(mes).padStart(2, '0')}-${String(dia).padStart(2, '0')}`;
}
somarMeses('2026-01-31', 1); // 2026-02-28
somarMeses('2024-01-31', 1); // 2024-02-29
O Date.UTC(ano, mes, 0) pede o "dia 0" do mês seguinte, que é o último dia do mês desejado. Depois testei a função contra o relativedelta em centenas de datas sorteadas, e o resultado foi sempre igual.
O que aprendi
- Antes de somar meses, decida a regra e use a mesma no sistema, na planilha e no banco.
- Evite
setMonthemodify('+1 month')para datas de calendário. - "Vence em 30 dias" e "vence em 1 mês" não são a mesma coisa: a partir de 1º de fevereiro, dão 3 e 1º de março.
- Para prazos legais, o Código Civil manda usar o dia seguinte.
Você já tropeçou nisso? Conta nos comentários como resolveu.