Instant e Duration cálculo de horas extras

Estou pesquisando de todas as formas possíveis pra resolver o ultimo detalhe do meu “protótipo” pra cálculo de horas extras.
Portanto, eu redijo uma hora específica, mas não consigo colocar no formato adequado BR, “dd/MM/yyyy HH:mm:ss”
Pelo que vi, acho que usaria DateTimeofPattern, mas não estou conseguindo aplicar.

Alguém pode me ajudar, please?

import java.time.Duration;
import java.time.Instant;
import java.util.Scanner;

public class Main3 {

public static void main(String[] args) {
	Scanner sc = new Scanner(System.in);
	
	String Inicial = sc.nextLine();
	String Final = sc.nextLine();
	
	Instant iInicial = Instant.parse(Inicial);
	Instant iFinal = Instant.parse(Final);
	
	Duration duracao = Duration.between(iInicial, iFinal);
	
	double sum;
	sum = duracao.toHours() * 6.67;
	System.out.println(sum);
	
	sc.close();
}

}

entrada -> 2020-12-03T18:00:00.00Z
2020-12-03T20:00:00.00Z

saída - > 13.34

Acho que primeiro precisamos entender alguns conceitos. Considere as frases abaixo:

  • A reunião será às duas horas
  • A reunião durou duas horas

No primeiro caso, “duas horas” é um horário: um momento específico do dia. No segundo caso, “duas horas” é uma duração: uma quantidade de tempo (não diz que horas começou, somente quanto tempo durou).

Apesar de “parecidos”, esses conceitos não são a mesma coisa. O que pode confundir é que ambos usam as mesmas palavras (horas, minutos, segundos), mas eles não são a mesma coisa.

Em Java, classes como Instant, LocalDateTime, etc, representam datas e horas (pontos específicos na linha do tempo), enquanto um Duration representa uma duração.

E um DateTimeFormatter serve para formatar datas e horas, e infelizmente a API não fornece uma maneira de formatar durações.


Dito isso, o que exatamente você quer colocar no formato “dd/MM/yyyy HH:mm:ss”?

Se for a duração, não faz sentido. Você só está pegando a diferença em horas e multiplicando por um número, então o resultado não é uma data.

Só pra constar, se você quiser apenas a diferença em horas, nem precisa de Duration, daria para usar java.time.temporal.ChronoUnit:

double sum = ChronoUnit.HOURS.between(iInicial, iFinal) * 6.67;

Mas se você quer formatar o Instant, aí é um pouco mais chato, pois você precisa entender outros conceitos…


O que é o java.time.Instant

Um Instant representa um ponto na linha do tempo. Ele é um valor absoluto, que não depende de fusos horários (timezones).

No seu exemplo você usou 2020-12-03T18:00:00.00Z. Esse “Z” no final indica que esta data e hora está em UTC (que é o padrão a partir do qual todos os fusos horários se baseiam).

Um mesmo Instant pode corresponder a uma data e hora diferentes, dependendo do timezone. Por exemplo, no horário de Brasília, esse Instant corresponde a 2020-12-03T15:00 (pois o horário de Brasília está 3 horas atrás do UTC - exceto quando está em horário de verão, quando a diferença passa a ser de 2 horas).

Mas esse mesmo Instant também corresponde a 2020-12-04T03:00 no fuso horário do Japão (repare que até o dia mudou). Pois quando é 15h em Brasília, em Tóquio já é 3 da manhã do dia seguinte. O instante (o ponto na linha do tempo) é o mesmo, mas a data e hora local mudam.

Ou seja, antes de formatar o Instant, você tem que definir qual fuso horário será usado, pois dependendo da escolha, a saída será completamente diferente.

Por exemplo, para que o Instant seja convertido para o horário de Brasília:

Instant iInicial = ... // ler dados, fazer parse, etc

// horário de Brasília
ZoneId timezone = ZoneId.of("America/Sao_Paulo");

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/uuuu HH:mm:ss");
System.out.println(formatter.format(iInicial.atZone(timezone)));

