Se você já precisou montar um controle de ponto, uma planilha de freelas ou um relatório de horas, provavelmente esbarrou neste bug clássico:

1.30 + 0.45 // 1.75 ... mas 1h30 + 0h45 = 2h15! 

Horas não são base 10. Neste post vamos escrever, em JavaScript puro, um pequeno conjunto de funções para:

  1. converter "HH:MM" em minutos (e de volta);
  2. calcular horas trabalhadas num dia com intervalo;
  3. converter minutos em horas decimais (para multiplicar por valor/hora);
  4. somar uma semana e calcular o saldo.

A regra de ouro: trabalhe em minutos inteiros

Inteiros não têm problema de ponto flutuante. Converta tudo para minutos na entrada, faça as contas, e só formate na saída.

function toMinutes(hhmm) { const match = /^(-)?(\d{1,3}):([0-5]\d)$/.exec(hhmm.trim()); if (!match) throw new Error(`Horário inválido: "${hhmm}"`); const [, sign, h, m] = match; const total = Number(h) * 60 + Number(m); return sign ? -total : total; } function fromMinutes(total) { const sign = total < 0 ? '-' : ''; const abs = Math.abs(total); const h = Math.floor(abs / 60); const m = abs % 60; return `${sign}${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`; } console.log(toMinutes('07:45')); // 465 console.log(fromMinutes(135)); // "02:15" console.log(fromMinutes(-20)); // "-00:20" 

Horas trabalhadas no dia (com virada de meia-noite)

Um dia típico tem quatro marcações: entrada, saída para o almoço, retorno e saída. Se a saída for menor que a entrada, o turno atravessou a meia-noite — somamos 24h.

const DAY = 24 * 60; function interval(start, end) { let diff = toMinutes(end) - toMinutes(start); if (diff < 0) diff += DAY; // virou o dia return diff; } function workedMinutes(entrada, saidaAlmoco, volta, saida) { return interval(entrada, saidaAlmoco) + interval(volta, saida); } const dia = workedMinutes('08:00', '12:00', '13:00', '17:48'); console.log(fromMinutes(dia)); // "08:48" 

Minutos para horas decimais

Só converta para decimal quando for multiplicar por dinheiro. A fórmula é minutos / 60; arredonde apenas no resultado final.

function toDecimalHours(minutes, digits = 2) { return Number((minutes / 60).toFixed(digits)); } console.log(toDecimalHours(465)); // 7.75 (7h45) console.log(toDecimalHours(20)); // 0.33 (20 min) console.log(toDecimalHours(750)); // 12.5 (12h30) // Valor de 12h30 a R$ 18,00/h — multiplique antes de arredondar const valor = (750 / 60) * 18; console.log(valor.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })); // "R$ 225,00" 

Repare: se alguém digitar 12.30 achando que é 12h30, o valor sairia R$ 221,40. É exatamente esse erro que estamos evitando.

Saldo da semana (estilo banco de horas)

function weeklyBalance(days, jornada = '08:00') { const meta = toMinutes(jornada); return days.reduce((acc, [e, sa, v, s]) => { const trabalhado = workedMinutes(e, sa, v, s); return acc + (trabalhado - meta); }, 0); } const semana = [ ['08:00', '12:00', '13:00', '18:15'], // 9h15 → +75 ['08:10', '12:00', '13:00', '16:50'], // 7h40 → -20 ['07:50', '12:00', '13:00', '17:40'], // 8h50 → +50 ['08:00', '12:00', '13:00', '17:00'], // 8h00 → 0 ['08:30', '12:00', '13:00', '17:00'], // 7h30 → -30 ]; const saldo = weeklyBalance(semana); console.log(saldo, fromMinutes(saldo), toDecimalHours(saldo)); // 75 "01:15" 1.25 

Testando rapidinho com node:assert

import assert from 'node:assert/strict'; assert.equal(toMinutes('01:30') + toMinutes('00:45'), 135); assert.equal(fromMinutes(135), '02:15'); assert.equal(interval('22:00', '05:00'), 420); // turno noturno assert.equal(toDecimalHours(45), 0.75); assert.equal(weeklyBalance(semana), 75); console.log('ok ✅'); 

Salve tudo num arquivo horas.mjs e rode com node horas.mjs.

Para ir além

  • Hora noturna reduzida: na CLT (art. 73, § 1º), a hora noturna urbana (22h–5h) conta como 52min30s. Para converter, minutosNoturnos / 52.5 dá o número de horas noturnas — 420 minutos viram 8 horas.
  • Fuso e horário de verão: se você usar Date, cuidado com fusos. Para marcações de ponto no mesmo dia, strings HH:MM e minutos inteiros costumam ser mais seguros.
  • Validação: rejeite "25:00" ou "08:60" na entrada — a regex acima já barra minutos ≥ 60.

Se quiser conferir seus resultados contra uma ferramenta pronta, a Calculadora de Horas faz essas contas no navegador e foi útil para validar os casos de teste deste post.

E você, já foi mordido pelo bug do 1.30 + 0.45? Conta nos comentários 👇