Para usar o fuso de Tóquio, bastaria trocar para ZoneId.of("Asia/Tokyo"). Esses nomes no formato “Continente/Região” são padronizados pela IANA, e você pode consultar todos os disponíveis usando ZoneId.getAvailableZoneIds().

Se quiser usar a data e hora em UTC, pode fazer ZoneId timezone = ZoneOffset.UTC;

E se quiser, também pode usar o timezone default que estiver configurado na JVM, com ZoneId.systemDefault() - lembrando que esta é uma configuração que depende do ambiente, e que pode inclusive ser mudada em runtime (qualquer aplicação rodando na mesma JVM pode chamar TimeZone.getTimeZone e bagunçar tudo). Se você já sabe qual timezone vai usar, melhor colocar o nome fixo em vez de depender do default.


Vale lembrar que se for usar o horário de Brasília, verifique se sua JVM está atualizada com as últimas regras do horário de verão (veja este tópico para mais detalhes).

3 curtidas

Dica:
Não se deve fechar o System.in. :wink:

1 curtida

Curiosidade: isso valeria também para LocalDateTime?

Exemplo:

LocalDateTime inicio;
LocalDateTime fim;

String tempoTotal;

tempoTotal = String.valueOf(ChronoUnit.HOURS.between(inicio, fim)) + ":" + String.valueOf(ChronoUnit.MINUTES.between(inicio, fim));

Ou seria necessário converter o objeto tipo LocalDateTime para outro?

(Ah… e por que o *6,67 no post original?)

Obrigado

Funciona para qualquer objeto do java.time que tenha “horas” (ou seja, com LocalTime ou ZonedDateTime também funcionaria).

Só que ChronoUnit.HOURS sempre trará o valor arredondado (por exemplo, a diferença entre 10:00 e 12:59 do mesmo dia será 2):

LocalDateTime inicio = LocalDateTime.parse("2020-01-10T10:00");
LocalDateTime fim = LocalDateTime.parse("2020-01-10T12:59");
System.out.println(ChronoUnit.HOURS.between(inicio, fim)); // 2

Isso porque, mesmo que a diferença total seja de 2 horas e 59 minutos, ChronoUnit.HOURS arredondará as horas para baixo, retornando sempre um valor inteiro (fiz isso porque Duration.toHours, que foi usado no código original, também faz esse arredondamento).

Agora, se fizer do jeito que você fez, não funciona:

String tempoTotal = String.valueOf(ChronoUnit.HOURS.between(inicio, fim)) + ":" + String.valueOf(ChronoUnit.MINUTES.between(inicio, fim));
System.out.println(tempoTotal); // 2:179

Isso porque ChronoUnit.MINUTES retorna o total de minutos entre as datas (no caso, são 179 minutos).

Se a ideia é quebrar a duração em campos, no Java 8 tem que fazer manualmente:

LocalDateTime inicio = LocalDateTime.parse("2020-01-10T10:00");
LocalDateTime fim = LocalDateTime.parse("2020-01-10T12:59");
long totalMinutos = ChronoUnit.MINUTES.between(inicio, fim);
long horas = totalMinutos / 60;
long minutos = totalMinutos % 60;
String tempoTotal = String.format("%02d:%02d", horas, minutos);
System.out.println(tempoTotal); // 02:59

A partir do Java 9, existem métodos que foram adicionados no Duration que já trazem os valores quebrados:

LocalDateTime inicio = LocalDateTime.parse("2020-01-10T10:00");
LocalDateTime fim = LocalDateTime.parse("2020-01-10T12:59");
Duration d = Duration.between(inicio, fim);
String tempoTotal = String.format("%02d:%02d", d.toHoursPart(), d.toMinutesPart());
System.out.println(tempoTotal); // 02:59

O Duration também possui o método toMinutes, mas ele também retorna o total de minutos (e não os valores devidamente “quebrados”), ou seja, nesse caso ele retornaria 179.


Quanto ao 6.67, só o autor do post pode responder (eu só coloquei porque estava no código original, pelo que entendi deve ser o valor da hora extra ou algo assim).

2 curtidas

@hugokotsubo você é o cara! Obrigado pela explicação.

1 curtida

Só pra complementar, o objeto que você usa no método between pode interferir nos resultados, pois cada um tem sua própria lógica interna de cálculo, dependendo do caso.

Por exemplo, como LocalDateTime possui data (dia, mês, ano) e horário (hora, minuto, segundo), você pode calcular a diferença entre 22:00 de um dia e 02:00 do dia seguinte:

// diferença entre 22:00 de um dia e 02:00 do dia seguinte
System.out.println(ChronoUnit.HOURS.between(
    LocalDateTime.parse("2019-02-16T22:00"),
    LocalDateTime.parse("2019-02-17T02:00"))); // 4

Mas se usarmos LocalTime (que só possui horário, mas não tem nenhuma informação sobre a data), não tem como fazer o mesmo:

// LocalTime não tem dia, somente horas
System.out.println(ChronoUnit.HOURS.between(
    LocalTime.of(22, 0),
    LocalTime.of(2, 0))); // -20

Como LocalTime não possui nenhuma informação sobre a data (somente sobre o horário), ela não tem como saber a qual dia se referem os horários. Então ela usa a regra de considerar que meia-noite é o início do dia e 23:59:59.999999999 é o fim, e portanto a diferença entre 22:00 e 02:00 é de -20 horas (negativo porque o valor inicial é maior que o final).

E se usarmos ZonedDateTime com o timezone correspondente ao horário de Brasília?

ZoneId zone = ZoneId.of("America/Sao_Paulo");
ZonedDateTime inicio = LocalDateTime.parse("2019-02-16T22:00").atZone(zone);
ZonedDateTime fim = LocalDateTime.parse("2019-02-17T02:00").atZone(zone);
System.out.println(ChronoUnit.HOURS.between(inicio, fim)); // 5

A diferença deu 5 porque no dia 17/02/2019 houve o término do horário de verão: à meia-noite os relógios foram atrasados em uma hora, de volta para 23:00. Isso quer dizer que todos os minutos entre 23:00 e 23:59 ocorreram duas vezes (uma no horário de verão, outra no horário “normal”). Ou seja, tivemos “uma hora a mais” e portanto a diferença entre esses instantes é de 5 horas: imagine que às 22:00 do dia 16 você liga um cronômetro, quando chegar meia-noite o cronônometro está marcando que já se passaram 2 horas, aí o relógio é atrasado para 23:00 (só que o cronômetro não é “zerado” nem atrasado, ele continua sua contagem). Quando chegar 02:00 do dia 17, o cronômetro indicará que se passaram 5 horas, por isso between retorna 5.

Com LocalDateTime isso não ocorre porque esta classe não tem nenhuma informação sobre o fuso horário.

Ou seja, embora funcione com qualquer classe, você tem que escolher a que faz mais sentido em cada caso, senão pode ter resultados errados.

3 curtidas

como assim? o compilador reclama :confused:

Serei eternamente grato, e eu vou me esforçar mais nestes detalhes, com certeza a constituição da API em si eu devo ter perdido. Por isso a dificuldade.
Muito obrigado, mais uma vez.

Vê isso aqui, sobre o System.in: Entrou em loop infinito num scanner(system.in) que não reinicia

Mas dá erro de compilação (o código não chega a compilar) ou só dá um warning e o código roda normalmente?

Se for um warning, então nesse caso específico não precisa se preocupar, pois fechar o System.in é completamente desnecessário (e nem é recomendado, aliás).

3 curtidas

System.in e System.out são dois streams padrão do sistema, você não deve fechar eles.

O compilador só aponta um warning, quando você tem uma variável local do tipo Closeable e não fecha ela.
Se for variável de instância ou de classe, ele não vai apontar warning.

No seu caso o Scanner está encapsulando o System.in.
E o System.in não deve ser fechado.

Então você tem duas alternativas:

  • Você pode anotar sua variável local com @SuppressWarnings("resource");

  • Você pode transformar sua variável local em uma variável de instância ou em uma variável de classe (O que faz sentido, já que System.in também é static).

2 curtidas