├── .gitignore
├── scripts
└── mysql
│ └── criar_tabela.sql
├── src
├── main
│ └── java
│ │ ├── exemplos
│ │ ├── part3
│ │ │ ├── CalculaIdade.java
│ │ │ ├── Cap13PrincipiosBasicos.java
│ │ │ ├── Cap16AritmeticaDatas.java
│ │ │ ├── Cap19TestesOutrosCasos.java
│ │ │ ├── Cap17Formatacao.java
│ │ │ ├── Cap20Migracao.java
│ │ │ └── Cap15InstantEChronoFields.java
│ │ ├── setup
│ │ │ └── Setup.java
│ │ ├── part2
│ │ │ ├── thread
│ │ │ │ ├── LocalDateMultiThread.java
│ │ │ │ ├── DateTimeFormatterMultiThread.java
│ │ │ │ ├── CalendarMultiThread.java
│ │ │ │ └── SdfMultiThread.java
│ │ │ ├── Cap10Aritmetica.java
│ │ │ ├── Cap12Others.java
│ │ │ ├── Cap11JavaSql.java
│ │ │ ├── Cap07DateCalendar.java
│ │ │ └── Cap08e09FormatacaoParsing.java
│ │ └── part1
│ │ │ └── Capitulos1a6.java
│ │ ├── outros
│ │ ├── ThreetenBackportExemplos.java
│ │ └── ThreetenExtraExemplos.java
│ │ └── mysql
│ │ └── MysqlTests.java
└── test
│ └── java
│ └── exemplos
│ └── part3
│ └── CalculaIdadeTest.java
├── pom.xml
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | target
2 | bin
3 | .classpath
4 | .project
5 | .settings
6 |
--------------------------------------------------------------------------------
/scripts/mysql/criar_tabela.sql:
--------------------------------------------------------------------------------
1 | create table exemplo (
2 | id bigint(20) not null,
3 | data_ts timestamp null,
4 | data date null
5 | )
--------------------------------------------------------------------------------
/src/main/java/exemplos/part3/CalculaIdade.java:
--------------------------------------------------------------------------------
1 | package exemplos.part3;
2 |
3 | import java.time.Clock;
4 | import java.time.LocalDate;
5 | import java.time.temporal.ChronoUnit;
6 |
7 | /**
8 | * Classe feita apenas com fins didáticos, por isso não há verificações que um sistema real teria (verificar se a data de nascimento está no futuro, etc)
9 | *
10 | */
11 | public class CalculaIdade {
12 | // Clock a ser usado para obter a data atual
13 | private Clock clock;
14 |
15 | // em produção, você pode usar Clock.systemDefaultZone() para usar o relógio do sistema e o timezone default da JVM
16 | // nos testes, pode ser usado um Clock fixo para simular diversas situações
17 | public CalculaIdade(Clock clock) {
18 | this.clock = clock;
19 | }
20 |
21 | // retorna a idade (quantidade de anos)
22 | public long getIdade(LocalDate dataNasc) {
23 | return ChronoUnit.YEARS.between(dataNasc, LocalDate.now(clock));
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 | 4.0.0
3 | book.alura.datetime
4 | datetimeexamples
5 | 1.0-SNAPSHOT
6 |
7 |
8 | 1.8
9 | 1.8
10 |
11 |
12 |
13 |
14 | org.threeten
15 | threeten-extra
16 | 1.3.2
17 |
18 |
19 | org.threeten
20 | threetenbp
21 | 1.3.6
22 |
23 |
24 | joda-time
25 | joda-time
26 | 2.9.9
27 |
28 |
29 | com.mysql
30 | mysql-connector-j
31 | 8.4.0
32 |
33 |
34 |
35 | junit
36 | junit
37 | 4.13.2
38 | test
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/main/java/exemplos/setup/Setup.java:
--------------------------------------------------------------------------------
1 | package exemplos.setup;
2 |
3 | import java.time.Clock;
4 | import java.time.ZoneId;
5 | import java.time.ZonedDateTime;
6 | import java.util.Locale;
7 | import java.util.TimeZone;
8 |
9 | import exemplos.part3.Cap19TestesOutrosCasos;
10 |
11 | public class Setup {
12 |
13 | /**
14 | * Seta configurações default, para que você não precise mudar a sua JVM.
15 | *
16 | * Se não quiser usar este método, configure sua JVM com as opções abaixo:
17 | *
18 | *
19 | * -Duser.country=BR -Duser.language=pt -Duser.timezone=America/Sao_Paulo
20 | *
21 | */
22 | public static void setup() {
23 | Locale.setDefault(new Locale("pt", "BR"));
24 | TimeZone.setDefault(TimeZone.getTimeZone("America/Sao_Paulo"));
25 | }
26 |
27 | private static Clock DEFAULT_CLOCK = null;
28 |
29 | /**
30 | * Retorna um {@link Clock} para simular a data atual.
31 | *
32 | * O instante retornado pelo {@link Clock} corresponde a 4 de Maio de 2018, às 17:00 em São Paulo.
33 | *
34 | * O funcionamento do {@link Clock} é explicado em detalhes na classe {@link Cap19TestesOutrosCasos}
35 | */
36 | public static Clock clock() {
37 | if (DEFAULT_CLOCK == null) {
38 | ZonedDateTime z = ZonedDateTime.of(2018, 5, 4, 17, 0, 0, 0, ZoneId.of("America/Sao_Paulo"));
39 | DEFAULT_CLOCK = Clock.fixed(z.toInstant(), z.getZone());
40 | }
41 | return DEFAULT_CLOCK;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/java/exemplos/part2/thread/LocalDateMultiThread.java:
--------------------------------------------------------------------------------
1 | package exemplos.part2.thread;
2 |
3 | import java.time.LocalDate;
4 | import java.util.concurrent.ExecutorService;
5 | import java.util.concurrent.Executors;
6 | import java.util.concurrent.TimeUnit;
7 | import java.util.concurrent.atomic.AtomicInteger;
8 |
9 | public class LocalDateMultiThread {
10 |
11 | public static void main(String[] args) throws Exception {
12 | System.out.println("Start");
13 | ExecutorService pool = Executors.newCachedThreadPool();
14 |
15 | // LocalDate sempre é thread-safe, o número de erros é zero
16 | run(pool);
17 |
18 | // se você aumentar o número de threads, não esqueça de aumentar este valor
19 | pool.awaitTermination(10, TimeUnit.SECONDS);
20 | System.out.println(ERROS);
21 | }
22 |
23 | // contador de erros
24 | static AtomicInteger ERROS = new AtomicInteger(0);
25 |
26 | static LocalDate DT = LocalDate.now();
27 |
28 | static int n = 0;
29 |
30 | // LocalDate é sempre thread-safe
31 | static void run(ExecutorService pool) {
32 | // cria 100 threads
33 | for (int i = 0; i < 100; i++) {
34 | final int month = i % 2;
35 | pool.submit(() -> {
36 | // não tem setMonth, somente withMonth que retorna outro LocalDate
37 | LocalDate other = DT.withMonth(month);
38 | int m = other.getMonthValue();
39 | if (m != month) {
40 | // nunca vai entrar nesse if
41 | ERROS.incrementAndGet();
42 | }
43 | });
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/java/exemplos/part2/thread/DateTimeFormatterMultiThread.java:
--------------------------------------------------------------------------------
1 | package exemplos.part2.thread;
2 |
3 | import java.text.ParsePosition;
4 | import java.time.format.DateTimeFormatter;
5 | import java.time.temporal.ChronoField;
6 | import java.time.temporal.TemporalAccessor;
7 | import java.util.concurrent.ExecutorService;
8 | import java.util.concurrent.Executors;
9 | import java.util.concurrent.TimeUnit;
10 | import java.util.concurrent.atomic.AtomicInteger;
11 |
12 | public class DateTimeFormatterMultiThread {
13 |
14 | public static void main(String[] args) throws Exception {
15 | System.out.println("Start");
16 | ExecutorService pool = Executors.newCachedThreadPool();
17 |
18 | // DateTimeFormatter sempre é thread-safe, o número de erros é zero
19 | run(pool);
20 |
21 | // se você aumentar o número de threads, não esqueça de aumentar este valor
22 | pool.awaitTermination(10, TimeUnit.SECONDS);
23 | System.out.println(ERROS);
24 | }
25 |
26 | // contador de erros
27 | static AtomicInteger ERROS = new AtomicInteger(0);
28 |
29 | static DateTimeFormatter FMT = DateTimeFormatter.ofPattern("dd/MM/yyyy");
30 | static String INPUT = "10/01/2018";
31 |
32 | static int n = 0;
33 |
34 | static void run(ExecutorService pool) {
35 | // cria 100 threads
36 | for (int i = 0; i < 100; i++) {
37 | pool.submit(() -> {
38 | ParsePosition position = new ParsePosition(0);
39 | TemporalAccessor parsed = FMT.parseUnresolved(INPUT, position);
40 | if (position.getErrorIndex() >= 0) {
41 | System.out.println("Erro na posição " + position.getErrorIndex());
42 | ERROS.incrementAndGet();
43 | } else if (position.getIndex() < INPUT.length()) {
44 | System.out.println("Não fez parsing da String toda, parou na posição " + position.getIndex());
45 | ERROS.incrementAndGet();
46 | } else {
47 | if (parsed.getLong(ChronoField.DAY_OF_MONTH) != 10) {
48 | System.out.println(parsed);
49 | ERROS.incrementAndGet();
50 | }
51 | }
52 | });
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/test/java/exemplos/part3/CalculaIdadeTest.java:
--------------------------------------------------------------------------------
1 | package exemplos.part3;
2 |
3 | import static org.junit.Assert.assertEquals;
4 |
5 | import java.time.Clock;
6 | import java.time.LocalDate;
7 | import java.time.ZonedDateTime;
8 |
9 | import org.junit.Before;
10 | import org.junit.Test;
11 |
12 | public class CalculaIdadeTest {
13 |
14 | private CalculaIdade calc;
15 |
16 | @Before
17 | public void setup() {
18 | // usa um Clock que retorna a data atual -> 9 de janeiro de 2010, meia-noite, em São Paulo
19 | ZonedDateTime z = ZonedDateTime.parse("2010-01-09T00:00-03:00[America/Sao_Paulo]");
20 | calc = new CalculaIdade(Clock.fixed(z.toInstant(), z.getZone()));
21 | }
22 |
23 | @Test
24 | public void umDiaAntesDoDecimoAniversario() {
25 | LocalDate dataNasc = LocalDate.of(2000, 1, 10);
26 | assertEquals(calc.getIdade(dataNasc), 9); // um dia antes do décimo aniversário, a idade ainda é 9 anos
27 | }
28 |
29 | @Test
30 | public void umDiaDepoisDoDecimoAniversario() {
31 | LocalDate dataNasc = LocalDate.of(2000, 1, 8);
32 | assertEquals(calc.getIdade(dataNasc), 10); // um dia depois do décimo aniversário, a idade é 10
33 | }
34 |
35 | @Test
36 | public void noDiaDoAniversario() {
37 | LocalDate dataNasc = LocalDate.of(2000, 1, 9);
38 | assertEquals(calc.getIdade(dataNasc), 10); // no dia do aniversário, a idade é 10
39 | }
40 |
41 | // -------------------------------------------------------------------------
42 | // é possível modificar o Clock para as várias situações que você precisar
43 |
44 | /**
45 | * Este exemplo mostra como o java.time calcula quantos anos há entre duas datas, quando uma delas é 29 de fevereiro.
46 | *
47 | * Você pode ver como este comportamento varia conforme a API. Neste caso, o resultado é zero, mas no Joda-Time é 1 (veja em
48 | * {@link outros.JodaTimeExemplos#calculaIdadeNascido29Fev()})
49 | */
50 | @Test
51 | public void nascido29DeFevereiro() {
52 | // data de nascimento: 29 de fevereiro de 2000
53 | LocalDate dataNasc = LocalDate.of(2000, 2, 29);
54 |
55 | // em 28 de fevereiro de 2001, a idade será 1 ano?
56 | ZonedDateTime z = ZonedDateTime.parse("2001-02-28T10:00-03:00[America/Sao_Paulo]");
57 | calc = new CalculaIdade(Clock.fixed(z.toInstant(), z.getZone()));
58 |
59 | // a API java.time entende que em 28/02/2001 ainda não foi completado 1 ano
60 | assertEquals(calc.getIdade(dataNasc), 0);
61 |
62 | // --------------------------------------------
63 | // somente a partir de 1 de março de 2001, a idade será 1 ano
64 | z = ZonedDateTime.parse("2001-03-01T10:00-03:00[America/Sao_Paulo]");
65 | calc = new CalculaIdade(Clock.fixed(z.toInstant(), z.getZone()));
66 | assertEquals(calc.getIdade(dataNasc), 1);
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Datas e horas: Conceitos fundamentais e as APIs do Java
2 |
3 | Neste repositório estão alguns códigos de exemplo usados no livro ["Datas e horas: Conceitos fundamentais e as APIs do Java"](https://www.casadocodigo.com.br/products/livro-datas-e-horas). Como muitos códigos são explicados em mais detalhes no texto, e vários são apenas didáticos, não houve uma preocupação muito grande com *design patterns*, "boas práticas" e coisas do tipo.
4 |
5 | Algumas classes específicas são citadas no livro, mas a maioria são os mesmos códigos utilizados no texto, para servir como referência. Há um pacote para cada parte do livro, e uma classe para capítulo (exceto um ou outro que cita mais de uma classe específica), sendo que os exemplos seguem a ordem em que aparecem no texto, além de serem divididos em métodos, assim você pode chamar apenas os que lhe interessarem.
6 |
7 | Há também alguns exemplos que não estão no livro, seja por estarem fora do escopo da discussão, seja por falta de espaço mesmo.
8 |
9 | Os pacotes seguem a mesma divisão dos capítulos:
10 |
11 | - `exemplos.part1`: como esta parte é mais conceitual, os códigos são apenas para ilustrar os conceitos
12 | - `exemplos.part2`: mostra a API legada (`java.util.Date`, `Calendar`, `SimpleDateFormat` etc)
13 | - `exemplos.part3`: foca na API `java.time`
14 |
15 | Todos os exemplos usam o pacote `exemplos.setup` para fazer as configurações iniciais.
16 |
17 | Existem ainda pacotes adicionais, com códigos que não estão no livro:
18 |
19 | - `outros`: Joda-time e Threeten Backport, duas bibliotecas que são apenas mencionadas no livro. Este pacote possui alguns exemplos de uso destas APIs.
20 | - `mysql`: contém exemplos com o pacote `java.sql`, usando o banco MySQL.
21 |
22 | ---
23 |
24 | # Configurações
25 |
26 | É importante ressaltar que todos os códigos usam a classe `exemplos.setup.Setup`, que ajusta algumas configurações, de forma a não ter uma variação tão grande nos resultados gerados. Estas configurações são:
27 |
28 | - setar o *locale default* da JVM para `pt_BR` (português do Brasil)
29 | - setar o *timezone default* para `America/Sao_Paulo` (Horário de Brasília)
30 | - usar como data/hora atual "**4 de Maio de 2018, às 17:00 em São Paulo**"
31 |
32 | O terceiro item é feito usando-se um `java.time.Clock` (que é explicado em detalhes no capítulo 19). Sendo assim, nos códigos deste projeto, para obter a data/hora "atual" você terá algo assim:
33 |
34 | ```java
35 | Date dataAtual = new Date(clock().millis());
36 | LocalDateTime dataHoraAtual = LocalDateTime.now(clock());
37 | ```
38 |
39 | Sendo que no livro estará assim:
40 |
41 | ```java
42 | Date dataAtual = new Date();
43 | LocalDateTime dataHoraAtual = LocalDateTime.now();
44 | ```
45 |
46 | Se eu não usasse o `Clock`, o resultado seria a data/hora atual no momento em que você rodar o código, e obviamente os resultados mudarão a cada execução, além de ficarem totalmente diferentes do livro. Para deixar as coisas menos confusas e manter os mesmos resultados, optei por simular a data/hora atual. Se não souber como o `Clock` funciona, por ora basta saber que ele serve para obter este valor simulado.
47 |
--------------------------------------------------------------------------------
/src/main/java/exemplos/part2/thread/CalendarMultiThread.java:
--------------------------------------------------------------------------------
1 | package exemplos.part2.thread;
2 |
3 | import java.util.Calendar;
4 | import java.util.concurrent.ExecutorService;
5 | import java.util.concurrent.Executors;
6 | import java.util.concurrent.TimeUnit;
7 | import java.util.concurrent.atomic.AtomicInteger;
8 |
9 | public class CalendarMultiThread {
10 |
11 | public static void main(String[] args) throws Exception {
12 | System.out.println("Start");
13 | ExecutorService pool = Executors.newCachedThreadPool();
14 |
15 | // versão não-thread-safe, o número de erros varia a cada execução, mas quase nunca é zero
16 | runNotThreadSafe(pool);
17 |
18 | // versão thread-safe com synchronized, ERROS deve ser zero
19 | // runThreadSafeSynch(pool);
20 |
21 | // criar um novo Calendar ao invés de usar o estático, ERROS deve ser zero
22 | // runThreadSafeNotStatic(pool);
23 |
24 | // usar ThreadLocal, que cria somente um Calendar para cada thread, ERROS deve ser zero
25 | // runTheadLocal(pool);
26 |
27 | // se você aumentar o número de threads, não esqueça de aumentar este valor
28 | pool.awaitTermination(10, TimeUnit.SECONDS);
29 | System.out.println(ERROS);
30 | }
31 |
32 | // contador de erros
33 | static AtomicInteger ERROS = new AtomicInteger(0);
34 |
35 | static Calendar CAL = Calendar.getInstance();
36 |
37 | static int n = 0;
38 |
39 | // usa o Calendar estático, não é thread safe
40 | static void runNotThreadSafe(ExecutorService pool) {
41 | // cria 100 threads
42 | for (int i = 0; i < 100; i++) {
43 | final int month = i % 12;
44 | pool.submit(() -> {
45 | CAL.set(Calendar.MONTH, month);
46 | int m = CAL.get(Calendar.MONTH);
47 | if (m != month) {
48 | ERROS.incrementAndGet();
49 | }
50 | });
51 | }
52 | }
53 |
54 | static ThreadLocal TL = new ThreadLocal() {
55 | protected Calendar initialValue() {
56 | return Calendar.getInstance();
57 | };
58 | };
59 |
60 | // usa o Calendar estático, não é thread safe
61 | static void runTheadLocal(ExecutorService pool) {
62 | // cria 100 threads
63 | for (int i = 0; i < 100; i++) {
64 | final int month = i % 12;
65 | pool.submit(() -> {
66 | Calendar cal = TL.get();
67 | cal.set(Calendar.MONTH, month);
68 | int m = cal.get(Calendar.MONTH);
69 | if (m != month) {
70 | ERROS.incrementAndGet();
71 | }
72 | });
73 | }
74 | }
75 |
76 | // usa synchronized para não ter problemas de várias threads modificando o mesmo Calendar
77 | static void runThreadSafeSynch(ExecutorService pool) {
78 | // cria 100 threads
79 | for (int i = 0; i < 100; i++) {
80 | final int month = i % 12;
81 | pool.submit(() -> {
82 | synchronized (CAL) {
83 | CAL.set(Calendar.MONTH, month);
84 | int m = CAL.get(Calendar.MONTH);
85 | if (m != month) {
86 | ERROS.incrementAndGet();
87 | }
88 | }
89 | });
90 | }
91 | }
92 |
93 | // cria um novo Calendar ao invés de usar o estático
94 | static void runThreadSafeNotStatic(ExecutorService pool) {
95 | // cria 100 threads
96 | for (int i = 0; i < 100; i++) {
97 | final int month = i % 12;
98 | pool.submit(() -> {
99 | Calendar cal = Calendar.getInstance();
100 | cal.set(Calendar.MONTH, month);
101 | int m = cal.get(Calendar.MONTH);
102 | if (m != month) {
103 | ERROS.incrementAndGet();
104 | }
105 | });
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/main/java/outros/ThreetenBackportExemplos.java:
--------------------------------------------------------------------------------
1 | package outros;
2 |
3 | import java.util.Date;
4 |
5 | import org.threeten.bp.DateTimeUtils;
6 | import org.threeten.bp.DayOfWeek;
7 | import org.threeten.bp.Instant;
8 | import org.threeten.bp.LocalDate;
9 | import org.threeten.bp.LocalDateTime;
10 | import org.threeten.bp.LocalTime;
11 | import org.threeten.bp.format.DateTimeFormatter;
12 |
13 | /**
14 | * Exemplos com a biblioteca Threeten Backport (https://www.threeten.org/threetenbp/).
15 | *
16 | * Esta biblioteca é um excelente backport para as classes do Java 8, e pode ser usada com JDK 6 e 7. Ela possui a maioria das funcionalidades do java.time e
17 | * facilita muito uma futura migração.
18 | *
19 | * A classes possuem os mesmos nomes e métodos, com funcionalidades muito próximas (esta classe possui alguns exemplos de uso). A principal diferença é o nome
20 | * do pacote em que estão as classes: no JDK 8 é java.time e no backport é org.threeten.bp
21 | *
22 | * Se você está usando JDK <=5 não é possível usar o backport, mas neste caso eu recomendaria o Joda-Time, que tem vários exemplos de uso na classe
23 | * {@link JodaTimeExemplos}
24 | */
25 | public class ThreetenBackportExemplos {
26 |
27 | public static void main(String[] args) {
28 | funcionamentoBasico();
29 | converterParaDate();
30 | }
31 |
32 | static void funcionamentoBasico() {
33 | // dia de hoje (usa o relógio do sistema e o timezone default da JVM)
34 | LocalDate dataAtual = LocalDate.now();
35 |
36 | LocalDateTime dateTime = LocalDate
37 | // 20 de novembro de 2018
38 | .of(2018, 11, 20)
39 | // somar 1 dia
40 | .plusDays(1)
41 | // setar horário para meio-dia
42 | .atTime(LocalTime.NOON);
43 | System.out.println(dateTime); // 2018-11-21T12:00 <- saída no formato ISO 8601
44 |
45 | // O funcionamento básico é o mesmo do java.time (veja o pacote exemplos.part3 para mais detalhes). Mas há algumas diferenças para o Java 8
46 | DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd/MM/yyyy");
47 | // No Java 8 é possível usar method reference (DayOfWeek::from) como uma TemporalQuery (mas no JDK 6 e 7 a linha abaixo não compila)
48 | // DayOfWeek dow = fmt.parse("10/02/2018", DayOfWeek::from);
49 | // A alternativa é usar as respectivas constantes criadas para serem usadas no lugar do method reference
50 | DayOfWeek dow = fmt.parse("10/02/2018", DayOfWeek.FROM);
51 | System.out.println(dow); // SATURDAY
52 | // Em todas as classes que possuem um método from() foi adicionada a constante FROM
53 | }
54 |
55 | // conversões para java.util.Date, Calendar e java.sql.Date/Time/Timestamp, entre outras
56 | static void converterParaDate() {
57 | // No Java 8 a classe java.util.Date possui o método toInstant(), que converte para um java.time.Instant, e Date.from(Instant) que faz o inverso
58 | // No Java 6 e 7 estes métodos não existem, então as conversões são feitas pela classe org.threeten.bp.DateTimeUtils
59 | // cria um java.util.Date
60 | Date date = new Date();
61 | // converte para org.threeten.bp.Instant
62 | Instant instant = DateTimeUtils.toInstant(date);
63 | // converte de volta para java.util.Date
64 | date = DateTimeUtils.toDate(instant);
65 |
66 | // o detalhe é que o backport possui precisão de nanossegundos (9 casas decimais), enquanto a precisão de Date é de milissegundos (3 casas decimais)
67 | instant = Instant.parse("2018-01-01T10:20:30.123456789Z"); // fração de segundos com 9 casas decimais: 123456789
68 | System.out.println(instant); // 2018-01-01T10:20:30.123456789Z
69 | // converte para Date (somente os 3 primeiros dígitos da fração de segundo são mantidos)
70 | date = DateTimeUtils.toDate(instant);
71 | // converte para Instant (os dígitos 456789 já foram perdidos ao converter para Date, então eles não estarão mais aqui)
72 | instant = DateTimeUtils.toInstant(date);
73 | System.out.println(instant); // 2018-01-01T10:20:30.123Z
74 |
75 | /*
76 | * Verifique a documentação da classe (https://www.threeten.org/threetenbp/apidocs/org/threeten/bp/DateTimeUtils.html) para ver todos os métodos de
77 | * conversão. Há conversões de/para Calendar, java.sql.Date/Time/Timestamp, java.util.TimeZone, etc. Todas feitas para emular os respectivos métodos
78 | * adicionados no Java 8, que fazem as conversões destas classes de/para o java.time
79 | */
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/main/java/mysql/MysqlTests.java:
--------------------------------------------------------------------------------
1 | package mysql;
2 |
3 | import java.sql.Connection;
4 | import java.sql.Date;
5 | import java.sql.DriverManager;
6 | import java.sql.PreparedStatement;
7 | import java.sql.ResultSet;
8 | import java.sql.SQLException;
9 | import java.sql.Timestamp;
10 | import java.util.Calendar;
11 | import java.util.TimeZone;
12 |
13 | import exemplos.setup.Setup;
14 |
15 | /**
16 | * Teste feito no MySQL 5.7.21
17 | *
18 | * Para configurar os timezones no MySQL, veja em https://dev.mysql.com/doc/refman/5.5/en/time-zone-support.html
19 | *
20 | * Para criar a tabela de exemplo usada neste teste, veja o arquivo scripts/mysql/criar_tabela.sql
21 | *
22 | * Este é um código que não tem no livro, e serve para exemplificar como os valores de data e hora podem ser influenciados pela configuração de timezones e
23 | * retornar resultados confusos.
24 | *
25 | * No caso, o tipo TIMESTAMP do MySQL converte a data/hora recebida do timezone atual para UTC na hora de inserir, e ao consultar converte de volta de UTC para
26 | * o timezone atual. O que ele chama de "timezone atual" é o que está configurado no servidor, ou o timezone que está setado na conexão. Para mais detalhes,
27 | * veja a documentação: https://dev.mysql.com/doc/refman/5.6/en/datetime.html
28 | *
29 | * Neste exemplo eu seto o timezone na conexão (uso um no INSERT e outro no SELECT) para simular a situação na qual o timezone do servidor é alterado. Com isso
30 | * eu mostro como os valores podem mudar dependendo dessas configurações.
31 | *
32 | * Isso é algo específico do MySQL. Outros bancos de dados podem ou não ter problemas similares, dependendo de como cada um implementa seus tipos de data.
33 | * Sempre leia a documentação antes de decidir qual tipo usar (DATE, DATETIME, TIMESTAMP, TIMESTAMP WITH TIMEZONE, etc). Apesar de terem os mesmos nomes, cada
34 | * tipo pode ter comportamentos diferentes dependendo do banco, e as configurações de timezone (tanto do servidor quanto da conexão) podem influenciar no
35 | * resultado final (na data que você grava e na que você obtém quando consulta).
36 | *
37 | * Como o objetivo foi mostrar o comportamento dos campos de data, este código deliberadamente não está seguindo as "boas práticas", então o main tem um "throws
38 | * Exception". Desculpe, mas quis focar no mecanismo que envolve as datas e timezones, deixando as burocracias do JDBC um pouco de lado.
39 | */
40 | public class MysqlTests {
41 |
42 | static {
43 | // setar configurações, ver javadoc da classe Setup para mais detalhes
44 | Setup.setup();
45 | }
46 |
47 | public static void main(String[] args) throws Exception {
48 | Class.forName("com.mysql.cj.jdbc.Driver").newInstance();
49 |
50 | apagaTudo(); // só pra não poluir a tabela de testes, pode comentar essa linha se quiser
51 |
52 | // insere 4 de maio de 2018, 17:30, usando o timezone America/Sao_Paulo na conexão
53 | insertDate(1, 4, 5, 2018, 17, 30, "America/Sao_Paulo");
54 | // faz o select usando o timezone Asia/Tokyo na conexão
55 | select("Asia/Tokyo");
56 | /*
57 | * O timestamp retorna 2018-05-04 05:30:00.0, pois o valor do banco é 2018-05-04T17:30 (mas foi gravado com America/Sao_Paulo), e ao ler usando
58 | * Asia/Tokyo, ele é interpretado como 2018-05-04T17:30+09:00 (17:30 em Tóquio), que no timezone default da JVM (America/Sao_Paulo) equivale a 05:30 da
59 | * manhã. Eu usei timezones diferentes na conexão de propósito, mas o mesmo aconteceria se o MySQL estivesse rodando em um timezone na hora do INSERT,
60 | * mas mudasse para outro timezone e depois vc fizesse o SELECT (e não importa se a mudança de configuração do timezone foi sem querer ou de propósito,
61 | * o fato é que esta é uma situação fora do seu controle e podem começar a retornar datas erradas "do nada").
62 | */
63 | // O campo DATE, por sua vez, retorna 2018-05-04
64 |
65 | /*
66 | * Pelo mesmo motivo já explicado acima, esse SELECT mostra o timestamp como 2018-05-04 21:30:00.0. E repare que o respectivo valor do timestamp
67 | * (retornado por getTime()) é diferente do SELECT acima. O timezone usado na conexão (ou o configurado no servidor) muda completamente o valor
68 | * retornado
69 | */
70 | select("America/Los_Angeles");
71 |
72 | // ----------------------------------------------------
73 | // outro exemplo
74 | apagaTudo();
75 | insertDate(2, 4, 5, 2018, 23, 30, "Europe/London");
76 | select("Europe/London"); // timestamp retorna 2018-05-04 23:30:00.0
77 | select("America/Sao_Paulo"); // timestamp retorna 2018-05-05 03:30:00.0
78 | }
79 |
80 | static void insertDate(long id, int dia, int mes, int ano, int hora, int minuto, String timezone) throws Exception {
81 | try (Connection conn = connect(timezone);
82 | PreparedStatement st = conn.prepareStatement("insert into exemplo (id, data_ts, data) values (? , ? , ? )")) {
83 | st.setLong(1, id);
84 | st.setTimestamp(2, createTimestamp(dia, mes, ano, hora, minuto));
85 | st.setDate(3, createDate(dia, mes, ano));
86 | st.executeUpdate();
87 | }
88 | }
89 |
90 | static void select(String timezone) throws Exception {
91 | try (Connection conn = connect(timezone);
92 | PreparedStatement st = conn.prepareStatement("select * from exemplo");
93 | ResultSet rs = st.executeQuery()) {
94 | while (rs.next()) {
95 | Timestamp timestamp = rs.getTimestamp("data_ts");
96 | Date data = rs.getDate("data");
97 | System.out.printf("id=%s, data_ts=%s (%d), data=%s (%d)\n", rs.getLong("id"), timestamp, timestamp.getTime(), data, data.getTime());
98 | }
99 | }
100 | }
101 |
102 | static void apagaTudo() throws Exception {
103 | try (Connection conn = connect(TimeZone.getDefault().getID());
104 | PreparedStatement st = conn.prepareStatement("delete from exemplo")) {
105 | st.executeUpdate();
106 | }
107 | }
108 |
109 | static Connection connect(String timezone) throws SQLException {
110 | return DriverManager.getConnection("jdbc:mysql://localhost:3306/test?user=root&password=password&useTimezone=true&serverTimezone=" + timezone);
111 | }
112 |
113 | static Timestamp createTimestamp(int dia, int mes, int ano, int hora, int minuto) {
114 | Calendar cal = Calendar.getInstance();
115 | cal.set(ano, mes - 1, dia, hora, minuto, 0);
116 | cal.set(Calendar.MILLISECOND, 0);
117 |
118 | return new Timestamp(cal.getTimeInMillis());
119 | }
120 |
121 | static Date createDate(int dia, int mes, int ano) {
122 | Calendar cal = Calendar.getInstance();
123 | cal.set(ano, mes - 1, dia, 0, 0, 0);
124 | cal.set(Calendar.MILLISECOND, 0);
125 |
126 | return new Date(cal.getTimeInMillis());
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/main/java/exemplos/part2/thread/SdfMultiThread.java:
--------------------------------------------------------------------------------
1 | package exemplos.part2.thread;
2 |
3 | import java.text.SimpleDateFormat;
4 | import java.util.Date;
5 | import java.util.concurrent.ExecutorService;
6 | import java.util.concurrent.Executors;
7 | import java.util.concurrent.TimeUnit;
8 |
9 | // Teste mostrando que SimpleDateFormat não é thread-safe e como contornar esse problema
10 | public class SdfMultiThread {
11 |
12 | public static void main(String[] args) throws Exception {
13 | System.out.println("Start");
14 | ExecutorService pool = Executors.newCachedThreadPool();
15 |
16 | // versão não-thread-safe, o número de erros varia a cada execução, mas quase nunca é zero
17 | runNotThreadSafe(pool);
18 |
19 | // versão thread-safe com synchronized, sem erros
20 | // runThreadSafeSynch(pool);
21 |
22 | // criar um novo SimpleDateFormat ao invés de usar o estático, sem erros
23 | // runThreadSafeNotStatic(pool);
24 |
25 | // usar ThreadLocal, que cria somente um SimpleDateFormat para cada thread, sem erros
26 | // runThreadLocal(pool);
27 |
28 | // usar clone(), sem erros
29 | // runThreadSafeClone(pool);
30 |
31 | // se você aumentar o número de threads, não esqueça de aumentar este valor
32 | pool.awaitTermination(10, TimeUnit.SECONDS);
33 | System.out.println("Fim");
34 | }
35 |
36 | static SimpleDateFormat SDF = new SimpleDateFormat("dd/MM/yyyy");
37 |
38 | // usa o SimpleDateFormat estático, não é thread safe
39 | static void runNotThreadSafe(ExecutorService pool) {
40 | String entrada = "01/02/2018";
41 | // criar 100 threads
42 | for (int i = 0; i < 100; i++) {
43 | // cada thread faz parsing da String e em seguida formata
44 | pool.submit(() -> {
45 | try {
46 | Date date = SDF.parse(entrada);
47 | String dataFormatada = SDF.format(date);
48 | // se as Strings forem diferentes, imprime ambas
49 | if (!entrada.equals(dataFormatada)) {
50 | System.out.println(entrada + " diferente de " + dataFormatada);
51 | }
52 | } catch (Exception e) {
53 | // imprimir exceções
54 | System.out.println(e);
55 | }
56 | });
57 | }
58 | }
59 |
60 | static final ThreadLocal TL = new ThreadLocal() {
61 | protected SimpleDateFormat initialValue() {
62 | return new SimpleDateFormat("dd/MM/yyyy");
63 | }
64 | };
65 |
66 | // usa o ThreadLocal
67 | static void runThreadLocal(ExecutorService pool) {
68 | String entrada = "01/02/2018";
69 | // criar 100 threads
70 | for (int i = 0; i < 100; i++) {
71 | // cada thread faz parsing da String e em seguida formata
72 | pool.submit(() -> {
73 | try {
74 | SimpleDateFormat sdf = TL.get();
75 | Date date = sdf.parse(entrada);
76 | String dataFormatada = sdf.format(date);
77 | // se as Strings forem diferentes, imprime ambas
78 | if (!entrada.equals(dataFormatada)) {
79 | System.out.println(entrada + " diferente de " + dataFormatada);
80 | }
81 | } catch (Exception e) {
82 | // imprimir exceções
83 | System.out.println(e);
84 | }
85 | });
86 | }
87 | }
88 |
89 | // usa synchronized para não ter problemas de várias threads modificando o mesmo Calendar
90 | static void runThreadSafeSynch(ExecutorService pool) {
91 | String entrada = "01/02/2018";
92 | // criar 100 threads
93 | for (int i = 0; i < 100; i++) {
94 | // cada thread faz parsing da String e em seguida formata
95 | pool.submit(() -> {
96 | try {
97 | synchronized (SDF) {
98 | Date date = SDF.parse(entrada);
99 | String dataFormatada = SDF.format(date);
100 | // se as Strings forem diferentes, imprime ambas
101 | if (!entrada.equals(dataFormatada)) {
102 | System.out.println(entrada + " diferente de " + dataFormatada);
103 | }
104 | }
105 | } catch (Exception e) {
106 | // imprimir exceções
107 | System.out.println(e);
108 | }
109 | });
110 | }
111 | }
112 |
113 | // cria um novo SimpleDateFormat ao invés de usar o estático
114 | static void runThreadSafeNotStatic(ExecutorService pool) {
115 | String entrada = "01/02/2018";
116 | // criar 100 threads
117 | for (int i = 0; i < 100; i++) {
118 | // cada thread faz parsing da String e em seguida formata
119 | pool.submit(() -> {
120 | try {
121 | SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
122 | Date date = sdf.parse(entrada);
123 | String dataFormatada = sdf.format(date);
124 | // se as Strings forem diferentes, imprime ambas
125 | if (!entrada.equals(dataFormatada)) {
126 | System.out.println(entrada + " diferente de " + dataFormatada);
127 | }
128 | } catch (Exception e) {
129 | // imprimir exceções
130 | System.out.println(e);
131 | }
132 | });
133 | }
134 | }
135 |
136 | // cria um clone do SimpleDateFormat
137 | static void runThreadSafeClone(ExecutorService pool) {
138 | String entrada = "01/02/2018";
139 | // criar 100 threads
140 | for (int i = 0; i < 100; i++) {
141 | // cada thread faz parsing da String e em seguida formata
142 | pool.submit(() -> {
143 | try {
144 | SimpleDateFormat sdf = (SimpleDateFormat) SDF.clone();
145 | Date date = sdf.parse(entrada);
146 | String dataFormatada = sdf.format(date);
147 | // se as Strings forem diferentes, imprime ambas
148 | if (!entrada.equals(dataFormatada)) {
149 | System.out.println(entrada + " diferente de " + dataFormatada);
150 | }
151 | } catch (Exception e) {
152 | // imprimir exceções
153 | System.out.println(e);
154 | }
155 | });
156 | }
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/src/main/java/exemplos/part2/Cap10Aritmetica.java:
--------------------------------------------------------------------------------
1 | package exemplos.part2;
2 |
3 | import static exemplos.setup.Setup.clock;
4 |
5 | import java.text.ParseException;
6 | import java.text.SimpleDateFormat;
7 | import java.util.Calendar;
8 | import java.util.Date;
9 | import java.util.TimeZone;
10 | import java.util.concurrent.TimeUnit;
11 |
12 | public class Cap10Aritmetica {
13 |
14 | public static void main(String[] args) throws ParseException {
15 | somarDuasHorasDate();
16 | somarDuasHorasCalendar();
17 | somarDiaHorarioDeVerao();
18 | somarUmMes();
19 | subtrairMes();
20 | parseDuracao();
21 |
22 | // --------------------------------------------
23 | // exemplos que não estão no livro
24 | duracaoISO8601();
25 | }
26 |
27 | static void somarDuasHorasDate() {
28 | // usando nossa "data atual" simulada
29 | Date dataAtual = new Date(clock().millis());
30 | System.out.println(dataAtual); // Fri May 04 17:00:00 BRT 2018
31 |
32 | // 2 horas em milissegundos
33 | long duasHorasEmMs = TimeUnit.MILLISECONDS.convert(2, TimeUnit.HOURS);
34 | // somar 2 horas ao valor do timestamp
35 | dataAtual.setTime(dataAtual.getTime() + duasHorasEmMs);
36 | System.out.println(dataAtual); // Fri May 04 19:00:00 BRT 2018
37 | }
38 |
39 | static void somarDuasHorasCalendar() {
40 | Calendar dataAtual = Calendar.getInstance();
41 | // setar timestamp para nossa "data atual" simulada (se quiser a data atual de fato, remova esta linha)
42 | dataAtual.setTimeInMillis(clock().millis());
43 | System.out.println(dataAtual.getTime()); // Fri May 04 17:00:00 BRT 2018
44 |
45 | // somar 2 horas
46 | dataAtual.add(Calendar.HOUR_OF_DAY, 2);
47 | System.out.println(dataAtual.getTime()); // Fri May 04 19:00:00 BRT 2018
48 | }
49 |
50 | static void somarDiaHorarioDeVerao() {
51 | // cria um Calendar
52 | Calendar cal = Calendar.getInstance();
53 |
54 | // Um dia antes do horário de verão: 2017-10-14T10:00-03:00 (America/Sao_Paulo)
55 | cal.set(2017, Calendar.OCTOBER, 14, 10, 0, 0);
56 | cal.set(Calendar.MILLISECOND, 0);
57 |
58 | // antes de somar 1 dia
59 | long timestampAntes = cal.getTimeInMillis();
60 | System.out.println("antes: " + cal.getTime()); // antes: Sat Oct 14 10:00:00 BRT 2017
61 |
62 | // somar 1 dia
63 | cal.add(Calendar.DAY_OF_MONTH, 1);
64 | long timestampDepois = cal.getTimeInMillis();
65 | System.out.println("depois:" + cal.getTime()); // depois:Sun Oct 15 10:00:00 BRST 2017
66 |
67 | // calcula a diferenca em horas
68 | long diferenca = timestampDepois - timestampAntes;
69 | // a diferenca está em milissegundos, converter para horas
70 | System.out.println(TimeUnit.HOURS.convert(diferenca, TimeUnit.MILLISECONDS)); // 23
71 | }
72 |
73 | static void somarUmMes() {
74 | // cria um Calendar
75 | Calendar cal = Calendar.getInstance();
76 | // setar timestamp para nossa "data atual" simulada (se quiser a data atual de fato, remova esta linha)
77 | cal.setTimeInMillis(clock().millis());
78 |
79 | // 1 de janeiro de 2018
80 | cal.set(2018, Calendar.JANUARY, 1);
81 | // somar 1 mês
82 | cal.add(Calendar.MONTH, 1);
83 | // 1 de fevereiro
84 | System.out.println(cal.getTime()); // Thu Feb 01 17:00:00 BRST 2018
85 |
86 | // ------------------------------------------------------
87 | cal = Calendar.getInstance();
88 | // setar timestamp para nossa "data atual" simulada (se quiser a data atual de fato, remova esta linha)
89 | cal.setTimeInMillis(clock().millis());
90 |
91 | // 31 de janeiro de 2018
92 | cal.set(2018, Calendar.JANUARY, 31);
93 | // somar 1 mês
94 | cal.add(Calendar.MONTH, 1);
95 | System.out.println(cal.getTime()); // Wed Feb 28 17:00:00 BRT 2018
96 | }
97 |
98 | static void subtrairMes() {
99 | Calendar cal = Calendar.getInstance();
100 | // testar dos dias 28 a 31 de janeiro
101 | for (int dia = 28; dia <= 31; dia++) {
102 | cal.set(2018, Calendar.JANUARY, dia);
103 |
104 | // somar 1 mês
105 | cal.add(Calendar.MONTH, 1);
106 | System.out.println(cal.getTime()); // 28 de fevereiro
107 |
108 | // subtrair 1 mês
109 | cal.add(Calendar.MONTH, -1);
110 | System.out.println(cal.getTime()); // 28 de janeiro
111 | // mesmo com o dia inicial sendo 29, 30, ou 31, o resultado de somar 1 mês e depois subtrair 1 mês é 28 de janeiro
112 | }
113 | }
114 |
115 | // Parsing de duração, às vezes até funciona, mas não é o jeito certo
116 | // Nas APIs nativas do Java, somente o java.time tem classes específicas para tratar durações
117 | static void parseDuracao() throws ParseException {
118 | // ERRADO: fazer parsing de uma duração, tratando-a como uma data
119 | SimpleDateFormat parser = new SimpleDateFormat("'PT'H'H'm'M'");
120 | // usar UTC, para evitar problemas com horário de verão
121 | parser.setTimeZone(TimeZone.getTimeZone("UTC"));
122 | // parse de uma duração (10 horas e 20 minutos)
123 | Date date = parser.parse("PT10H20M");
124 | // "duração" em milissegundos
125 | long duracao = date.getTime();
126 | System.out.println(duracao); // 37200000 - por coincidência, está correto, mas nem sempre funciona
127 |
128 | // ---------------------------------------------------------------------
129 | // ERRADO: tentando fazer parsing de uma duração, tratando-a como uma data
130 | parser = new SimpleDateFormat("'P'M'M'");
131 | parser.setTimeZone(TimeZone.getTimeZone("UTC"));
132 | // duração de 1 mês
133 | date = parser.parse("P1M");
134 | duracao = date.getTime(); // qual será o valor da duração?
135 | System.out.println(duracao); // zero (totalmente errado, já que a entrada corresponde a uma duração de 1 mês)
136 |
137 | // Isso acontece porque SimpleDateFormat só sabe lidar com datas. Tratar os dados como uma duração pode até funcionar às vezes por coincidência, mas nem
138 | // sempre funcionará. A solução é fazer o parsing manualmente (String.split/substring/regex) ou usar uma API com suporte a durações, como o java.time
139 | }
140 |
141 | // converter uma duração em milissegundos para o formato ISO8601
142 | static void duracaoISO8601() {
143 | // duracao total em milisegundos
144 | long millis = 21878400000L;
145 |
146 | // quantidade total de segundos
147 | long secs = TimeUnit.SECONDS.convert(millis, TimeUnit.MILLISECONDS);
148 | // descontar a quantidade de segundos do valor dos milissegundos
149 | millis -= TimeUnit.MILLISECONDS.convert(secs, TimeUnit.SECONDS);
150 |
151 | // quantidade total de minutos
152 | long mins = TimeUnit.MINUTES.convert(secs, TimeUnit.SECONDS);
153 | // descontar a quantidade de minutos do valor dos segundos
154 | secs -= TimeUnit.SECONDS.convert(mins, TimeUnit.MINUTES);
155 |
156 | // quantidade total de horas
157 | long horas = TimeUnit.HOURS.convert(mins, TimeUnit.MINUTES);
158 | // descontar a quantidade de horas do valor dos minutos
159 | mins -= TimeUnit.MINUTES.convert(horas, TimeUnit.HOURS);
160 |
161 | // quantidade total de dias
162 | long dias = TimeUnit.DAYS.convert(horas, TimeUnit.HOURS);
163 | // descontar a quantidade de dias do total de horas
164 | horas -= TimeUnit.HOURS.convert(dias, TimeUnit.DAYS);
165 |
166 | // quantidade total de meses
167 | long meses = dias / 30; // considerar todo mês com 30 dias
168 | // descontar a quantidade de meses do total de dias
169 | dias -= (meses * 30);
170 |
171 | System.out.printf("P%dM%dDT%dH%dM%d.%dS", meses, dias, horas, mins, secs, millis); // P8M13DT5H20M0.0S
172 | // o problema é que eu considerei todos os meses com 30 dias, o que é uma aproximação arbitrária
173 | // dependendo das datas envolvidas, 1 mês pode ter de 28 a 31 dias, porém a API não fornece um mecanismo mais preciso para calcularmos corretamente
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/src/main/java/outros/ThreetenExtraExemplos.java:
--------------------------------------------------------------------------------
1 | package outros;
2 |
3 | import java.time.Duration;
4 | import java.time.Instant;
5 | import java.time.LocalDate;
6 | import java.time.LocalDateTime;
7 | import java.time.Period;
8 | import java.time.format.DateTimeFormatter;
9 | import java.time.format.DateTimeFormatterBuilder;
10 | import java.time.temporal.ChronoField;
11 | import java.time.temporal.ChronoUnit;
12 | import java.util.Locale;
13 | import java.util.concurrent.TimeUnit;
14 |
15 | import org.threeten.extra.Interval;
16 | import org.threeten.extra.PeriodDuration;
17 | import org.threeten.extra.Temporals;
18 |
19 | import exemplos.part3.Cap16AritmeticaDatas;
20 | import exemplos.setup.Setup;
21 |
22 | /**
23 | * Durante o desenvolvimento do java.time, algumas ideias da JSR 310 foram rejeitadas e acabaram não sendo incorporadas ao JDK 8. Porém, isto não impediu seu
24 | * autor (Stephen Colebourne) de adicionar estas funcionalidades em um projeto à parte. Daí surgiu o ThreeTen-Extra (http://www.threeten.org/threeten-extra).
25 | *
26 | * Funciona somente em Java >= 8
27 | */
28 | public class ThreetenExtraExemplos {
29 |
30 | public static void main(String[] args) {
31 | periodDurationJuntos();
32 | intervalos();
33 | parseVariosFormatters();
34 | outros();
35 | }
36 |
37 | /**
38 | * No java.time há duas classes para durações: {@link java.time.Period} e {@link java.time.Duration} - ver exemplos em {@link Cap16AritmeticaDatas}
39 | *
40 | * No Threeten Extra foi criada uma classe que junta um Period com um Duration
41 | */
42 | static void periodDurationJuntos() {
43 | // 1 de maio de 2018, 10:30:45
44 | LocalDateTime inicio = LocalDateTime.of(2018, 5, 1, 10, 30, 45);
45 | // 3 de maio de 2018, 15:25:55
46 | LocalDateTime fim = LocalDateTime.of(2018, 5, 3, 15, 25, 55);
47 | // Usando Period, eu tenho que converter as datas para LocalDate, pois o método Period.between() não aceita parâmetros de outro tipo
48 | System.out.println(Period.between(inicio.toLocalDate(), fim.toLocalDate())); // P2D (2 dias)
49 |
50 | // Já o método Duration.between aceita qualquer classe que implemente Temporal, então eu posso usar LocalDateTime sem problemas:
51 | System.out.println(Duration.between(inicio, fim)); // PT52H55M10S (52 horas, 55 minutos e 10 segundos)
52 |
53 | // juntando Period com Duration, com a classe org.threeten.extra.PeriodDuration
54 | System.out.println(PeriodDuration.between(inicio, fim)); // P2DT4H55M10S (2 dias, 4 horas, 55 minutos e 10 segundos)
55 |
56 | // duração de 3 dias, 5 horas e 10 minutos
57 | PeriodDuration pd = PeriodDuration.parse("P3DT5H10M");
58 | // pode ser somado à datas
59 | LocalDateTime dt = inicio.plus(pd);
60 | System.out.println(dt); // 2018-05-04T15:40:45
61 |
62 | // -------------------------------------------------
63 | // PeriodDuration pode ter campos negativos
64 | // 1 de maio de 2018, 10:00
65 | inicio = LocalDateTime.of(2018, 5, 1, 10, 0, 0);
66 | // 3 de maio de 2018, 09:00
67 | fim = LocalDateTime.of(2018, 5, 3, 9, 0, 0);
68 | System.out.println(Period.between(inicio.toLocalDate(), fim.toLocalDate())); // P2D (2 dias)
69 | System.out.println(Duration.between(inicio, fim)); // PT47HS (47 horas)
70 | System.out.println(PeriodDuration.between(inicio, fim)); // P2DT-1H (2 dias e menos 1 hora)
71 | // é possível normalizar o PeriodDuration
72 | pd = PeriodDuration.between(inicio, fim).normalizedStandardDays();
73 | System.out.println(pd); // P1DT23H (1 dia e 23 horas)
74 |
75 | // mas a normalização nem sempre deixa os campos positivos
76 | pd = PeriodDuration.parse("P1DT-49H"); // 1 dia e menos 49 horas
77 | System.out.println(pd.normalizedStandardDays()); // P-1DT-1H (menos 1 dia e menos 1 hora)
78 | /*
79 | * O resultado é esse porque:
80 | *
81 | * - a duração original é de 1 dia e menos 49 horas
82 | *
83 | * - 1 dia é igual a 24 horas, e subtraindo-se 49 horas, temos menos 25 horas (`PT-25H`)
84 | *
85 | * - menos 25 horas é o mesmo que menos 1 dia e menos 1 hora (basta pensar que subtrair 25 horas é o mesmo que subtrair 1 dia e depois subtrair 1 hora)
86 | */
87 | }
88 |
89 | /**
90 | * Ideia que existia no Joda-time (ver {@link JodaTimeExemplos#intervalos()}), mas ficou de fora do java.time
91 | */
92 | public static void intervalos() {
93 | // instante atual simulado: 4 de Maio de 2018, às 17:00 em São Paulo
94 | Instant inicio = Instant.now(Setup.clock());
95 | Instant fim = inicio.plusSeconds(589344593);
96 | // intervalo entre os instantes
97 | Interval intervalo = Interval.of(inicio, fim);
98 | // imprime no format ISO 8601: https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
99 | System.out.println(intervalo); // 2018-05-04T20:00:00Z/2037-01-05T22:49:53Z
100 | System.out.println(intervalo.getStart()); // 2018-05-04T20:00:00Z
101 | System.out.println(intervalo.getEnd()); // 2037-01-05T22:49:53Z
102 |
103 | // duração do intervalo
104 | System.out.println(intervalo.toDuration()); // PT163706H49M53S (163706 horas, 49 minutos e 53 segundos)
105 | // verificar se um Instant pertence ao intervalo
106 | System.out.println(intervalo.contains(Instant.parse("2019-01-01T10:30:00Z"))); // true
107 |
108 | // ---------------------------------------
109 | // outro modo de criar um intervalo
110 | Duration duracao = Duration.ofHours(10); // duração de 10 horas
111 | // instante final do intervalo é 10 horas depois do inicial
112 | intervalo = Interval.of(inicio, duracao); // Duration é somada ao Instant inicial, para gerar o Instant final
113 | // o código abaixo é equivalente: o instante final também é 10 horas depois do inicial
114 | intervalo = Interval.of(inicio, inicio.plus(10, ChronoUnit.HOURS));
115 |
116 | // também é possível simular um intervalo só com instante inicial
117 | intervalo = Interval.of(inicio, Instant.MAX);
118 | System.out.println(intervalo.isUnboundedEnd()); // true (não tem instante final)
119 | // ou um intervalo sem início (só com o instante final)
120 | intervalo = Interval.of(Instant.MIN, fim);
121 | System.out.println(intervalo.isUnboundedStart()); // true (não tem instante inicial)
122 | }
123 |
124 | static void parseVariosFormatters() {
125 | // formatos possíveis para a data
126 | DateTimeFormatter fmt1 = DateTimeFormatter.ofPattern("dd/MM/uuuu");
127 | DateTimeFormatter fmt2 = DateTimeFormatter.ofPattern("MMM, dd uuuu", Locale.ENGLISH);
128 | DateTimeFormatter fmt3 = new DateTimeFormatterBuilder()
129 | // ano-mês, com dia opcional
130 | .appendPattern("uuuu-MM[-dd]")
131 | // valor default para o dia = 1 (quando a string não tiver o dia)
132 | .parseDefaulting(ChronoField.DAY_OF_MONTH, 1)
133 | // criar o DateTimeFormatter
134 | .toFormatter();
135 |
136 | // string pode estar em qualquer um dos 3 formatos
137 | LocalDate dt = Temporals.parseFirstMatching("May, 04 2018", LocalDate::from, fmt1, fmt2, fmt3);
138 | // O método tenta fazer o parsing com fmt1, fmt2 e fmt3 (ele para no primeiro que der certo)
139 | // O resultado é determinado pelo segundo parâmetro, que é um TemporalQuery (no caso, usei o method reference LocalDate::from)
140 | System.out.println(dt); // 2018-05-04
141 | }
142 |
143 | static void outros() {
144 | // conversões entre ChronoUnit e TimeUnit
145 | ChronoUnit chronoUnit = Temporals.chronoUnit(TimeUnit.HOURS);
146 | System.out.println(chronoUnit); // Hours
147 | try {
148 | // lança exceção, pois TimeUnit não possui equivalente para YEARS
149 | TimeUnit timeUnit = Temporals.timeUnit(ChronoUnit.YEARS);
150 | System.out.println(timeUnit);
151 | } catch (IllegalArgumentException e) {
152 | System.out.println(e.getMessage()); // ChronoUnit cannot be converted to TimeUnit: Years
153 | }
154 |
155 | // TemporalAdjuster para dias úteis (considerando que dias úteis são: de segunda a sexta)
156 | LocalDate date = LocalDate.of(2018, 5, 4); // 2018-05-04
157 | // próximo dia útil
158 | System.out.println(date.with(Temporals.nextWorkingDay())); // 2018-05-07
159 | // dia útil anterior
160 | System.out.println(date.with(Temporals.previousWorkingDay())); // 2018-05-03
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/src/main/java/exemplos/part2/Cap12Others.java:
--------------------------------------------------------------------------------
1 | package exemplos.part2;
2 |
3 | import static exemplos.setup.Setup.clock;
4 | import static exemplos.setup.Setup.setup;
5 |
6 | import java.text.ParseException;
7 | import java.text.SimpleDateFormat;
8 | import java.util.Calendar;
9 | import java.util.Date;
10 | import java.util.HashSet;
11 | import java.util.Locale;
12 | import java.util.Set;
13 | import java.util.TimeZone;
14 | import java.util.concurrent.TimeUnit;
15 |
16 | import exemplos.part3.Cap19TestesOutrosCasos;
17 | import exemplos.setup.Setup;
18 |
19 | public class Cap12Others {
20 |
21 | static {
22 | // setar configurações, ver javadoc da classe Setup para mais detalhes
23 | setup();
24 | }
25 |
26 | public static void main(String[] args) throws ParseException {
27 | getInfoTimezone();
28 | encontrarTimezonesPorOffset();
29 | encontrarTimezonesPorAbreviacao();
30 | proximaSexta();
31 | ultimoDiaDoMes();
32 | terceiraSegundaFeiraDoMes();
33 | }
34 |
35 | static void getInfoTimezone() {
36 | TimeZone zone = TimeZone.getTimeZone("America/Sao_Paulo");
37 | System.out.println(zone.getID()); // America/Sao_Paulo
38 | // mostra o DisplayName no locale padrão da JVM (pt_BR)
39 | System.out.println(zone.getDisplayName()); // Fuso horário de Brasília
40 | // mostra o DisplayName em inglês
41 | System.out.println(zone.getDisplayName(Locale.US)); // Brasilia Time
42 |
43 | // obter o offset no instante "atual" (usar nosso timestamp simulado)
44 | System.out.println(zone.getOffset(clock().millis())); // -10800000 (milissegundos, que corresponde a -3 horas -> offset -03:00)
45 |
46 | // -------------------------------------------------------------
47 | // usando nossa "data atual" simulada
48 | Date dataAtual = new Date(clock().millis());
49 | // verificar se a data atual está em horário de verão
50 | System.out.println(zone.inDaylightTime(dataAtual)); // false
51 |
52 | // quantidade de milissegundos adicionado à hora local quando é horário de verão
53 | System.out.println(zone.getDSTSavings()); // 3600000 (corresponde a uma hora)
54 | }
55 |
56 | /**
57 | * Vários timezones usam o mesmo offset ou a mesma abreviação, e estes podem mudar dependendo da data e hora. Por exemplo, America/Sao_Paulo usa o offset
58 | * -03:00 e abreviação BRT, mas durante o horário de verão muda para -02:00 e BRST. Mas ele não é o único timezone que usa os offsets -03:00 e -02:00, então
59 | * apenas com a informação do offset não temos como afirmar com certeza que se trata deste timezone.
60 | *
61 | * Abreviações também são problemáticas. IST, por exemplo, é usada na Índia, Irlanda e Israel, cada um com um offset diferente (e na Irlanda é só durante o
62 | * horário de verão).
63 | *
64 | * Por isso, dado um offset ou abreviação, não há como obter um único timezone. O melhor que podemos obter é uma lista de timezones, e mesmo esta lista pode
65 | * variar conforme a data e hora que escolhemos como referência. Se o offset for -02:00 e a data for em dezembro, provavelmente America/Sao_Paulo estará na
66 | * lista, já que em dezembro este timezone está em horário de verão (exceto se consultarmos em um ano que não teve horário de verão - como nos anos 70, por
67 | * exemplo - e portanto o offset era -03:00).
68 | *
69 | * Os 2 métodos abaixo (encontrarTimezonesPorOffset() e encontrarTimezonesPorAbreviacao()) usam como referência a data "atual" simulada da classe
70 | * {@link Setup} e verificam quais os timezones que nesta data usam determinado offset ou abreviação. Compare com a forma que o java.time faz a mesma coisa
71 | * em {@link Cap19TestesOutrosCasos#encontrarTimezonesPorOffsetOuAbreviacao()}
72 | */
73 | public static void encontrarTimezonesPorOffset() {
74 | // offset +02:00 em milissegundos
75 | long offset = TimeUnit.MILLISECONDS.convert(2, TimeUnit.HOURS);
76 | // timestamp a ser usado como referência (equivalente a 2018-05-04T17:00-03:00 - nosso instante "atual" simulado)
77 | long referencia = clock().millis();
78 | // Set para guardar os resultados
79 | Set timezones = new HashSet<>();
80 | // procurar todos os timezones que usam o offset na data de referência
81 | for (String id : TimeZone.getAvailableIDs()) {
82 | // obtém o offset usado neste timezone, na data de referência
83 | int offsetUsado = TimeZone.getTimeZone(id).getOffset(referencia);
84 | if (offset == offsetUsado) {
85 | timezones.add(id);
86 | }
87 | }
88 | // saída depende da versão do TZDB instalada na JVM, mas deve imprimir pelo menos uns 50 timezones
89 | System.out.println(timezones);
90 | }
91 |
92 | public static void encontrarTimezonesPorAbreviacao() {
93 | // abreviação
94 | String abrev = "EST";
95 | // data a ser usada como referência (equivalente a 2018-05-04T17:00-03:00 - nosso instante "atual" simulado)
96 | Date referencia = new Date(clock().millis());
97 | // Set para guardar os resultados
98 | Set timezones = new HashSet<>();
99 | // verificar todos os locales (pois a abreviação é locale sensitive)
100 | for (Locale locale : Locale.getAvailableLocales()) {
101 | // pattern "z" para a abreviação do timezone
102 | SimpleDateFormat formatter = new SimpleDateFormat("z", locale);
103 | // verificar todos os timezones
104 | for (String id : TimeZone.getAvailableIDs()) {
105 | // obtém a abreviação usada por este timezone, na data de referência
106 | formatter.setTimeZone(TimeZone.getTimeZone(id));
107 | if (abrev.equals(formatter.format(referencia))) {
108 | timezones.add(id);
109 | }
110 | }
111 | }
112 | // saída depende da versão do TZDB instalada na JVM, mas com certeza terá mais de um
113 | System.out.println(timezones);
114 | }
115 |
116 | static void proximaSexta() {
117 | Calendar cal = Calendar.getInstance();
118 | // setar timestamp para nossa "data atual" simulada (2018-05-04T17:00-03:00)
119 | cal.setTimeInMillis(clock().millis());
120 | // 27 de dezembro de 2011
121 | cal.set(2011, Calendar.DECEMBER, 27);
122 |
123 | // vou somando um dia, até encontrar a próxima Sexta-feira
124 | do {
125 | cal.add(Calendar.DAY_OF_MONTH, 1);
126 | } while (cal.get(Calendar.DAY_OF_WEEK) != Calendar.FRIDAY);
127 | System.out.println(cal.getTime()); // Fri Dec 30 17:00:00 BRST 2011 <- o horário original (17:00) é preservado
128 |
129 | // -------------------------------------------------------------
130 | // rodar de novo, mas mudando o timezone padrão
131 | // No timezone Pacific/Apia (Samoa), o dia 30/12/2011 foi "pulado" e a próxima sexta-feira será 6 de janeiro de 2012
132 | // https://www.telegraph.co.uk/news/worldnews/australiaandthepacific/samoa/8980665/Samoa-prepares-to-skip-Dec-30.html
133 | TimeZone.setDefault(TimeZone.getTimeZone("Pacific/Apia"));
134 | cal = Calendar.getInstance();
135 | // setar timestamp para nossa "data atual" simulada (2018-05-04T17:00-03:00)
136 | cal.setTimeInMillis(clock().millis());
137 | // 27 de dezembro de 2011
138 | cal.set(2011, Calendar.DECEMBER, 27);
139 |
140 | // vou somando um dia, até encontrar a próxima Sexta-feira
141 | do {
142 | cal.add(Calendar.DAY_OF_MONTH, 1);
143 | } while (cal.get(Calendar.DAY_OF_WEEK) != Calendar.FRIDAY);
144 | System.out.println(cal.getTime()); // Fri Jan 06 09:00:00 WSDT 2012
145 |
146 | // -------------------------------------------------------------
147 | // rodar de novo, mas mudando o timezone padrão
148 | // voltar para America/Sao_Paulo e ver o que acontece quando há mudança de horário de verão
149 | TimeZone.setDefault(TimeZone.getTimeZone("America/Sao_Paulo"));
150 | // 14 de outubro de 2017, à meia-noite - timezone padrão é America/Sao_Paulo
151 | cal = Calendar.getInstance();
152 | cal.set(2017, Calendar.OCTOBER, 14, 0, 0, 0);
153 | // vou somando um dia, até encontrar a próxima Sexta-feira
154 | do {
155 | // quando começa o horário de verão, à meia-noite o relógio é adiantado para 01:00
156 | // e essa mudança de horário permanece nas demais datas
157 | cal.add(Calendar.DAY_OF_MONTH, 1);
158 | } while (cal.get(Calendar.DAY_OF_WEEK) != Calendar.FRIDAY);
159 | // o resultado tem horário igual a 01:00
160 | System.out.println(cal.getTime()); // Fri Oct 20 01:00:00 BRST 2017
161 |
162 | // voltar config default (para não impactar os outros métodos)
163 | setup();
164 | }
165 |
166 | static void ultimoDiaDoMes() {
167 | // criar uma data em fevereiro de 2018
168 | Calendar cal = Calendar.getInstance();
169 | cal.set(2018, Calendar.FEBRUARY, 16, 0, 0, 0);
170 |
171 | // mudar para o último dia do mês (28 de fevereiro de 2018)
172 | cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
173 | System.out.println(cal.getTime()); // Wed Feb 28 00:00:00 BRT 2018
174 | }
175 |
176 | static void terceiraSegundaFeiraDoMes() {
177 | Calendar cal = Calendar.getInstance();
178 | // setar timestamp para nossa "data atual" simulada (2018-05-04T17:00-03:00)
179 | cal.setTimeInMillis(clock().millis());
180 |
181 | // primeiro mudamos para a terceira semana
182 | cal.set(Calendar.DAY_OF_WEEK_IN_MONTH, 3);
183 | // depois mudamos para a Segunda-feira desta semana
184 | cal.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
185 |
186 | System.out.println(cal.getTime()); // Mon May 21 17:00:00 BRT 2018
187 | }
188 | }
--------------------------------------------------------------------------------
/src/main/java/exemplos/part2/Cap11JavaSql.java:
--------------------------------------------------------------------------------
1 | package exemplos.part2;
2 |
3 | import static exemplos.setup.Setup.setup;
4 |
5 | import java.sql.Time;
6 | import java.sql.Timestamp;
7 | import java.text.ParseException;
8 | import java.text.SimpleDateFormat;
9 | import java.util.Calendar;
10 | import java.util.Date;
11 | import java.util.TimeZone;
12 |
13 | public class Cap11JavaSql {
14 |
15 | static {
16 | // setar configurações, ver javadoc da classe Setup para mais detalhes
17 | setup();
18 | }
19 |
20 | // "normaliza" um timestamp, setando a hora para 00:00:00.000 ou o dia para 1970-01-01
21 | static long normalizar(long timestamp, boolean mudarHorario) {
22 | // criar Calendar com o valor do timestamp
23 | Calendar cal = Calendar.getInstance();
24 | cal.setTimeInMillis(timestamp);
25 |
26 | if (mudarHorario) {
27 | // setar horário para meia-noite
28 | cal.set(Calendar.HOUR_OF_DAY, 0);
29 | cal.set(Calendar.MINUTE, 0);
30 | cal.set(Calendar.SECOND, 0);
31 | } else {
32 | cal.set(Calendar.YEAR, 1970);
33 | cal.set(Calendar.MONDAY, Calendar.JANUARY);
34 | cal.set(Calendar.DAY_OF_MONTH, 1);
35 | }
36 |
37 | // ambos os casos ignoram os milissegundos
38 | cal.set(Calendar.MILLISECOND, 0);
39 |
40 | return cal.getTimeInMillis();
41 | }
42 |
43 | public static void main(String[] args) throws ParseException {
44 | dataHoraMudaConformeTimezone();
45 | dataPossuiHorario();
46 | comparar();
47 | compararNormalizado();
48 | parse();
49 | parseTimestamp();
50 | parseTimestampFracaoSegundosVariavel();
51 | }
52 |
53 | static void dataHoraMudaConformeTimezone() {
54 | // timestamp 1525464000000 = 2018-05-04T17:00-03:00 (America/Sao_Paulo)
55 | java.sql.Date date = new java.sql.Date(1525464000000L);
56 | Time time = new Time(1525464000000L);
57 | // 2018-05-04T17:00-03:00
58 | Timestamp ts = new Timestamp(1525464000000L);
59 |
60 | // imprimir a data e hora usando o timezone padrão
61 | System.out.println(date); // 2018-05-04
62 | System.out.println(time); // 17:00:00
63 | System.out.println(ts); // 2018-05-04 17:00:00.0
64 | // mudar o timezone padrão
65 | TimeZone.setDefault(TimeZone.getTimeZone("Asia/Tokyo"));
66 | System.out.println(date); // 2018-05-05
67 | System.out.println(time); // 05:00:00
68 | System.out.println(ts); // 2018-05-05 05:00:00.0
69 |
70 | // voltar config default (para não impactar os outros métodos)
71 | setup();
72 | }
73 |
74 | static void dataPossuiHorario() {
75 | // 1525464000000 = 2018-05-04T17:00-03:00 (America/Sao_Paulo)
76 | java.sql.Date sqlDate = new java.sql.Date(1525464000000L);
77 |
78 | try {
79 | sqlDate.getHours(); // método deprecated e lança exceção
80 | } catch (IllegalArgumentException e) {
81 | System.out.println("Não pode obter horas de java.sql.Date");
82 | }
83 |
84 | // na verdade, há como obter as horas
85 | SimpleDateFormat formatter = new SimpleDateFormat("dd/MM/yyyy HH:mm");
86 | System.out.println(formatter.format(sqlDate)); // 04/05/2018 17:00
87 |
88 | // ------------------------------------------------------------
89 | // o mesmo vale para java.sql.Time
90 | Time time = new Time(1525464000000L);
91 | try {
92 | time.getMonth(); // método deprecated e lança exceção
93 | } catch (IllegalArgumentException e) {
94 | System.out.println("Não pode obter mês de java.sql.Time");
95 | }
96 |
97 | // na verdade, há como obter o mês (e demais campos de data)
98 | System.out.println(formatter.format(sqlDate)); // 04/05/2018 17:00
99 |
100 | // ou seja, os getters escondem artificialmente as informações - uma pequena "gambiarra", necessária porque estas classes herdam de java.util.Date
101 | }
102 |
103 | static void comparar() {
104 | // 1525464000000 = 2018-05-04T17:00-03:00
105 | java.sql.Date sqlDate = new java.sql.Date(1525464000000L);
106 | // 1525464000001 = 2018-05-04T17:00:00.001-03:00
107 | java.sql.Date sqlDate2 = new java.sql.Date(1525464000001L);
108 | System.out.println(sqlDate); // 2018-05-04
109 | System.out.println(sqlDate2); // 2018-05-04
110 | System.out.println("iguais? " + sqlDate.equals(sqlDate2)); // iguais? false
111 | System.out.println("sqlDate antes de sqlDate2? " + sqlDate.before(sqlDate2)); // sqlDate antes de sqlDate2? true
112 |
113 | // 1525464000000 = 2018-05-04T17:00-03:00
114 | Time time1 = new Time(1525464000000L);
115 | // 1525464000001 = 2018-05-04T17:00:00.001-03:00
116 | Time time2 = new Time(1525464000001L);
117 | System.out.println(time1); // 17:00:00
118 | System.out.println(time2); // 17:00:00
119 | System.out.println(time1.equals(time2)); // false
120 | System.out.println(time1.before(time2)); // true
121 | }
122 |
123 | static void compararNormalizado() {
124 | // 1525464000000 = 2018-05-04T17:00-03:00 (normalizar para 2018-05-04T00:00-03:00)
125 | java.sql.Date sqlDate = new java.sql.Date(normalizar(1525464000000L, true));
126 | // 1525464000001 = 2018-05-04T17:00:00.001-03:00 (normalizar para 2018-05-04T00:00-03:00)
127 | java.sql.Date sqlDate2 = new java.sql.Date(normalizar(1525464000001L, true));
128 | System.out.println(sqlDate); // 2018-05-04
129 | System.out.println(sqlDate2); // 2018-05-04
130 | System.out.println("iguais? " + sqlDate.equals(sqlDate2)); // iguais? true
131 | System.out.println("sqlDate antes de sqlDate2? " + sqlDate.before(sqlDate2)); // sqlDate antes de sqlDate2? false
132 |
133 | // 1525464000001 = 2018-05-04T17:00:00.001-03:00 (normalizar para 1970-01-01T17:00:00-03:00)
134 | Time time1 = new Time(normalizar(1525464000000L, false));
135 | // 1525464000001 = 2018-05-04T17:00:00.001-03:00 (normalizar para 1970-01-01T17:00:00.001-03:00)
136 | Time time2 = new Time(normalizar(1525464000001L, false));
137 | System.out.println(time1); // 17:00:00
138 | System.out.println(time2); // 17:00:00
139 | System.out.println(time1.equals(time2)); // true
140 | System.out.println(time1.before(time2)); // false
141 | }
142 |
143 | static void parse() throws ParseException {
144 | // valueOf recebe Strings no formato ISO 8601
145 | java.sql.Date sqlDate = java.sql.Date.valueOf("2018-05-04");
146 | Time time = Time.valueOf("10:00:00");
147 | System.out.println(sqlDate); // 2018-05-04
148 | System.out.println(time); // 10:00:00
149 | // lembrando que os valores são normalizados: sqlDate tem horário igual a meia-noite e time tem data igual a 1970-01-01 (ambos no timezone padrão da
150 | // JVM)
151 |
152 | // parsing de String em formato diferente do ISO 8601
153 | SimpleDateFormat parser = new SimpleDateFormat("dd/MM/yyyy");
154 | // parse() retorna sempre um java.util.Date
155 | java.util.Date date = parser.parse("04/05/2018");
156 | // passar o timestamp para java.sql.Date
157 | sqlDate = new java.sql.Date(date.getTime());
158 | System.out.println(sqlDate); // 2018-05-04
159 | // timestamp já está normalizado no timezone padrão da JVM
160 | // porém, é possível mudar o timezone usando parser.setTimeZone()
161 | }
162 |
163 | /**
164 | * As frações de segundo são tratadas separadamente, já que SimpleDateFormat não consegue fazer o parsing corretamente quando há mais de 3 casas decimais
165 | * (conforme visto em {@link Cap08e09FormatacaoParsing#parseFracaoSegundos()})
166 | */
167 | static void parseTimestamp() throws ParseException {
168 | Timestamp ts = Timestamp.valueOf("2018-05-04 10:30:45.123456789");
169 | System.out.println(ts); // 2018-05-04 10:30:45.123456789
170 | System.out.println(ts.getTime()); // 1525397445123
171 | System.out.println(ts.getNanos()); // 123456789
172 |
173 | String input = "04/05/2018 10:30:45.123456789";
174 | // separar frações de segundo do restante da data/hora
175 | String[] dadosSeparados = input.split("\\.");
176 | // fazer parsing da data/hora (sem os nanossegundos)
177 | SimpleDateFormat parser = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
178 | Date date = parser.parse(dadosSeparados[0]);
179 | // usar o valor do timestamp no construtor
180 | ts = new Timestamp(date.getTime());
181 | // setar os nanossegundos
182 | ts.setNanos(Integer.parseInt(dadosSeparados[1]));
183 | System.out.println(ts); // 2018-05-04 10:30:45.123456789
184 | System.out.println(ts.getTime()); // 1525440645123
185 | System.out.println(ts.getNanos()); // 123456789
186 | }
187 |
188 | // exemplo que não tem no livro, trata fração de segundos com qualquer quantidade de dígitos
189 | static void parseTimestampFracaoSegundosVariavel() throws ParseException {
190 | String input = "04/05/2018 10:30:45.12345";
191 | // separar frações de segundo do restante da data/hora
192 | String[] dadosSeparados = input.split("\\.");
193 | // fazer parsing da data/hora (sem os nanossegundos)
194 | SimpleDateFormat parser = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
195 | Date date = parser.parse(dadosSeparados[0]);
196 | // usar o valor do timestamp no construtor
197 | Timestamp ts = new Timestamp(date.getTime());
198 | // setar os nanossegundos (preencher a string com zeros à direita para completar os 9 dígitos)
199 | ts.setNanos(Integer.parseInt(String.format("%-9s", dadosSeparados[1]).replaceAll(" ", "0")));
200 | System.out.println(ts); // 2018-05-04 10:30:45.12345
201 | System.out.println(ts.getTime()); // 1525440645123
202 | System.out.println(ts.getNanos()); // 123450000
203 | }
204 | }
--------------------------------------------------------------------------------
/src/main/java/exemplos/part3/Cap13PrincipiosBasicos.java:
--------------------------------------------------------------------------------
1 | package exemplos.part3;
2 |
3 | import static exemplos.setup.Setup.clock;
4 | import static exemplos.setup.Setup.setup;
5 | import static java.time.temporal.TemporalAdjusters.dayOfWeekInMonth;
6 | import static java.time.temporal.TemporalAdjusters.firstDayOfMonth;
7 | import static java.time.temporal.TemporalAdjusters.firstDayOfNextMonth;
8 | import static java.time.temporal.TemporalAdjusters.firstDayOfNextYear;
9 | import static java.time.temporal.TemporalAdjusters.firstDayOfYear;
10 | import static java.time.temporal.TemporalAdjusters.firstInMonth;
11 | import static java.time.temporal.TemporalAdjusters.lastDayOfMonth;
12 | import static java.time.temporal.TemporalAdjusters.lastInMonth;
13 | import static java.time.temporal.TemporalAdjusters.next;
14 |
15 | import java.time.DateTimeException;
16 | import java.time.DayOfWeek;
17 | import java.time.LocalDate;
18 | import java.time.LocalDateTime;
19 | import java.time.LocalTime;
20 | import java.time.Month;
21 | import java.time.temporal.UnsupportedTemporalTypeException;
22 | import java.util.Arrays;
23 | import java.util.Collections;
24 | import java.util.List;
25 |
26 | public class Cap13PrincipiosBasicos {
27 |
28 | static {
29 | // setar configurações, ver javadoc da classe Setup para mais detalhes
30 | setup();
31 | }
32 |
33 | public static void main(String[] args) {
34 | criarDatas();
35 | obterInfo();
36 | mudarValores();
37 | usarTemporalAdjuster();
38 | ajustesComAsClassesLocais();
39 | comparar();
40 | ordenar();
41 |
42 | // --------------------------------------------
43 | // exemplos que não estão no livro
44 | outrosModosDeCriarDatas();
45 | }
46 |
47 | static void criarDatas() {
48 | // 4 de maio de 2018
49 | LocalDate data = LocalDate.of(2018, 5, 4);
50 | // saída no formato ISO 8601
51 | System.out.println(data); // 2018-05-04
52 |
53 | try {
54 | // tentando criar data com mês 13
55 | LocalDate mesInvalido = LocalDate.of(2018, 13, 1);
56 | } catch (DateTimeException e) {
57 | System.out.println(e.getMessage()); // Invalid value for MonthOfYear (valid values 1 - 12): 13
58 | }
59 |
60 | // 17:30 (cinco e meia da tarde)
61 | LocalTime horario = LocalTime.of(17, 30);
62 | // 2018-05-04T17:30
63 | LocalDateTime dataHora = LocalDateTime.of(2018, 5, 4, 17, 30);
64 |
65 | System.out.println(horario); // 17:30
66 | System.out.println(dataHora); // 2018-05-04T17:30
67 |
68 | // saída omite os segundos e frações de segundo, se forem zero
69 | System.out.println(LocalTime.of(17, 0)); // 17:00
70 | System.out.println(LocalTime.of(7, 0, 45)); // 07:00:45
71 | System.out.println(LocalTime.of(0, 0, 0, 123000000)); // 00:00:00.123
72 |
73 | // data/hora atual
74 | // usando a data/hora "atual" simulada
75 | LocalDate dataAtual = LocalDate.now(clock());
76 | LocalTime horarioAtual = LocalTime.now(clock());
77 | LocalDateTime dataHoraAtual = LocalDateTime.now(clock());
78 | System.out.println(dataAtual); // 2018-05-04
79 | System.out.println(horarioAtual); // 17:00
80 | System.out.println(dataHoraAtual); // 2018-05-04T17:00
81 | // now() sem parâmetros usa o relógio do sistema e o timezone padrão da JVM
82 | // LocalDate dataAtual = LocalDate.now();
83 | // LocalTime horarioAtual = LocalTime.now();
84 | // LocalDateTime dataHoraAtual = LocalDateTime.now();
85 | }
86 |
87 | static void obterInfo() {
88 | // 4 de maio de 2018
89 | LocalDate data = LocalDate.of(2018, 5, 4);
90 | int diaDoMes = data.getDayOfMonth(); // 4
91 | int ano = data.getYear(); // 2018
92 | Month mes = data.getMonth(); // Month.MAY
93 | int valorNumericoMes = data.getMonthValue(); // 5
94 | int diaDoAno = data.getDayOfYear(); // 124 (pois 4 de maio de 2018 é o centésimo vigésimo quarto dia do ano)
95 | DayOfWeek diaDaSemana = data.getDayOfWeek(); // DayOfWeek.FRIDAY
96 | }
97 |
98 | static void mudarValores() {
99 | // 4 de maio de 2018
100 | LocalDate data = LocalDate.of(2018, 5, 4);
101 | // mudar o dia do mês para 1 (a variável "data" continua sendo dia 4)
102 | LocalDate primeiroDeMaio = data.withDayOfMonth(1);
103 | System.out.println(primeiroDeMaio); // 2018-05-01
104 | // "data" continua sendo dia 4
105 | System.out.println(data); // 2018-05-04
106 |
107 | // 2018-05-04T17:00:35.123
108 | LocalDateTime dataHora = LocalDateTime.of(2018, 5, 4, 17, 0, 35, 123000000);
109 | // mudar para 2015-05-01T10:30:35.123
110 | LocalDateTime dataModificada = dataHora
111 | // muda o dia do mês para 1
112 | .withDayOfMonth(1)
113 | // muda o ano para 2015
114 | .withYear(2015)
115 | // mudar hora para 10
116 | .withHour(10)
117 | // mudar minuto para 30
118 | .withMinute(30);
119 |
120 | System.out.println(dataModificada); // 2015-05-01T10:30:35.123
121 | // "dataHora" permanece inalterada
122 | System.out.println(dataHora); // 2018-05-04T17:00:35.123
123 | }
124 |
125 | static void usarTemporalAdjuster() {
126 | // 4 de maio de 2018
127 | LocalDate data = LocalDate.of(2018, Month.MAY, 4);
128 | // mudar para o último dia do mês
129 | LocalDate ultimoDiaDeMaio = data.with(lastDayOfMonth());
130 | System.out.println(ultimoDiaDeMaio); // 2018-05-31
131 |
132 | // -----------------------------------------------------------
133 | // outros adjusters
134 | System.out.println(data.with(firstDayOfMonth())); // 2018-05-01
135 | System.out.println(data.with(firstDayOfNextMonth())); // 2018-06-01
136 | System.out.println(data.with(firstDayOfYear())); // 2018-01-01
137 | System.out.println(data.with(firstDayOfNextYear())); // 2019-01-01
138 |
139 | // -----------------------------------------------------------
140 | // ajustes com dia da semana
141 | // próxima Sexta-feira: 2018-05-11
142 | LocalDate proximaSexta = data.with(next(DayOfWeek.FRIDAY));
143 |
144 | // terceira Quinta-feira do mês: 2018-05-17
145 | LocalDate terceiraQuinta = data.with(dayOfWeekInMonth(3, DayOfWeek.THURSDAY));
146 |
147 | // primeiro Sábado do mês: 2018-05-05
148 | LocalDate primeiroSabado = data.with(firstInMonth(DayOfWeek.SATURDAY));
149 |
150 | // último Sábado do mês: 2018-05-26
151 | LocalDate ultimoSabado = data.with(lastInMonth(DayOfWeek.SATURDAY));
152 |
153 | try {
154 | // LocalTime não tem dia, então ajustar para o primeiro dia do mês lança exceção
155 | LocalTime horario = LocalTime.now().with(firstDayOfMonth());
156 | } catch (UnsupportedTemporalTypeException e) {
157 | System.out.println(e.getMessage()); // Unsupported field: DayOfMonth
158 | }
159 | }
160 |
161 | static void ajustesComAsClassesLocais() {
162 | // 2018-05-04T10:00:35.123456
163 | LocalDateTime dataHora = LocalDateTime.of(2018, 5, 4, 10, 0, 35, 123456000);
164 | // mudar horário para 17:30 -> 2018-05-04T17:30
165 | dataHora = dataHora.with(LocalTime.of(17, 30)); // todos os campos de horário são mudados
166 |
167 | // -----------------------------------------------------------
168 | // 2018-05-04T00:00
169 | dataHora = LocalDateTime.of(2018, 5, 4, 0, 0);
170 | // mudar data para 1 de janeiro de 2001 -> 2001-01-01T00:00
171 | dataHora = dataHora.with(LocalDate.of(2001, 1, 1));
172 | }
173 |
174 | static void comparar() {
175 | // -----------------------------------------------------------
176 | // comparar datas (somente dia, mês e ano)
177 | // 4 de maio de 2018
178 | LocalDate maio = LocalDate.of(2018, 5, 4);
179 | // 10 de janeiro de 2018
180 | LocalDate janeiro = LocalDate.of(2018, 1, 10);
181 | boolean maioDepoisDeJaneiro = maio.isAfter(janeiro); // true
182 | boolean maioAntesDeJaneiro = maio.isBefore(janeiro); // false
183 |
184 | // -----------------------------------------------------------
185 | // comparar horários (somente hora, minuto, segundo, nanossegundo)
186 | LocalTime dezDaNoite = LocalTime.of(22, 0);
187 | LocalTime tresDaManha = LocalTime.of(3, 0);
188 | boolean antes = dezDaNoite.isBefore(tresDaManha); // false
189 |
190 | // -----------------------------------------------------------
191 | // comparar data e horário
192 | // 2018-05-04T22:00
193 | LocalDateTime dataHora1 = LocalDateTime.of(2018, 5, 4, 22, 0);
194 | // 2018-05-05T03:00
195 | LocalDateTime dataHora2 = LocalDateTime.of(2018, 5, 5, 3, 0);
196 | antes = dataHora1.isBefore(dataHora2); // true
197 | }
198 |
199 | static void verificarDatasIguais() {
200 | // somente data
201 | LocalDate data1 = LocalDate.of(2018, 5, 4);
202 | LocalDate data2 = LocalDate.of(2018, Month.MAY, 4);
203 | boolean datasIguais = data1.equals(data2); // true
204 |
205 | // -----------------------------------------------------------
206 | // LocalDate retorna false se a outra classe não for LocalDate
207 | // 2018-05-04T17:00
208 | LocalDateTime dataHora = data1.atTime(17, 0);
209 | boolean iguais = data1.equals(dataHora); // false
210 | // A comparação deve ser feita entre dois LocalDates
211 | iguais = data1.equals(dataHora.toLocalDate()); // true
212 | }
213 |
214 | static void ordenar() {
215 | List lista = Arrays.asList(LocalDate.of(2018, 5, 4), // 2018-05-04
216 | LocalDate.of(1995, 5, 4), // 1995-05-04
217 | LocalDate.of(2018, 1, 20)); // 2018-01-20
218 | Collections.sort(lista);
219 |
220 | // datas são ordenadas em ordem alfabética
221 | System.out.println(lista); // [1995-05-04, 2018-01-20, 2018-05-04]
222 | }
223 |
224 | static void outrosModosDeCriarDatas() {
225 | // Construir 2018-05-04 usando o epoch-day (quantidade de dias desde o Unix Epoch)
226 | LocalDate data = LocalDate.ofEpochDay(17655);
227 |
228 | // centésimo dia de 2018 (2018-04-10)
229 | data = LocalDate.ofYearDay(2018, 100);
230 | // Internamente, LocalDate só possui os valores do dia, mês e ano.
231 | // O epochDay e yearDay são usados para calcular estes valores, mas não fazem parte do LocalDate
232 |
233 | // -----------------------------------------------------------
234 | // centésimo segundo do dia (considerando que o dia começa à meia-noite)
235 | LocalTime time = LocalTime.ofSecondOfDay(100); // 00:01:40
236 |
237 | // centésimo nanossegundo do dia
238 | time = LocalTime.ofNanoOfDay(100); // 00:00:00.000000100
239 | }
240 | }
241 |
--------------------------------------------------------------------------------
/src/main/java/exemplos/part2/Cap07DateCalendar.java:
--------------------------------------------------------------------------------
1 | package exemplos.part2;
2 |
3 | import static exemplos.setup.Setup.clock;
4 | import static exemplos.setup.Setup.setup;
5 |
6 | import java.text.ParseException;
7 | import java.util.Calendar;
8 | import java.util.Date;
9 | import java.util.TimeZone;
10 |
11 | public class Cap07DateCalendar {
12 |
13 | static {
14 | // setar configurações, ver javadoc da classe Setup para mais detalhes
15 | setup();
16 | }
17 |
18 | public static void main(String[] args) throws ParseException {
19 | dataAtual();
20 | mudarTimezonePadrao();
21 | criarDataJaneiro();
22 | criarDataJaneiroCalendar();
23 | setarHoraAmPm();
24 | obterCamposCalendar();
25 | exemploSetarCamposComParamsErrados();
26 | mudarTimezoneDoCalendar();
27 | timezoneNaoValidaNome();
28 | calendarLeniente();
29 | calendarNaoLeniente();
30 | }
31 |
32 | static void dataAtual() {
33 | // Date contendo o instante atual
34 | Date agora = new Date();
35 | // usando nossa "data atual" simulada
36 | agora = new Date(clock().millis());
37 |
38 | // imprime a data e hora no timezone padrão da JVM (America/Sao_Paulo)
39 | System.out.println(agora); // Fri May 04 17:00:00 BRT 2018
40 | }
41 |
42 | static void mudarTimezonePadrao() {
43 | // usando nossa "data atual" simulada
44 | Date agora = new Date(clock().millis());
45 |
46 | /*
47 | * Mudar o timezone padrão faz com que seja impresso um valor diferente para data/hora.
48 | *
49 | * Mas o timestamp (getTime()) continua o mesmo.
50 | */
51 | TimeZone.setDefault(TimeZone.getTimeZone("Europe/Berlin"));
52 | // 1525464000000=Fri May 04 22:00:00 CEST 2018 - Europe/Berlin
53 | System.out.println(agora.getTime() + "=" + agora + " - " + TimeZone.getDefault().getID());
54 |
55 | TimeZone.setDefault(TimeZone.getTimeZone("Asia/Tokyo"));
56 | // 1525464000000=Sat May 05 05:00:00 JST 2018 - Asia/Tokyo
57 | System.out.println(agora.getTime() + "=" + agora + " - " + TimeZone.getDefault().getID());
58 |
59 | TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
60 | // 1525464000000=Fri May 04 20:00:00 UTC 2018 - UTC
61 | System.out.println(agora.getTime() + "=" + agora + " - " + TimeZone.getDefault().getID());
62 |
63 | // voltar config default (para não impactar os outros métodos)
64 | setup();
65 | }
66 |
67 | static void criarDataJaneiro() {
68 | // tentando criar 10 de janeiro de 2018
69 | Date janeiroErrado = new Date(2018, 1, 10); // construtor deprecated
70 | // data criada corresponde a fevereiro de 3918
71 | System.out.println(janeiroErrado); // Sun Feb 10 00:00:00 BRST 3918
72 |
73 | // ano = 2018 - 1900, mês = começa em zero (janeiro = 0, fevereiro = 1, etc)
74 | Date janeiro = new Date(118, 0, 10);
75 | // agora sim é 10 de janeiro de 2018
76 | System.out.println(janeiro); // Wed Jan 10 00:00:00 BRST 2018
77 |
78 | // criando com Calendar.JANUARY (é uma constante cujo valor é zero, apenas minimiza a chance de erro)
79 | janeiro = new Date(118, Calendar.JANUARY, 10);
80 | System.out.println(janeiro); // Wed Jan 10 00:00:00 BRST 2018
81 | }
82 |
83 | static void criarDataJaneiroCalendar() {
84 | // setando campos com Calendar
85 | // cria um Calendar
86 | Calendar cal = Calendar.getInstance();
87 | // setar timestamp para nossa "data atual" simulada (se quiser a data atual de fato, remova esta linha, pois getInstance já retorna um Calendar com o
88 | // timestamp atual)
89 | cal.setTimeInMillis(clock().millis());
90 |
91 | // muda o ano para 2018, o mês para janeiro e o dia do mês para 10
92 | cal.set(Calendar.YEAR, 2018);
93 | cal.set(Calendar.MONTH, Calendar.JANUARY);
94 | cal.set(Calendar.DAY_OF_MONTH, 10);
95 | System.out.println(cal); // imprime várias linhas (java.util.GregorianCalendar[ etc etc ...])
96 |
97 | // obter um java.util.Date a partir do Calendar
98 | Date date = cal.getTime();
99 | // se vc não usar a "data atual" simulada, este valor poderá ser diferente, pois será usada a hora atual em vez de 17:00
100 | // o mesmo vale para o valor do timestamp, pois mudando o horário, também muda o timestamp
101 | System.out.println(date); // Wed Jan 10 17:00:00 BRST 2018
102 |
103 | // obter o valor do timestamp (todos serão 1515610800000)
104 | long timestamp1 = date.getTime();
105 | // outra maneira de obter o timestamp, sem precisar criar o Date
106 | long timestamp2 = cal.getTimeInMillis();
107 | // também funciona, mas fica meio confuso (prefira usar getTimeInMillis())
108 | long timestamp3 = cal.getTime().getTime();
109 | System.out.println(timestamp1);
110 | System.out.println(timestamp2);
111 | System.out.println(timestamp3);
112 |
113 | // se quiser, pode mudar o horário para meia-noite (isso muda o Date e o timestamp)
114 | cal.set(Calendar.HOUR_OF_DAY, 0);
115 | cal.set(Calendar.MINUTE, 0);
116 | cal.set(Calendar.SECOND, 0);
117 | cal.set(Calendar.MILLISECOND, 0);
118 |
119 | // -------------------------------------------------------------
120 | // outras opções para mudar os dados do Calendar
121 | // muda somente o ano, mês e dia (o horário não é mudado)
122 | cal.set(2018, Calendar.JANUARY, 10);
123 |
124 | // muda os campos para 10 de janeiro de 2018, meia-noite
125 | cal.set(2018, Calendar.JANUARY, 10, 0, 0, 0);
126 | // não se esqueça de mudar os milissegundos também
127 | cal.set(Calendar.MILLISECOND, 0);
128 | }
129 |
130 | static void setarHoraAmPm() {
131 | // cria um Calendar
132 | Calendar cal = Calendar.getInstance();
133 | // setar timestamp para nossa "data atual" simulada (se quiser a data atual de fato, remova esta linha)
134 | cal.setTimeInMillis(clock().millis());
135 |
136 | // mudar o horário para 17:00 (5 PM)
137 | cal.set(Calendar.AM_PM, Calendar.PM);
138 | cal.set(Calendar.HOUR, 5);
139 |
140 | System.out.println(cal.getTime()); // Fri May 04 17:00:00 BRT 2018
141 | }
142 |
143 | static void obterCamposCalendar() {
144 | // cria um Calendar
145 | Calendar cal = Calendar.getInstance();
146 | // setar timestamp para nossa "data atual" simulada (se quiser a data atual de fato, remova esta linha)
147 | cal.setTimeInMillis(clock().millis());
148 |
149 | // valor numérico do dia
150 | int dia = cal.get(Calendar.DAY_OF_MONTH);
151 | // valor numérico do mês - janeiro é 0, fevereiro é 1 etc.
152 | int mes = cal.get(Calendar.MONTH);
153 | System.out.println(dia); // 4
154 | System.out.println(mes); // 4 ("maio")
155 | }
156 |
157 | static void exemploSetarCamposComParamsErrados() {
158 | // data/hora atual: 2018-05-04T17:00-03:00 (America/Sao_Paulo)
159 | Calendar cal = Calendar.getInstance();
160 | // setar timestamp para nossa "data atual" simulada (se quiser a data atual de fato, remova esta linha)
161 | cal.setTimeInMillis(clock().millis());
162 |
163 | // tentar mudar o mês para outubro: parâmetros na ordem ERRADA (estão invertidos)
164 | // isso compila normalmente, pois as constantes são todas int
165 | // mas o mês não será mudado para outubro
166 | cal.set(Calendar.OCTOBER, Calendar.MONTH);
167 |
168 | // imprime a data, para ver se fizemos tudo certo (spoiler: não fizemos)
169 | System.out.println(cal.getTime()); // Sat May 05 05:00:00 BRT 2018
170 |
171 | // obter o dia da semana: usar um valor (FRIDAY) ao invés de um campo (DAY_OF_WEEK)
172 | System.out.println(cal.get(Calendar.FRIDAY)); // 125
173 | // Calendar.FRIDAY tem o valor 6, que é o mesmo valor do campo Calendar.DAY_OF_YEAR (dia do ano)
174 | // como a data do Calendar é 5 de maio de 2018, este é o 125º dia do ano
175 | }
176 |
177 | static void mudarTimezoneDoCalendar() {
178 | // cria um Calendar
179 | Calendar cal = Calendar.getInstance();
180 | // setar timestamp para nossa "data atual" simulada (se quiser a data atual de fato, remova esta linha)
181 | cal.setTimeInMillis(clock().millis());
182 |
183 | // imprimir hora e timestamp
184 | System.out.println(cal.get(Calendar.HOUR_OF_DAY)); // 17
185 | System.out.println(cal.getTimeInMillis()); // 1525464000000
186 |
187 | // mudar timezone: horário muda, mas o timestamp continua o mesmo
188 | cal.setTimeZone(TimeZone.getTimeZone("Europe/Berlin"));
189 | System.out.println(cal.get(Calendar.HOUR_OF_DAY)); // 22
190 | System.out.println(cal.getTimeInMillis()); // 1525464000000
191 |
192 | // outra opção é criar o Calendar no timezone desejado
193 | cal = Calendar.getInstance(TimeZone.getTimeZone("Europe/Berlin"));
194 | }
195 |
196 | static void timezoneNaoValidaNome() {
197 | // nome errado ("Pualo" em vez de "Paulo")
198 | System.out.println(TimeZone.getTimeZone("America/Sao_Pualo"));
199 | // retorna um timezone que equivale a UTC:
200 | // sun.util.calendar.ZoneInfo[id="GMT",offset=0,dstSavings=0,useDaylight=false,transitions=0,lastRule=null]
201 | // ou seja, se houver erro de digitação, não dará erro na hora, e você só vai perceber quando ver que as datas/horas estão erradas
202 |
203 | // -----------------------------------------------------------------
204 | // o jeito é verificar se o nome está na lista de timezones válidos
205 | for (String zone : TimeZone.getAvailableIDs()) {
206 | if ("America/Sao_Paulo".equals(zone)) {
207 | // OK, timezone America/Sao_Paulo existe
208 | break;
209 | }
210 | }
211 |
212 | // outro jeito é ver se o ID é o mesmo (quando o nome é inválido, é criado um timezone com nome "GMT")
213 | TimeZone tz = TimeZone.getTimeZone("America/Sao_Pualo");
214 | if ("America/Sao_Paulo".equals(tz.getID())) {
215 | // OK, timezone America/Sao_Paulo criado com sucesso
216 | } else {
217 | // ID diferente, porque o nome estava errado e foi criado um timezone com nome "GMT"
218 | }
219 | }
220 |
221 | static void calendarLeniente() {
222 | // cria um Calendar
223 | Calendar cal = Calendar.getInstance();
224 | // setar timestamp para nossa "data atual" simulada (se quiser a data atual de fato, remova esta linha)
225 | cal.setTimeInMillis(clock().millis()); // 4 de Maio de 2018, às 17:00 em São Paulo.
226 |
227 | // mudar o dia do mês para 33
228 | cal.set(Calendar.DAY_OF_MONTH, 33); // 33 de maio é ajustado para 2 de junho
229 | System.out.println(cal.getTime()); // Sat Jun 02 17:00:00 BRT 2018
230 | }
231 |
232 | static void calendarNaoLeniente() {
233 | // cria um Calendar
234 | Calendar cal = Calendar.getInstance();
235 | // setar timestamp para nossa "data atual" simulada (se quiser a data atual de fato, remova esta linha)
236 | cal.setTimeInMillis(clock().millis());
237 |
238 | // desligar modo leniente
239 | cal.setLenient(false);
240 | try {
241 | // mudar o dia do mês para 33
242 | cal.set(Calendar.DAY_OF_MONTH, 33);
243 | System.out.println(cal.getTime()); // java.lang.IllegalArgumentException: DAY_OF_MONTH
244 | } catch (IllegalArgumentException e) {
245 | System.out.println("Erro ao setar dia 33: " + e);
246 | }
247 | }
248 | }
249 |
--------------------------------------------------------------------------------
/src/main/java/exemplos/part3/Cap16AritmeticaDatas.java:
--------------------------------------------------------------------------------
1 | package exemplos.part3;
2 |
3 | import static exemplos.setup.Setup.setup;
4 |
5 | import java.time.Duration;
6 | import java.time.LocalDate;
7 | import java.time.LocalDateTime;
8 | import java.time.LocalTime;
9 | import java.time.Period;
10 | import java.time.ZoneId;
11 | import java.time.ZonedDateTime;
12 | import java.time.format.DateTimeParseException;
13 | import java.time.temporal.ChronoUnit;
14 | import java.time.temporal.UnsupportedTemporalTypeException;
15 |
16 | public class Cap16AritmeticaDatas {
17 |
18 | static {
19 | // setar configurações, ver javadoc da classe Setup para mais detalhes
20 | setup();
21 | }
22 |
23 | public static void main(String[] args) {
24 | somaSubtracao();
25 | somarQualquerUnidadeDeTempo();
26 | testarPeriod();
27 | somarPeriods();
28 | testarDuration();
29 | somarDuracoesDatas();
30 | quantasHorasTemUmDia();
31 | diferencaEntreDatas();
32 | aritmeticaDeDatasEstranha();
33 | }
34 |
35 | static void somaSubtracao() {
36 | // 2018-05-04T17:00
37 | LocalDateTime dataHora = LocalDateTime.of(2018, 5, 4, 17, 0);
38 |
39 | // somar 1 dia
40 | LocalDateTime diaSeguinte = dataHora.plusDays(1);
41 | System.out.println(diaSeguinte); // 2018-05-05T17:00
42 |
43 | // subtrair 3 horas
44 | LocalDateTime tresHorasAntes = dataHora.minusHours(3);
45 | System.out.println(tresHorasAntes); // 2018-05-04T14:00
46 |
47 | // podemos somar valores negativos (o que equivale à subtração)
48 | System.out.println(dataHora.plusHours(-2).equals(dataHora.minusHours(2))); // true
49 |
50 | // podem ser feitos ajustes, dependendo do caso
51 | // 2018-01-31
52 | LocalDate jan = LocalDate.of(2018, 1, 31);
53 | LocalDate fev = jan.plusMonths(1);
54 | System.out.println(fev); // 2018-02-28
55 | // Ao somar 1 mês a 31 de janeiro de 2018, o resultado seria 31 de fevereiro. Mas como fevereiro não tem 31 dias, o resultado é ajustado para o último
56 | // dia válido deste mês. Por isso o valor final é 28 de fevereiro de 2018
57 |
58 | // ajustes devido ao timezone
59 | // 2017-10-14T10:00-03:00[America/Sao_Paulo] (um dia antes de começar o horário de verão)
60 | ZonedDateTime z = ZonedDateTime.of(2017, 10, 14, 10, 0, 0, 0, ZoneId.of("America/Sao_Paulo"));
61 | // somar 1 dia, resultado é o mesmo horário do dia seguinte (offset muda porque começou horário de verão)
62 | System.out.println(z.plusDays(1)); // 2017-10-15T10:00-02:00[America/Sao_Paulo]
63 | // somar 24 horas, resultado não é o mesmo horário, porque quando começa o horário de verão, adianta-se o relógio 1 hora (ou seja, 1 hora é "pulada")
64 | System.out.println(z.plusHours(24)); // 2017-10-15T11:00-02:00[America/Sao_Paulo]
65 | }
66 |
67 | static void somarQualquerUnidadeDeTempo() {
68 | // 10:30:00.123456789
69 | LocalTime hora = LocalTime.of(10, 30, 0, 123456789);
70 | // somar 200 milissegundos (é o mesmo que hora.plusNanos(200000000), mas sem precisar fazer a conta para converter milissegundos para nanossegundos)
71 | hora = hora.plus(200, ChronoUnit.MILLIS);
72 | System.out.println(hora); // 10:30:00.323456789
73 |
74 | // não some unidades que a classe não suporta
75 | try {
76 | LocalTime lt = LocalTime.now().plus(1, ChronoUnit.MONTHS);
77 | System.out.println(lt); // não será executado, pois LocalTime não suporta ChronoUnit.MONTHS e lança exceção
78 | } catch (UnsupportedTemporalTypeException e) {
79 | System.out.println(e.getMessage()); // Unsupported unit: Months
80 | }
81 | // é melhor verificar antes se a classe suporta uma unidade antes de somá-la (ou subtraí-la)
82 | hora = LocalTime.now();
83 | if (hora.isSupported(ChronoUnit.MONTHS)) {
84 | // não entra neste if, pois LocalTime não suporta ChronoUnit.MONTHS e isSupported retorna false
85 | hora = hora.plus(1, ChronoUnit.MONTHS);
86 | }
87 | }
88 |
89 | static void testarPeriod() {
90 | // 2 jeitos de criar um período de "1 ano, 2 meses e 20 dias"
91 | System.out.println(Period.parse("P1Y2M20D")); // P1Y2M20D
92 | System.out.println(Period.of(1, 2, 20)); // P1Y2M20D
93 |
94 | // semanas são automaticamente convertida em dias
95 | System.out.println(Period.ofWeeks(2)); // P14D
96 |
97 | // Period tem somente anos, meses e dias
98 | try {
99 | // período com campo de horas lança exceção
100 | Period.parse("P1DT1H");
101 | } catch (DateTimeParseException e) {
102 | System.out.println(e.getMessage()); // Text cannot be parsed to a Period
103 | }
104 | }
105 |
106 | static void somarPeriods() {
107 | // somar 40 dias ao periodo de 1 mês
108 | Period period = Period.ofMonths(1);
109 | period = period.plusDays(40);
110 | System.out.println(period); // P1M40D
111 |
112 | // 1 ano e 3 meses
113 | period = Period.parse("P1Y3M");
114 | // somar um Period de 5 meses e 10 dias
115 | period = period.plus(Period.parse("P5M10D"));
116 | System.out.println(period); // P1Y8M10D
117 |
118 | // obter os campos separadamente
119 | System.out.println(period.getYears()); // 1
120 | System.out.println(period.getMonths()); // 8
121 | System.out.println(period.getDays()); // 10
122 | }
123 |
124 | static void testarDuration() {
125 | // 2 modos de criar uma duração de 10 minutos
126 | System.out.println(Duration.parse("PT10M")); // PT10M
127 | System.out.println(Duration.ofMinutes(10)); // PT10M
128 |
129 | // várias formas de somar valores a um Duration
130 | Duration duracao = Duration
131 | .ofSeconds(3) // 3 segundos
132 | // somar 10 minutos (usando o valor numérico)
133 | .plusMinutes(10)
134 | // somar 0.5 segundos (usando outro Duration)
135 | .plus(Duration.ofMillis(500))
136 | // somar 3 horas (usando ChronoUnit)
137 | .plus(3, ChronoUnit.HOURS);
138 | System.out.println(duracao); // PT3H10M3.5S
139 | // Internamente, Duration só guarda segundos e nanossegundos
140 | System.out.println(duracao.getSeconds()); // 11403
141 | System.out.println(duracao.getNano()); // 500000000
142 | // Ao imprimir o Duration usando println, o método toString() converte para PT3H10M3.5S (mas internamente, há somente segundos e nanossegundos)
143 |
144 | // *****************************************************
145 | // Obter os valores numéricos das horas e minutos
146 |
147 | // jeito errado
148 | System.out.println(duracao.toHours()); // 3 (certo)
149 | System.out.println(duracao.toMinutes()); // 190 (errado)
150 | // toMinutes converte a duração em minutos -> PT3H10M3.5S = 3 horas, 10 minutos e 3.5 segundos = 190 minutos (arredondado, desconsidera os segundos)
151 |
152 | // jeito certo: é necessário fazer as contas manualmente
153 | // obter o total de segundos
154 | long segundos = duracao.getSeconds();
155 | // obter a quantidade de dias
156 | long dias = segundos / (24 * 3600);
157 | segundos -= dias * 24 * 3600; // descontar os dias do total
158 | // repetir o mesmo para horas e minutos
159 | long horas = segundos / 3600;
160 | segundos -= horas * 3600;
161 | long minutos = segundos / 60;
162 | segundos -= minutos * 60;
163 | // 0 dias, 3 horas, 10 minutos, 3.500000000 segundos
164 | System.out.format("%d dias, %d horas, %d minutos, %d.%d segundos\n", dias, horas, minutos, segundos, duracao.getNano());
165 | // A partir do Java 9, há os métodos toMinutesPart(), toHoursPart(), etc, e esses cálculos não são mais necessários
166 | // veja a documentação -> https://docs.oracle.com/javase/9/docs/api/java/time/Duration.html
167 | }
168 |
169 | static void somarDuracoesDatas() {
170 | // 3 horas, 10 minutos e 3.5 segundos
171 | Duration duracao = Duration.parse("PT3H10M3.5S");
172 | // 1 mês e 10 dias
173 | Period periodo = Period.parse("P1M10D");
174 | // 2018-05-04T17:00
175 | LocalDateTime dataHora = LocalDateTime.of(2018, 5, 4, 17, 0);
176 | dataHora = dataHora
177 | // somar o Duration -> 2018-05-04T20:10:03.500
178 | .plus(duracao)
179 | // subtrair o Period -> 2018-03-25T20:10:03.500
180 | .minus(periodo);
181 | System.out.println(dataHora); // 2018-03-25T20:10:03.500
182 |
183 | try {
184 | // LocalTime só tem campos de horário (hora, minuto, segundo e nanossegundo)
185 | // Period só tem anos, meses e dias (campos não suportados por LocalTime)
186 | LocalTime.now().plus(periodo); // por isso esta soma lança exceção
187 | } catch (UnsupportedTemporalTypeException e) {
188 | System.out.println(e.getMessage()); // Unsupported unit: Months
189 | }
190 | // verificar se uma classe suporta um Duration (também funciona com Period, pois também tem o método getUnits())
191 | boolean suportaDuracao = Duration
192 | .parse("PT1H30M4.5S")
193 | // obter as TemporalUnits da duracao
194 | .getUnits()
195 | // verificar se todas são suportadas pela data
196 | .stream()
197 | .allMatch(dataHora::isSupported);
198 | System.out.println(suportaDuracao); // true
199 | }
200 |
201 | static void quantasHorasTemUmDia() {
202 | ZoneId zone = ZoneId.of("America/Sao_Paulo");
203 | // 2017-10-14T10:00-03:00[America/Sao_Paulo] (um dia antes de começar o horário de verão)
204 | ZonedDateTime z = ZonedDateTime.of(2017, 10, 14, 10, 0, 0, 0, zone);
205 |
206 | // somar Period de 1 dia é o mesmo que plusDays(1) -> resulta na mesma hora do dia seguinte (offset é ajustado)
207 | ZonedDateTime mais1dia = z.plus(Period.ofDays(1));
208 | System.out.println(mais1dia); // 2017-10-15T10:00-02:00[America/Sao_Paulo]
209 | // diferença em horas
210 | System.out.println(ChronoUnit.HOURS.between(z, mais1dia)); // 23
211 |
212 | // somar Duration de 1 dia é o mesmo que plusHours(24) -> o resultado não é na mesma hora devido ao horário de verão que pulou 1 hora
213 | ZonedDateTime mais24horas = z.plus(Duration.ofDays(1));
214 | System.out.println(mais24horas); // 2017-10-15T11:00-02:00[America/Sao_Paulo]
215 | // diferença em horas
216 | System.out.println(ChronoUnit.HOURS.between(z, mais24horas)); // 24
217 | }
218 |
219 | static void diferencaEntreDatas() {
220 | // obter diferença como um valor numérico
221 | LocalDate dtInicio = LocalDate.of(2018, 1, 1);
222 | LocalDate dtFim = LocalDate.of(2018, 1, 10);
223 | // quantos dias entre 1 e 10 de janeiro
224 | long dias = ChronoUnit.DAYS.between(dtInicio, dtFim);
225 | System.out.println(dias); // 9
226 |
227 | // between() sempre arredonda para baixo
228 | // 2018-01-01T11:00
229 | LocalDateTime inicio = LocalDateTime.of(2018, 1, 1, 11, 0);
230 | // 2018-01-02T10:59:59.999999999 (falta 1 nanossegundo para completar 1 dia, mesmo assim, between() retorna zero)
231 | LocalDateTime fim = LocalDateTime.of(2018, 1, 2, 10, 59, 59, 999999999);
232 | dias = ChronoUnit.DAYS.between(inicio, fim);
233 | System.out.println(dias); // 0
234 |
235 | // calcular diferença, ignorando o horário
236 | dias = ChronoUnit.DAYS.between(inicio.toLocalDate(), fim.toLocalDate());
237 | System.out.println(dias); // 1
238 |
239 | try {
240 | // LocalDate não tem minutos, então MINUTES.between lança exceção
241 | long minutos = ChronoUnit.MINUTES.between(dtInicio, dtFim);
242 | System.out.println(minutos); // não será executado
243 | } catch (UnsupportedTemporalTypeException e) {
244 | System.out.println(e.getMessage()); // Unsupported unit: Minutes
245 | }
246 |
247 | // obter diferença como Period ou Duration
248 | // 2018-01-01T11:00
249 | inicio = LocalDateTime.of(2018, 1, 1, 11, 0);
250 | // 2018-02-10T10:00
251 | fim = LocalDateTime.of(2018, 2, 10, 10, 0);
252 | // Period só tem anos, meses e dias, e o método between só aceita LocalDate (portanto, não considera os horários)
253 | Period periodo = Period.between(inicio.toLocalDate(), fim.toLocalDate());
254 | System.out.println(periodo); // P1M9D - 1 mês e 9 dias (diferença entre as datas, sem considerar os horários)
255 | // Duration considera os horários, e converte a diferença para o total de segundos e nanossegundos
256 | Duration duracao = Duration.between(inicio, fim);
257 | System.out.println(duracao); // PT959H - 959 horas (diferença considerando os horários)
258 |
259 | // a diferença pode ser negativa
260 | dtInicio = LocalDate.of(2018, 1, 1);
261 | dtFim = LocalDate.of(2018, 1, 10);
262 | // quantos dias entre 10 e 1 de janeiro
263 | dias = ChronoUnit.DAYS.between(dtFim, dtInicio);
264 | System.out.println(dias); // -9
265 | // período de -9 dias (P-9D)
266 | periodo = Period.between(dtFim, dtInicio);
267 | System.out.println(periodo); // P-9D
268 |
269 | // se você só quiser o valor absoluto, pode inverter o sinal
270 | if (periodo.isNegative()) {
271 | periodo = periodo.negated();
272 | System.out.println(periodo); // P9D
273 | }
274 | }
275 |
276 | static void aritmeticaDeDatasEstranha() {
277 | // 2016-02-29
278 | LocalDate data = LocalDate.of(2016, 2, 29);
279 | // somar 1 ano, o resultado seria 29 de fevereiro, mas como 2017 não é bissexto, o dia é ajustado para 2017-02-28
280 | LocalDate umAnoDepois = data.plusYears(1);
281 |
282 | // se eu somei 1 ano, as diferenças abaixo devem ser de 1 ano, certo? Errado!
283 | System.out.println(ChronoUnit.YEARS.between(data, umAnoDepois)); // 0
284 | System.out.println(Period.between(data, umAnoDepois)); // P11M30D
285 | // Mas o resultado não deveria ser 1 ano? Depende. Basta imaginar uma pessoa que nasceu em 29 de fevereiro de 2016. Em 28 de fevereiro de 2017 ela já
286 | // terá completado 1 ano? A API entende que não (e não há uma regra oficial para isso, cada API implementa de um jeito)
287 | }
288 | }
289 |
--------------------------------------------------------------------------------
/src/main/java/exemplos/part3/Cap19TestesOutrosCasos.java:
--------------------------------------------------------------------------------
1 | package exemplos.part3;
2 |
3 | import static exemplos.setup.Setup.setup;
4 |
5 | import java.time.Clock;
6 | import java.time.Instant;
7 | import java.time.LocalDate;
8 | import java.time.LocalDateTime;
9 | import java.time.LocalTime;
10 | import java.time.OffsetDateTime;
11 | import java.time.ZoneId;
12 | import java.time.ZoneOffset;
13 | import java.time.ZonedDateTime;
14 | import java.time.format.DateTimeFormatter;
15 | import java.time.temporal.ChronoField;
16 | import java.time.temporal.ChronoUnit;
17 | import java.time.temporal.TemporalAdjuster;
18 | import java.time.zone.ZoneOffsetTransition;
19 | import java.time.zone.ZoneRules;
20 | import java.util.HashSet;
21 | import java.util.Locale;
22 | import java.util.Set;
23 |
24 | import exemplos.part2.Cap12Others;
25 | import exemplos.setup.Setup;
26 |
27 | public class Cap19TestesOutrosCasos {
28 |
29 | static {
30 | // setar configurações, ver javadoc da classe Setup para mais detalhes
31 | setup();
32 | }
33 |
34 | /**
35 | * Para os exemplos de cálculo de idade, veja a classe {@link CalculaIdade} e os seus unit tests em {@link CalculaIdadeTest}
36 | */
37 | public static void main(String[] args) {
38 | usoBasicoDoClock();
39 | obterInformacoesTimezone();
40 | obterInformacoesTimezone2();
41 | compararTimezones();
42 | encontrarTimezonesPorOffsetOuAbreviacao();
43 | criarTemporalAdjuster();
44 | }
45 |
46 | static void usoBasicoDoClock() {
47 | // -------------------------------------------------
48 | // Usar um Clock fixo (sempre retorna o mesmo valor para o instante atual)
49 |
50 | // 4 de maio de 2018, às 17:00 em São Paulo
51 | ZonedDateTime agoraFixo = ZonedDateTime.parse("2018-05-04T17:00-03:00[America/Sao_Paulo]");
52 | // criar o Clock fixo
53 | Clock clock = Clock.fixed(
54 | // Instant que corresponde ao timestamp atual
55 | agoraFixo.toInstant(), // 2018-05-04T20:00:00Z
56 | // timezone usado para converter o timestamp atual em data, hora e offset
57 | agoraFixo.getZone()); // America/Sao_Paulo
58 | // o método now() obtém o instante atual do Clock e converte para data/hora/offset usando o ZoneId do Clock
59 |
60 | System.out.println(LocalDate.now(clock)); // 2018-05-04
61 | System.out.println(LocalTime.now(clock)); // 17:00
62 | System.out.println(OffsetDateTime.now(clock)); // 2018-05-04T17:00-03:00
63 | System.out.println(Instant.now(clock)); // 2018-05-04T20:00:00Z
64 |
65 | // para mudar o timezone do Clock
66 | clock = clock.withZone(ZoneId.of("Asia/Tokyo"));
67 | // o Instant atual é o mesmo, mas agora ele é convertido para o timezone de Tóquio, alterando os valores de data, hora e offset
68 | System.out.println(OffsetDateTime.now(clock)); // 2018-05-05T05:00+09:00
69 | System.out.println(Instant.now(clock)); // 2018-05-04T20:00:00Z <- o instante atual é o mesmo
70 |
71 | // usar o relógio do sistema e o timezone default da JVM
72 | clock = Clock.systemDefaultZone();
73 | System.out.println(OffsetDateTime.now(clock)); // vai mostrar a data, hora e offset atual da máquina onde a JVM está rodando
74 |
75 | // usar o relógio do sistema e um timezone específico (em vez do default da JVM)
76 | clock = Clock.system(ZoneId.of("Europe/London"));
77 | System.out.println(OffsetDateTime.now(clock)); // vai mostrar a data, hora e offset atual da máquina onde a JVM está rodando
78 | // mas a data e hora serão convertidas para o timezone indicado
79 | }
80 |
81 | static void obterInformacoesTimezone() {
82 | ZoneRules rules = ZoneId.of("America/Sao_Paulo").getRules();
83 | rules.getTransitions().forEach(System.out::println); // mostra várias linhas com as mudanças de offset. Ex:
84 | // Transition[Gap at 2009-10-18T00:00-03:00 to -02:00]
85 | // Transition[Overlap at 2010-02-21T00:00-02:00 to -03:00]
86 | // Estas linhas indicam quando houve um gap ou overlap
87 |
88 | // -------------------------------------------------
89 | // obtendo uma transição qualquer (apenas para vermos em detalhes)
90 | ZoneOffsetTransition transition = rules.getTransitions().get(30);
91 | // pode variar de acordo com a versão da JVM ou do TZDB que está instalado, mas nos meus testes a saída foi a transição de janeiro de 1989
92 | System.out.println(transition); // Transition[Overlap at 1989-01-29T00:00-02:00 to -03:00]
93 | System.out.println(transition.isGap()); // false
94 | System.out.println(transition.isOverlap()); // true
95 | System.out.println(transition.isValidOffset(ZoneOffset.ofHours(-3))); // true (offset válido para esta transição)
96 | System.out.println(transition.isValidOffset(ZoneOffset.ofHours(+7))); // false (offset inválido para esta transição)
97 |
98 | // -------------------------------------------------
99 | // aqui podemos ver que à meia-noite os relógios foram atrasados em uma hora
100 | // data/hora e offset antes da transição
101 | System.out.println(transition.getDateTimeBefore()); // 1989-01-29T00:00 (meia-noite, hora em que o relógio foi atrasado)
102 | System.out.println(transition.getOffsetBefore()); // -02:00
103 | // data/hora e offset depois da transição
104 | System.out.println(transition.getDateTimeAfter()); // 1989-01-28T23:00
105 | System.out.println(transition.getOffsetAfter()); // -03:00
106 |
107 | // -------------------------------------------------
108 | // instante (UTC) exato em que ocorre a mudança de offset
109 | System.out.println(transition.getInstant()); // 1989-01-29T02:00:00Z
110 | // duração (no caso, o relógio foi atrasado em uma hora, por isso o valor negativo)
111 | System.out.println(transition.getDuration()); // PT-1H (menos uma hora, indicando que o relógio foi atrasado em uma hora)
112 | }
113 |
114 | static void obterInformacoesTimezone2() {
115 | ZoneRules rules = ZoneId.of("America/Sao_Paulo").getRules();
116 |
117 | // próxima mudança de offset (ou seja, quando vai começar/terminar o horário de verão?)
118 | // usei o Setup.clock() para ter a data/hora simulada (4 de Maio de 2018, às 17:00 em São Paulo)
119 | // mas para usar o instante atual, use Instant.now()
120 | Instant instant = Instant.now(Setup.clock());
121 | ZoneOffsetTransition proxima = rules.nextTransition(instant);
122 | // A JVM que estou usando tem a versão 2018e do TZDB (https://www.iana.org/time-zones), que já tem a nova regra do horário de verão brasileiro (começa
123 | // no primeiro domingo de novembro), por isso a transição abaixo ocorre em 4 de novembro. Se sua JVM possui as regras antigas, a saída será o terceiro
124 | // domingo de outubro
125 | System.out.println(proxima); // Transition[Gap at 2018-11-04T00:00-03:00 to -02:00]
126 | // também é possível usar previousTransition para obter a transição anterior ao Instant
127 |
128 | // ------------------------------
129 | // outros métodos úteis de ZoneRules
130 |
131 | // obtém o offset usado pelo timezone no Instant indicado
132 | System.out.println(rules.getOffset(instant)); // -03:00
133 | // o Instant corresponde a um instante em que o timezone está em horário de verão?
134 | System.out.println(rules.isDaylightSavings(instant)); // false
135 | // Quando está em horário de verão, retorna um Duration com a diferença para o horário "normal" (ou seja, para a grande maioria dos lugares, é 1 hora)
136 | // Apesar de ter alguns lugares que adiantavam apenas meia-hora durante o horário de verão, como Montevidéu em 1968:
137 | // https://www.timeanddate.com/time/zone/uruguay/montevideo?year=1968
138 | // Como America/Sao_Paulo não está em horário de verão no Instant indicado, o retorno é uma Duration com valor igual a zero
139 | System.out.println(rules.getDaylightSavings(instant)); // PT0S
140 |
141 | // retorna os offsets válidos em determinada data/hora
142 | LocalDateTime dt = LocalDateTime.parse("2009-10-18T00:30");
143 | System.out.println(rules.getValidOffsets(dt)); // data/hora está em um gap, lista de offsets válidos é vazia
144 | dt = LocalDateTime.parse("2010-02-20T23:30");
145 | System.out.println(rules.getValidOffsets(dt)); // [-02:00, -03:00] -> data/hora está em um overlap, há dois offsets válidos
146 | dt = LocalDateTime.parse("2010-05-01T10:30");
147 | System.out.println(rules.getValidOffsets(dt)); // [-03:00] -> data/hora está em horário normal, apenas um offset válido
148 | dt = LocalDateTime.parse("2010-01-01T10:30");
149 | System.out.println(rules.getValidOffsets(dt)); // [-02:00] -> data/hora está em horário de verão, apenas um offset válido
150 |
151 | // verifica se o timezone tem offset fixo (a maioria não tem, pois quase todos os lugarem têm ou já tiveram horário de verão ou usavam um offset
152 | // diferente em algum momento da história)
153 | System.out.println(rules.isFixedOffset()); // false
154 | // os que têm offset fixo são UTC e os ... offset fixos :/
155 | System.out.println(ZoneOffset.UTC.getRules().isFixedOffset()); // true
156 | System.out.println(ZoneOffset.of("+05:00").getRules().isFixedOffset()); // true
157 | }
158 |
159 | static void compararTimezones() {
160 | // segundo arquivo de backward (https://github.com/eggert/tz/blob/2018e/backward#L60) America/Sao_Paulo e Brazil/East são sinônimos
161 | ZoneId sp = ZoneId.of("America/Sao_Paulo");
162 | ZoneId br = ZoneId.of("Brazil/East");
163 | System.out.println(sp.equals(br)); // false <- equals compara o ID (a String correspondente ao nome)
164 |
165 | // para saber se 2 timezones são iguais, devemos comparar o histórico de offsets (ou seja, o ZoneRules)
166 | System.out.println(sp.getRules().equals(br.getRules())); // true
167 | // Se o histórico (ZoneRules) é diferente, significa que em algum momento da história os timezones não usavam o mesmo offset
168 | // E se há uma diferença no histórico, por mínimo que seja, a IANA cria outro timezone (por isso ter ZoneRules iguais garante que é o mesmo timezone)
169 | }
170 |
171 | /**
172 | * Vários timezones usam o mesmo offset ou a mesma abreviação, e estes podem mudar dependendo da data e hora. Por exemplo, America/Sao_Paulo usa o offset
173 | * -03:00 e abreviação BRT, mas durante o horário de verão muda para -02:00 e BRST. Mas ele não é o único timezone que usa os offsets -03:00 e -02:00, então
174 | * apenas com a informação do offset não temos como afirmar com certeza que se trata deste timezone.
175 | *
176 | * Abreviações também são problemáticas. IST, por exemplo, é usada na Índia, Irlanda e Israel, cada um com um offset diferente (e na Irlanda é só durante o
177 | * horário de verão).
178 | *
179 | * Por isso, dado um offset ou abreviação, não há como obter um único timezone. O melhor que podemos obter é uma lista de timezones, e mesmo esta lista pode
180 | * variar conforme a data e hora que escolhemos como referência. Se o offset for -02:00 e a data for em dezembro, provavelmente America/Sao_Paulo estará na
181 | * lista, já que em dezembro este timezone está em horário de verão (exceto se consultarmos em um ano que não teve horário de verão - como nos anos 70, por
182 | * exemplo - e portanto o offset era -03:00).
183 | *
184 | * Este método gera dois Sets contendo os nomes dos timezones para determinado offset ou abreviação. É interessante comparar com a API legada em
185 | * {@link Cap12Others#encontrarTimezonesPorAbreviacao()} e {@link Cap12Others#encontrarTimezonesPorOffset()}
186 | */
187 | public static void encontrarTimezonesPorOffsetOuAbreviacao() {
188 | // instante a ser usado como referência - ou seja, vou buscar os timezones que, **neste instante específico**, usam o offset +02:00 ou a abreviação EST
189 | // se mudar o Instant, pode ser que a lista de timezones mude
190 | Instant referencia = Instant.now(Setup.clock()); // 2018-05-04T20:00:00Z
191 | // procurar timezones que usam offset +02:00
192 | ZoneOffset offset = ZoneOffset.ofHours(2);
193 | // procurar timezones que usam abreviação EST
194 | String abreviacao = "EST";
195 |
196 | // Sets para guardar os nomes dos timezones
197 | Set zonesOffset2 = new HashSet<>();
198 | Set zonesAbrevEST = new HashSet<>();
199 |
200 | // pattern "z" retorna a abreviação do timezone
201 | DateTimeFormatter fmt = DateTimeFormatter.ofPattern("z");
202 |
203 | // percorrer todos os timezones
204 | ZoneId.getAvailableZoneIds().stream().map(ZoneId::of).forEach(zone -> {
205 | if (offset.equals(zone.getRules().getOffset(referencia))) {
206 | // timezone usa o offset +02:00 no instante de referência
207 | zonesOffset2.add(zone.getId());
208 | }
209 |
210 | // verificar todos os locales, pois a abreviação é locale sensitive
211 | for (Locale locale : Locale.getAvailableLocales()) {
212 | String abrev = fmt.withLocale(locale).format(referencia.atZone(zone));
213 | if (abreviacao.equals(abrev)) {
214 | // timezone usa a abreviação EST no instante de referência
215 | zonesAbrevEST.add(zone.getId());
216 | }
217 | }
218 | });
219 |
220 | // a saída pode variar conforme a versão do TZDB instalado na sua JVM
221 | System.out.println(zonesOffset2); // mais de 50 timezones: [Europe/Ljubljana, Africa/Lusaka, Europe/Kaliningrad, Africa/Gaborone, etc....
222 | System.out.println(zonesAbrevEST); // 8 timezones: [America/Coral_Harbour, SystemV/EST5, America/Jamaica, America/Cayman, America/Cancun,
223 | // America/Panama, America/Atikokan, Jamaica]
224 | }
225 |
226 | static void criarTemporalAdjuster() {
227 | // ajustar data para 3 meses no futuro, no primeiro dia do mês
228 | TemporalAdjuster adjuster = temporal -> {
229 | return temporal
230 | // 3 meses no futuro
231 | .plus(3, ChronoUnit.MONTHS)
232 | // no dia 1
233 | .with(ChronoField.DAY_OF_MONTH, 1);
234 | };
235 | LocalDate data = LocalDate.of(2018, 5, 4).with(adjuster);
236 | System.out.println(data); // 2018-08-01
237 | }
238 | }
239 |
--------------------------------------------------------------------------------
/src/main/java/exemplos/part3/Cap17Formatacao.java:
--------------------------------------------------------------------------------
1 | package exemplos.part3;
2 |
3 | import static exemplos.setup.Setup.setup;
4 |
5 | import java.time.Instant;
6 | import java.time.LocalDate;
7 | import java.time.LocalDateTime;
8 | import java.time.LocalTime;
9 | import java.time.ZoneId;
10 | import java.time.ZoneOffset;
11 | import java.time.ZonedDateTime;
12 | import java.time.format.DateTimeFormatter;
13 | import java.time.format.DateTimeFormatterBuilder;
14 | import java.time.format.FormatStyle;
15 | import java.time.temporal.ChronoField;
16 | import java.time.temporal.UnsupportedTemporalTypeException;
17 | import java.util.HashMap;
18 | import java.util.Locale;
19 | import java.util.Map;
20 | import java.util.TimeZone;
21 |
22 | import exemplos.setup.Setup;
23 |
24 | public class Cap17Formatacao {
25 |
26 | static {
27 | // setar configurações, ver javadoc da classe Setup para mais detalhes
28 | setup();
29 | }
30 |
31 | public static void main(String[] args) {
32 | exemplosBasicos();
33 | diferencaEntreUeY();
34 | mudarIdioma();
35 | naoDependeDoTimezoneDefault();
36 | formatarInstant();
37 | formatarISO8601();
38 | patternsOpcionais();
39 | qtdDigitosVariavel();
40 | textoCustomizado();
41 |
42 | textoCustomizadoAmPm();
43 | formatosPorLocaleETipo();
44 | }
45 |
46 | static void exemplosBasicos() {
47 | LocalDate data = LocalDate.of(2018, 5, 4);
48 | // usar pattern para "dia/mês/ano"
49 | DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/uuuu");
50 | // as duas formas abaixo são equivalentes
51 | System.out.println(formatter.format(data)); // 04/05/2018
52 | System.out.println(data.format(formatter)); // 04/05/2018
53 |
54 | // verifique se o objeto tem todos os campos que estão no pattern
55 | formatter = DateTimeFormatter.ofPattern("HH:mm"); // HH (hora) e mm (minuto)
56 | try {
57 | System.out.println(formatter.format(LocalDate.now()));
58 | // LocalDate não tem campos de horário, por isso lança exceção
59 | } catch (UnsupportedTemporalTypeException e) {
60 | System.out.println(e.getMessage()); // Unsupported field: HourOfDay
61 | }
62 | }
63 |
64 | // Na API legada, SimpleDateFormat só possui o pattern "y" para o ano, mas o java.time possui "u" e "y"
65 | static void diferencaEntreUeY() {
66 | // para anos positivos (DC - Depois de Cristo), não faz diferença
67 | LocalDate data = LocalDate.of(2010, 1, 1);
68 | // mostra o ano com uuuu e yyyy, e em seguida o nome da era (GGGG) - será usado o Locale default da JVM (pt_BR - Portugês do Brasi)
69 | DateTimeFormatter fmt = DateTimeFormatter.ofPattern("uuuu yyyy GGGG");
70 | System.out.println(fmt.format(data)); // 2010 2010 Ano do Senhor
71 |
72 | // para anos AC (Antes de Cristo), faz diferença
73 | data = LocalDate.of(-10, 1, 1);
74 | System.out.println(fmt.format(data)); // -0010 0011 Antes de Cristo
75 | /*
76 | * Esta diferença acontece porque "y" considera que não houve ano zero: o primeiro ano da era em que estamos atualmente é o "ano 1 Depois de Cristo", e
77 | * o ano anterior a este é o "ano 1 Antes de Cristo". Já o pattern "u" considera que antes do ano 1 vem o ano zero e antes disso os anos são negativos.
78 | */
79 | // exemplo (com vários anos)
80 | fmt = DateTimeFormatter.ofPattern("uuuu yyyy G");
81 | // do ano 2 ao -2
82 | for (int ano = 2; ano >= -2; ano--) {
83 | System.out.println(fmt.format(LocalDate.of(ano, 1, 1)));
84 | }
85 | /*
86 | * A saída é:
87 | *
88 | * 0002 0002 d.C.
89 | *
90 | * 0001 0001 d.C.
91 | *
92 | * 0000 0001 a.C.
93 | *
94 | * -0001 0002 a.C.
95 | *
96 | * -0002 0003 a.C.
97 | */
98 | }
99 |
100 | static void mudarIdioma() {
101 | LocalDate data = LocalDate.of(2018, 5, 4);
102 | DateTimeFormatter formatter = DateTimeFormatter
103 | // definir o pattern e usar locale pt_BR
104 | .ofPattern("dd 'de' MMMM 'de' uuuu", new Locale("pt", "BR"));
105 | System.out.println(data.format(formatter)); // 04 de Maio de 2018
106 |
107 | // criar formatter para português
108 | formatter = DateTimeFormatter.ofPattern("dd 'de' MMMM 'de' uuuu", new Locale("pt", "BR"));
109 | // criar outro DateTimeFormatter com o locale inglês (o pattern é o mesmo)
110 | DateTimeFormatter fmtIngles = formatter.withLocale(Locale.ENGLISH);
111 | data = LocalDate.of(2018, 5, 4);
112 | System.out.println(fmtIngles.format(data)); // 04 de May de 2018
113 | // Repare que o texto literal "de" não foi traduzido, somente os campos que são afetados pelo locale (no caso, o mês)
114 |
115 | /*
116 | * Se você não especificar um Locale, será usado o default da JVM (Locale.getDefault()). O problema é que este pode ser mudado por outras aplicações
117 | * rodando na mesma JVM (qualquer um pode chamar Locale.setDefault). Ou esta config pode ser mudada sem o seu conhecimento (a equipe responsável pelo
118 | * servidor muda o locale - seja sem querer ou de propósito - e de repente as datas começam a ser formatadas em outro idioma, sem você ter mudado o
119 | * código). Então se você já sabe em qual idioma deve estar as strings formatadas, passe o Locale explicitamente e não dependa do default.
120 | */
121 | }
122 |
123 | static void naoDependeDoTimezoneDefault() {
124 | // diferente do que acontece com SimpleDateFormat, a saída do DateTimeFormatter não depende do timezone padrão da JVM
125 | // mudar timezone padrão para Asia/Tokyo
126 | TimeZone.setDefault(TimeZone.getTimeZone("Asia/Tokyo"));
127 | // 2018-05-04T17:00-03:00[America/Sao_Paulo] (usando a data/hora "atual" simulada )
128 | ZonedDateTime agora = ZonedDateTime.now(Setup.clock().withZone(ZoneId.of("America/Sao_Paulo")));
129 | // usar pattern para "dia/mês/ano hora:minuto"
130 | DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/uuuu HH:mm");
131 | // agora é 4 de maio, 17h em São Paulo - mesmo com o timezone padrão setado para Tóquio, o valor do ZonedDateTime não é afetado
132 | // (se fosse SimpleDateFormat, os valores de data e hora seriam convertidos para Asia/Tokyo)
133 | System.out.println(formatter.format(agora)); // 04/05/2018 17:00
134 |
135 | // voltar config default (para não impactar os outros métodos)
136 | setup();
137 | }
138 |
139 | static void formatarInstant() {
140 | DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/uuuu HH:mm");
141 | Instant instant = Instant.now(Setup.clock()); // Instante atual (usando valor "simulado")
142 | try {
143 | System.out.println(formatter.format(instant));
144 | } catch (UnsupportedTemporalTypeException e) {
145 | System.out.println(e.getMessage()); // Unsupported field: DayOfMonth
146 | }
147 | // Instant representa um timestamp e portanto pode corresponder a uma data e hora diferentes em cada timezone
148 | // Por isso tentar formatá-lo lança exceção, pois ele não possui os campos de data e hora
149 | // Para obter tais campos, deve-se converter o Instant para um timezone ou offset
150 |
151 | // converter para UTC
152 | System.out.println(formatter.format(instant.atOffset(ZoneOffset.UTC))); // 04/05/2018 20:00
153 | // converter para algum timezone
154 | System.out.println(formatter.format(instant.atZone(ZoneId.of("Asia/Tokyo")))); // 05/05/2018 05:00
155 |
156 | // outra opção é setar o timezone no DateTimeFormatter
157 | formatter = DateTimeFormatter
158 | .ofPattern("dd/MM/uuuu HH:mm")
159 | // usar timezone Asia/Tokyo
160 | .withZone(ZoneId.of("Asia/Tokyo"));
161 | // instant corresponde a 2018-05-04T20:00:00Z (mas ele é convertido para o timezone do DateTimeFormatter e o resultado é a data e hora de Tóquio)
162 | System.out.println(formatter.format(instant)); // 05/05/2018 05:00
163 |
164 | // objetos que podem ser convertidos para Instant (como ZonedDateTime e OffsetDateTime) também são convertidos para o timezone do DateTimeFormatter
165 | // 2018-05-04T17:00-03:00[America/Sao_Paulo]
166 | ZonedDateTime agora = ZonedDateTime.now(Setup.clock().withZone(ZoneId.of("America/Sao_Paulo")));
167 | System.out.println(formatter.format(agora)); // 05/05/2018 05:00
168 | // LocalDateTime não possui timezone (e não pode ser convertido diretamente para Instant) -> o timezone do DateTimeFormatter não tem efeito
169 | // 2018-05-04T17:00
170 | LocalDateTime dataHora = LocalDateTime.now(Setup.clock());
171 | System.out.println(formatter.format(dataHora)); // 04/05/2018 17:00
172 | }
173 |
174 | static void formatarISO8601() {
175 | // 2018-05-04T17:00-03:00[America/Sao_Paulo]
176 | ZonedDateTime agora = ZonedDateTime.now(Setup.clock().withZone(ZoneId.of("America/Sao_Paulo")));
177 |
178 | // existem várias constantes predefinidas para o formato ISO 8601
179 | System.out.println(agora.format(DateTimeFormatter.ISO_LOCAL_DATE)); // 2018-05-04
180 | System.out.println(agora.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); // 2018-05-04T17:00:00
181 | System.out.println(agora.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)); // 2018-05-04T17:00:00-03:00
182 |
183 | // vários formatos para o offset
184 | DateTimeFormatter formatter = DateTimeFormatter
185 | // formato ISO 8601, offset com os dois pontos (igual a ISO_OFFSET_DATE_TIME)
186 | .ofPattern("uuuu-MM-dd'T'HH:mm:ssXXX");
187 | System.out.println(agora.format(formatter)); // 2018-05-04T17:00:00-03:00
188 | formatter = DateTimeFormatter
189 | // formato ISO 8601, offset sem os dois pontos
190 | .ofPattern("uuuu-MM-dd'T'HH:mm:ssXX");
191 | System.out.println(agora.format(formatter)); // 2018-05-04T17:00:00-0300
192 | formatter = DateTimeFormatter
193 | // formato ISO 8601, offset sem o valor dos minutos
194 | .ofPattern("uuuu-MM-dd'T'HH:mm:ssX");
195 | // Este formato pode omitir informações importante caso o offset tenha um valor para os minutos (a Índia, por exemplo, usa o offset +05:30, mas este
196 | // formato só mostraria +05)
197 | System.out.println(agora.format(formatter)); // 2018-05-04T17:00:00-03
198 |
199 | // a diferença para os formatadores predefinidos se dá nas frações de segundo
200 | LocalDateTime dateTime = LocalDateTime.of(2018, 1, 1, 10, 30, 20, 123000000);
201 | formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSSSSSSSS");
202 | System.out.println(dateTime.format(formatter)); // 2018-01-01T10:30:20.123000000 (imprime todos as 9 casas decimais)
203 | System.out.println(dateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); // 2018-01-01T10:30:20.123 (os zeros no final são omitidos)
204 | }
205 |
206 | static void patternsOpcionais() {
207 | // data obrigatória, hora opcional
208 | DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/uuuu[ HH:mm:ss]");
209 | // 2018-05-04
210 | LocalDate data = LocalDate.of(2018, 5, 4);
211 | // 2018-05-04T17:30
212 | LocalDateTime dataHora = data.atTime(17, 30);
213 | System.out.println(formatter.format(data)); // 04/05/2018
214 | System.out.println(formatter.format(dataHora)); // 04/05/2018 17:30:00
215 | }
216 |
217 | static void qtdDigitosVariavel() {
218 | DateTimeFormatter formatter = new DateTimeFormatterBuilder()
219 | // hora:minuto:segundo
220 | .appendPattern("HH:mm:ss")
221 | // nanossegundos, com 6 casas decimais no máximo
222 | .appendFraction(ChronoField.NANO_OF_SECOND, 0, 6, true)
223 | // criar o DateTimeFormatter
224 | .toFormatter();
225 | LocalTime hora = LocalTime.of(10, 30, 15, 123400000);
226 | System.out.println(formatter.format(hora)); // 10:30:15.1234 (usa de zero a 6 casas decimais, omitindo os zeros no final)
227 | }
228 |
229 | static void textoCustomizado() {
230 | Map nomesDosDias = new HashMap<>();
231 | // valor 1 (escrito como "1L", já que os valores devem ser long)
232 | nomesDosDias.put(1L, "Primeiro"); // dia 1 será formatado como "Primeiro" (os demais valores serão o próprio número)
233 | DateTimeFormatter formatter = new DateTimeFormatterBuilder()
234 | // nomes customizados para o dia do mês
235 | .appendText(ChronoField.DAY_OF_MONTH, nomesDosDias)
236 | // nome do mês
237 | .appendPattern(" 'de' MMMM")
238 | // criar o DateTimeFormatter com locale pt_BR
239 | .toFormatter(new Locale("pt", "BR"));
240 | // formatar 4 de maio de 2018
241 | System.out.println(LocalDate.of(2018, 5, 4).format(formatter)); // 4 de Maio
242 | // formatar 1 de maio de 2018
243 | System.out.println(LocalDate.of(2018, 5, 1).format(formatter)); // Primeiro de Maio
244 | }
245 |
246 | // ------------------------------------------------
247 | // exemplos que não estão no livro
248 | static void textoCustomizadoAmPm() {
249 | // mudar o valor de AM e PM para "manhã" e "tarde"
250 | Map textosCustomizados = new HashMap<>();
251 | textosCustomizados.put(0L, "manhã");
252 | textosCustomizados.put(1L, "tarde");
253 | DateTimeFormatter formatter = new DateTimeFormatterBuilder()
254 | // horário (hh é a hora-AM-PM, com valores de 1 a 12 - ou seja, é ambíguo a menos que se tenha também o campo AM/PM)
255 | .appendPattern("hh:mm 'da' ")
256 | // nomes customizados para o dia do mês
257 | .appendText(ChronoField.AMPM_OF_DAY, textosCustomizados)
258 | // criar o DateTimeFormatter
259 | .toFormatter();
260 | System.out.println(formatter.format(LocalTime.of(10, 30))); // 10:30 da manhã
261 | System.out.println(formatter.format(LocalTime.of(17, 50))); // 05:50 da tarde
262 | }
263 |
264 | // existem formatos predefinidos para cada Locale
265 | static void formatosPorLocaleETipo() {
266 | // data/hora atual simulada: 4 de Maio de 2018, às 17:00 em São Paulo
267 | ZonedDateTime zdt = ZonedDateTime.now(Setup.clock());
268 | DateTimeFormatter formatter;
269 | // percorrer todos os locales
270 | for (Locale locale : Locale.getAvailableLocales()) {
271 | // percorrer todos os estilos
272 | for (FormatStyle style : FormatStyle.values()) {
273 | formatter = DateTimeFormatter.ofLocalizedDateTime(style).withLocale(locale);
274 | System.out.format("Formato %s e locale %s: %s\n", style.toString(), locale.toString(), zdt.format(formatter));
275 | }
276 | }
277 |
278 | // Atenção quando for usar estes formatos predefinidos. Dependendo do Locale e do FormatStyle, o formato retornado pode ter ou não determinados campos
279 | // (como só a data ou só a hora, ou data, hora e offset, etc), e se o tipo de data/hora sendo formatado não tiver esses campos, lançará exceção
280 | }
281 | }
282 |
--------------------------------------------------------------------------------
/src/main/java/exemplos/part2/Cap08e09FormatacaoParsing.java:
--------------------------------------------------------------------------------
1 | package exemplos.part2;
2 |
3 | import static exemplos.setup.Setup.clock;
4 | import static exemplos.setup.Setup.setup;
5 |
6 | import java.text.ParseException;
7 | import java.text.SimpleDateFormat;
8 | import java.util.Calendar;
9 | import java.util.Date;
10 | import java.util.Locale;
11 | import java.util.TimeZone;
12 |
13 | import exemplos.part3.Cap18Parsing;
14 |
15 | public class Cap08e09FormatacaoParsing {
16 |
17 | static {
18 | // setar configurações, ver javadoc da classe Setup para mais detalhes
19 | setup();
20 | }
21 |
22 | public static void main(String[] args) throws ParseException {
23 | formatarData();
24 | formatarDataMudarTimezonePadrao();
25 | formatarDataMudarTimezoneFormatter();
26 | naoFormataCalendar();
27 | formatarMesDiaDaSemana();
28 | formatarMesDiaDaSemanaLocaleDefault();
29 | formatarMesDiaDaSemanaLocale();
30 | formatarLocaleVsTimezone();
31 | formatarISO8601();
32 | parseData();
33 | parseDataSetarHorario();
34 | parseHorarioDeVerao();
35 | parseOverlap();
36 | converterFormatos();
37 | parseComLocale();
38 | parseISO8601();
39 | parseLeniente();
40 | parseLenienteAteDemais();
41 | parseFracaoSegundos();
42 |
43 | // --------------------------------------------
44 | // exemplos que não estão no livro
45 | parseDataSamoa();
46 | parseAnoDoisDigitos();
47 | parseDiaMesSemAno();
48 | parseISO8601Java6();
49 | }
50 |
51 | static void formatarData() {
52 | // usando nossa "data atual" simulada
53 | Date dataAtual = new Date(clock().millis());
54 |
55 | // usar o formato dia/mês/ano
56 | SimpleDateFormat formatter = new SimpleDateFormat("dd/MM/yyyy");
57 | // formatando a data atual
58 | String dataFormatada = formatter.format(dataAtual);
59 | System.out.println(dataFormatada); // 04/05/2018
60 | }
61 |
62 | static void formatarDataMudarTimezonePadrao() {
63 | // usando nossa "data atual" simulada
64 | Date dataAtual = new Date(clock().millis());
65 |
66 | // mudar o timezone padrão
67 | TimeZone.setDefault(TimeZone.getTimeZone("Asia/Tokyo"));
68 | // usar o formato dia/mês/ano
69 | SimpleDateFormat formatter = new SimpleDateFormat("dd/MM/yyyy");
70 | // formatando a data atual (2018-05-04T17:00-03:00)
71 | System.out.println(formatter.format(dataAtual)); // 05/05/2018
72 |
73 | // voltar config default (para não impactar os outros métodos)
74 | setup();
75 | }
76 |
77 | static void formatarDataMudarTimezoneFormatter() {
78 | // usando nossa "data atual" simulada
79 | Date dataAtual = new Date(clock().millis());
80 |
81 | // usar o formato dia/mês/ano
82 | SimpleDateFormat formatter = new SimpleDateFormat("dd/MM/yyyy");
83 | // usar um timezone específico (não importa qual o timezone padrão da JVM, será usado o timezone abaixo)
84 | formatter.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));
85 | // formatando a data atual (2018-05-04T17:00-03:00)
86 | System.out.println(formatter.format(dataAtual)); // 05/05/2018
87 |
88 | // setar o timezone no SimpleDateFormat é melhor porque TimeZone.setDefault muda a config para toda a JVM
89 | }
90 |
91 | static void naoFormataCalendar() {
92 | Calendar cal = Calendar.getInstance();
93 | SimpleDateFormat formatter = new SimpleDateFormat("dd/MM/yyyy");
94 |
95 | try {
96 | formatter.format(cal); // java.lang.IllegalArgumentException: Cannot format given Object as a Date
97 | } catch (IllegalArgumentException e) {
98 | System.out.println("SimpleDateForma não formata Calendar: " + e);
99 | }
100 | // para formatar, temos que passar o Date
101 | System.out.println(formatter.format(cal.getTime()));
102 | }
103 |
104 | static void formatarMesDiaDaSemana() {
105 | // usando nossa "data atual" simulada
106 | Date dataAtual = new Date(clock().millis());
107 |
108 | SimpleDateFormat formatter = new SimpleDateFormat("EEE dd/MMM/yyyy");
109 | System.out.println(formatter.format(dataAtual)); // Sex 04/mai/2018
110 |
111 | formatter = new SimpleDateFormat("EEEE dd/MMMM/yyyy");
112 | System.out.println(formatter.format(dataAtual)); // Sexta-feira 04/Maio/2018
113 | }
114 |
115 | static void formatarMesDiaDaSemanaLocaleDefault() {
116 | // usando nossa "data atual" simulada
117 | Date dataAtual = new Date(clock().millis());
118 |
119 | // mudar o locale padrão para inglês
120 | Locale.setDefault(Locale.ENGLISH);
121 |
122 | SimpleDateFormat formatter = new SimpleDateFormat("EEEE dd/MMMM/yyyy");
123 | System.out.println(formatter.format(dataAtual)); // Friday 04/May/2018
124 |
125 | // voltar config default (para não impactar os outros métodos)
126 | setup();
127 | }
128 |
129 | static void formatarMesDiaDaSemanaLocale() {
130 | // usando nossa "data atual" simulada
131 | Date dataAtual = new Date(clock().millis());
132 |
133 | SimpleDateFormat formatter = new SimpleDateFormat("EEEE dd/MMMM/yyyy", Locale.ENGLISH);
134 | System.out.println(formatter.format(dataAtual)); // Friday 04/May/2018
135 | }
136 |
137 | static void formatarLocaleVsTimezone() {
138 | // usando nossa "data atual" simulada
139 | Date dataAtual = new Date(clock().millis());
140 |
141 | // formatter em inglês
142 | SimpleDateFormat formatter = new SimpleDateFormat("dd/MMM/yyyy", Locale.ENGLISH);
143 | // converter a data para o timezone do Japão
144 | formatter.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));
145 |
146 | // saída em inglês, mas data corresponde ao timezone do Japão
147 | System.out.println(formatter.format(dataAtual)); // 05/May/2018
148 |
149 | // o Locale não interfere no timezone, e vice-versa
150 | }
151 |
152 | static void formatarISO8601() {
153 | // usando nossa "data atual" simulada
154 | Date dataAtual = new Date(clock().millis());
155 |
156 | SimpleDateFormat iso8601Format;
157 | try {
158 | // dá erro porque "T" não é um pattern válido
159 | iso8601Format = new SimpleDateFormat("yyyy-MM-ddTHH:mm:ss.SSSXXX");
160 | } catch (IllegalArgumentException e) {
161 | System.out.println("Erro porque T não é um pattern válido: " + e); // java.lang.IllegalArgumentException: Illegal pattern character 'T'
162 | }
163 |
164 | // "T" deve estar entre aspas simples
165 | iso8601Format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
166 |
167 | System.out.println(iso8601Format.format(dataAtual)); // 2018-05-04T17:00:00.000-03:00
168 |
169 | // há a opção de setar um timezone no formatter. Exemplo: usar UTC
170 | iso8601Format.setTimeZone(TimeZone.getTimeZone("UTC"));
171 | System.out.println(iso8601Format.format(dataAtual)); // 2018-05-04T20:00:00.000Z
172 | }
173 |
174 | static void parseData() {
175 | // usar o formato dia/mês/ano
176 | SimpleDateFormat parser = new SimpleDateFormat("dd/MM/yyyy");
177 | try {
178 | // parsing: converte a String para java.util.Date
179 | Date data = parser.parse("04/05/2018");
180 | System.out.println(data); // Fri May 04 00:00:00 BRT 2018
181 | System.out.println(data.getTime()); // 1525402800000
182 | } catch (ParseException e) {
183 | e.printStackTrace();
184 | }
185 | }
186 |
187 | static void parseDataSetarHorario() throws ParseException {
188 | SimpleDateFormat parser = new SimpleDateFormat("dd/MM/yyyy");
189 | // setar o timezone para não depender da configuração padrão
190 | parser.setTimeZone(TimeZone.getTimeZone("America/Sao_Paulo"));
191 | Date data = parser.parse("04/05/2018");
192 | // String não tem campos de horário, então é setado para meia-noite
193 |
194 | // criar um Calendar e usar o Date obtido pelo parsing
195 | Calendar cal = Calendar.getInstance();
196 | cal.setTime(data);
197 | // mudar o horário para 17:00
198 | cal.set(Calendar.HOUR_OF_DAY, 17);
199 | // obter o Date com o novo horário
200 | data = cal.getTime();
201 | System.out.println(data); // Fri May 04 17:00:00 BRT 2018
202 | System.out.println(data.getTime()); // 1525464000000
203 | }
204 |
205 | static void parseHorarioDeVerao() throws ParseException {
206 | SimpleDateFormat parser = new SimpleDateFormat("dd/MM/yyyy");
207 | Date date = parser.parse("15/10/2017");
208 | // horário é setado para meia-noite, mas devido ao horário de verão, à meia-noite, os relógios são adiantados para 01:00, e portanto meia-noite não
209 | // existe neste dia, neste timezone
210 | System.out.println(date); // Sun Oct 15 01:00:00 BRST 2017
211 | }
212 |
213 | static void parseOverlap() throws ParseException {
214 | // formato com dia/mês/ano e hora:minuto
215 | SimpleDateFormat parser = new SimpleDateFormat("dd/MM/yyyy HH:mm");
216 | Date date = parser.parse("17/02/2018 23:00");
217 | // no término do horário de verão, à meia-noite o relógio é atrasado em uma hora, de volta para 23:00 (ou seja, os minutos entre 23:00 e 23:59 existem
218 | // duas vezes) - o parse, por padrão, retorna a segunda ocorrência (depois que acabou o horário de verão)
219 | System.out.println(date); // Sat Feb 17 23:00:00 BRT 2018
220 | }
221 |
222 | static void converterFormatos() throws ParseException {
223 | // parsing do formato com dia/mês/ano e hora:minuto
224 | SimpleDateFormat parser = new SimpleDateFormat("dd/MM/yyyy HH:mm");
225 | Date date = parser.parse("17/02/2018 23:00");
226 |
227 | // formatar para ISO 8601
228 | SimpleDateFormat iso8601Format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
229 | System.out.println(iso8601Format.format(date)); // 2018-02-17T23:00:00.000-03:00
230 | }
231 |
232 | static void parseComLocale() throws ParseException {
233 | // formato com dia/mês/ano e locale inglês
234 | SimpleDateFormat parser = new SimpleDateFormat("dd/MMM/yyyy", Locale.ENGLISH);
235 | System.out.println(parser.parse("01/Oct/2017")); // Sun Oct 01 00:00:00 BRT 2017
236 | }
237 |
238 | static void parseISO8601() throws ParseException {
239 | // jeito errado de se fazer parsing de UTC (Z entre aspas simples)
240 | SimpleDateFormat iso8601Format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'");
241 | // 4 de maio de 2018, à 01:00 em UTC
242 | String entrada = "2018-05-04T01:00Z";
243 | // resultado é 1 da manhã no timezone padrão (America/Sao_Paulo), mas a entrada está em UTC, então o resultado está errado, pois 1 da manhã do dia 4 de
244 | // maio no timezone America/Sao_Paulo equivale a 4 da manhã em UTC
245 | System.out.println(iso8601Format.parse(entrada)); // Fri May 04 01:00:00 BRT 2018
246 |
247 | // segunda tentativa: retirar as aspas simples do Z
248 | iso8601Format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mmZ");
249 | try {
250 | System.out.println(iso8601Format.parse(entrada)); // java.text.ParseException: Unparseable date: "2018-05-04T01:00Z"
251 | } catch (ParseException e) {
252 | System.out.println("Não deu: " + e);
253 | }
254 |
255 | // jeito certo: usar o pattern XXX (offset)
256 | iso8601Format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mmXXX");
257 | System.out.println(iso8601Format.parse(entrada)); // Thu May 03 22:00:00 BRT 2018
258 | // agora sim, 3 de maio às 22:00 no timezone America/Sao_Paulo equivale a 4 de maio às 01:00 em UTC (que é exatamente o que a String representa)
259 | }
260 |
261 | static void parseLeniente() throws ParseException {
262 | SimpleDateFormat parser = new SimpleDateFormat("dd/MM/yyyy");
263 | // 31 de fevereiro de 2018 - parsing resulta em 3 de março
264 | System.out.println(parser.parse("31/02/2018")); // Sat Mar 03 00:00:00 BRT 2018
265 |
266 | // cancelar o comportamento leniente
267 | parser.setLenient(false);
268 | try {
269 | System.out.println(parser.parse("31/02/2018")); // java.text.ParseException: Unparseable date: "31/02/2018"
270 | } catch (ParseException e) {
271 | System.out.println("Modo leniente desligado, só aceito datas válidas: " + e);
272 | }
273 | }
274 |
275 | static void parseLenienteAteDemais() throws ParseException {
276 | // formato ano, mês e dia, sem separadores
277 | SimpleDateFormat parser = new SimpleDateFormat("yyyyMMdd");
278 | // 1 de fevereiro de 2018, em formato ISO 8601 (ou seja, formato diferente do pattern acima)
279 | System.out.println(parser.parse("2018-02-01")); // Sat Dec 02 00:00:00 BRST 2017
280 | // resultado foi 2 de dezembro de 2017, uma data completamente diferente
281 |
282 | // formato dia/mês/ano
283 | parser = new SimpleDateFormat("dd/MM/yyyy");
284 | parser.setLenient(false);
285 | // 1 de fevereiro de 2018, às 10:30
286 | System.out.println(parser.parse("01/02/2018 10:30")); // Thu Feb 01 00:00:00 BRST 2018
287 | // resultado ignorou o horário (10:30 foi ignorado, o resultado tem horário igual a meia-noite)
288 | }
289 |
290 | static void parseFracaoSegundos() throws ParseException {
291 | // pattern com 6 dígitos na fração de segundos
292 | SimpleDateFormat parser = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS");
293 | // parsing da data (horário estará diferente da entrada)
294 | Date date = parser.parse("2018-02-01T10:20:30.123456");
295 | System.out.println(date); // Thu Feb 01 10:22:33 BRST 2018
296 | // O trecho "123456" foi interpretado como 123456 milissegundos, que correspondem a "2 minutos, 3 segundos e 456 milissegundos". E este valor é somado
297 | // ao horário que já havia sido obtido anteriormente (10:20:30). Por isso o resultado é 10:22:33.456 (mas repare que Date.toString() não mostra os
298 | // milissegundos - mas eles estão lá).
299 |
300 | // formatar o Date usando o mesmo SimpleDateFormat (com 6 casas decimais)
301 | System.out.println(parser.format(date)); // 2018-02-01T10:22:33.000456 <-- errado (no final deveria ser ".456")
302 | }
303 |
304 | // --------------------------------------------
305 | // exemplos que não estão no livro
306 |
307 | // No timezone Pacific/Apia (Samoa) não houve o dia 30/12/2011 (foi pulado quando eles trocaram de lado da Linha Internacional de Data)
308 | static void parseDataSamoa() throws ParseException {
309 | // formato dia/mês/ano
310 | SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
311 | // usar timezone Pacific/Apia
312 | sdf.setTimeZone(TimeZone.getTimeZone("Pacific/Apia"));
313 |
314 | // parse de uma data (30 de dezembro de 2011)
315 | Date date = sdf.parse("30/12/2011");
316 |
317 | // formatar a data
318 | System.out.println(sdf.format(date)); // 31/12/2011
319 | }
320 |
321 | static void parseAnoDoisDigitos() throws ParseException {
322 | SimpleDateFormat parser = new SimpleDateFormat("dd/MM/yy");
323 | Date date = parser.parse("04/05/18");
324 | System.out.println(date); // Fri May 04 00:00:00 BRT 2018
325 |
326 | // --------------------------------------------------------------------------
327 | parser = new SimpleDateFormat("dd/MM/yy HH:mm:ss.SSS");
328 | // setar data de referência para parse de ano com 2 dígitos
329 | // usar timestamp -999144000000, que corresponde a 1938-05-04T17:00-03:00
330 | parser.set2DigitYearStart(new Date(-999144000000L));
331 | // todas as datas devem estar entre 2DigitYearStart e (2DigitYearStart + 100 anos)
332 | // ou seja, entre 1938-05-04T17:00-03:00 (inclusive) e 2038-05-04T17:00-03:00 (exclusive)
333 |
334 | // único ano que faz esta data e hora estar no intervalo válido é 2038
335 | System.out.println(parser.parse("04/05/38 16:59:59.999")); // Tue May 04 16:59:59 BRT 2038
336 |
337 | // único ano que faz esta data e hora estar no intervalo válido é 1938
338 | System.out.println(parser.parse("04/05/38 17:00:00.000")); // Wed May 04 17:00:00 BRT 1938
339 | // quando não usamos set2DigitYearStart, é usado "a data atual menos 80 anos"
340 | // sendo que "data atual" é o instante em que o SimpleDateFormat é criado
341 | // por isso é recomendado setar o 2DigitYearStart para algum valor fixo, em vez de depender da data atual, que muda o tempo todo
342 | }
343 |
344 | /**
345 | * Data uma string com data (sem o ano) e dia da semana, encontrar um ano correspondente.
346 | *
347 | * Compare com {@link Cap18Parsing#encontrarAnoParaDiaMesDiaDaSemana()} para ver a diferença entre as APIs
348 | */
349 | public static void parseDiaMesSemAno() throws ParseException {
350 | // locale português do Brasil
351 | Locale locale = new Locale("pt", "BR");
352 |
353 | // parser com dia da semana, dia/mês
354 | SimpleDateFormat parser = new SimpleDateFormat("EEE, dd/MM", locale);
355 | Date date = parser.parse("Qua, 04/05");
356 | // o ano é setado para 1970, e a data resultante corresponde a uma Segunda-feira - ignorando o dia da semana que estava na String de entrada ("Qua")
357 | System.out.println(date); // Mon May 04 00:00:00 BRT 1970
358 |
359 | // -----------------------------------------------------------------------------
360 | // a solução é fazer o parse do dia da semana separadamente e tentar encontrar um ano em que o dia 4 de maio seja uma Quarta-feira
361 | // quebrar a String em duas partes (separar o dia da semana do restante)
362 | String[] entrada = "Qua, 04/05".split(", ");
363 |
364 | // locale português do Brasil
365 | locale = new Locale("pt", "BR");
366 | // faz parsing do dia da semana (primeira parte da String original)
367 | date = new SimpleDateFormat("EEE", locale).parse(entrada[0]);
368 | // cria um Calendar para pegar o valor do dia da semana correspondente
369 | Calendar cal = Calendar.getInstance();
370 | cal.setTime(date);
371 | int diaDaSemana = cal.get(Calendar.DAY_OF_WEEK);
372 |
373 | // faz parsing do dia e mês (segunda parte da String original)
374 | parser = new SimpleDateFormat("dd/MM");
375 | date = parser.parse(entrada[1]);
376 | cal.setTime(date);
377 |
378 | // começar no ano atual (2018)
379 | int ano = Calendar.getInstance().get(Calendar.YEAR);
380 | do {
381 | // avançar o ano, até encontrar um 4 de maio que seja Quarta-feira
382 | cal.set(Calendar.YEAR, ano);
383 | ano++;
384 | } while (cal.get(Calendar.DAY_OF_WEEK) != diaDaSemana);
385 | System.out.println(cal.getTime()); // Wed May 04 00:00:00 BRT 2022
386 | }
387 |
388 | static void parseISO8601Java6() throws ParseException {
389 | String input = "2018-05-04T01:00Z";
390 |
391 | // A partir do Java 7 é possível fazer parsing da String acima usando new SimpleDateFormat("yyyy-MM-dd'T'HH:mmXXX");
392 | // Mas no Java <= 6 não existe o pattern X, então o jeito é quebrar a String
393 | // 2018-05-04T01:00
394 | String dataHora = input.substring(0, 16);
395 | // Z
396 | String offset = input.substring(16);
397 |
398 | // formato ISO 8601 (data e hora, sem offset)
399 | SimpleDateFormat iso8601Format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm");
400 | // se o offset for numérico (+05:30, -03:00, etc), deve ter o prefixo "GMT"
401 | // se o offset for "Z", a String "GMTZ" é inválida, mas getTimeZone() não valida e retorna UTC
402 | // (um caso que "funciona" aproveitando o comportamento da API, que não valida o nome do timezone)
403 | iso8601Format.setTimeZone(TimeZone.getTimeZone("GMT" + offset));
404 |
405 | System.out.println(iso8601Format.parse(dataHora)); // Thu May 03 22:00:00 BRT 2018
406 | // a saída é diferente porque a entrada está em UTC, mas a data é impressa usando o timezone padrão da JVM
407 | }
408 | }
409 |
--------------------------------------------------------------------------------
/src/main/java/exemplos/part3/Cap20Migracao.java:
--------------------------------------------------------------------------------
1 | package exemplos.part3;
2 |
3 | import static exemplos.setup.Setup.setup;
4 |
5 | import java.text.ParseException;
6 | import java.text.SimpleDateFormat;
7 | import java.time.Clock;
8 | import java.time.Duration;
9 | import java.time.Instant;
10 | import java.time.LocalDate;
11 | import java.time.LocalTime;
12 | import java.time.OffsetDateTime;
13 | import java.time.ZoneId;
14 | import java.time.ZoneOffset;
15 | import java.time.ZonedDateTime;
16 | import java.time.format.DateTimeFormatter;
17 | import java.time.format.DateTimeFormatterBuilder;
18 | import java.time.format.DateTimeParseException;
19 | import java.time.format.ResolverStyle;
20 | import java.time.temporal.ChronoField;
21 | import java.time.temporal.ChronoUnit;
22 | import java.time.zone.ZoneRulesException;
23 | import java.util.Calendar;
24 | import java.util.Date;
25 | import java.util.GregorianCalendar;
26 | import java.util.Locale;
27 | import java.util.TimeZone;
28 |
29 | import exemplos.setup.Setup;
30 |
31 | public class Cap20Migracao {
32 |
33 | static {
34 | // setar configurações, ver javadoc da classe Setup para mais detalhes
35 | setup();
36 | }
37 |
38 | public static void main(String[] args) throws ParseException {
39 | dateVsInstant();
40 | dateVsLocalDate();
41 | conversoesCalendar();
42 | timezoneVsZoneId();
43 | javaSql();
44 | quandoNaoConverter();
45 | ultimoInstanteDoDia();
46 | simpleDateFormatVsDateTimeFormatter();
47 | }
48 |
49 | static void dateVsInstant() {
50 | Date date = new Date();
51 | // converter para Instant
52 | Instant instant = date.toInstant();
53 | // converter de volta para Date
54 | date = Date.from(instant);
55 |
56 | // detalhe: Date tem precisão de milissegundos e Instant, de nanossegundos. Portanto, a conversão pode ocasionar perda de precisão
57 | // valor com nanossegundos (9 casas decimais)
58 | instant = Instant.parse("2018-01-01T10:00:00.123456789Z");
59 | System.out.println(instant); // 2018-01-01T10:00:00.123456789Z
60 | // converter para Date (os 6 últimos dígitos são perdidos)
61 | date = Date.from(instant);
62 | System.out.println(date); // Mon Jan 01 08:00:00 BRST 2018 (saída pode variar conforme o timezone default da sua JVM)
63 | // date.toString() não mostra os milissegundos, então vou usar SimpleDateFormat
64 | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
65 | sdf.setTimeZone(TimeZone.getTimeZone("UTC")); // usando UTC, para mostrar mesma data/hora do Instant
66 | System.out.println(sdf.format(date)); // 2018-01-01T10:00:00.123Z (os dígitos 456789 foram perdidos)
67 | // converter de volta para Instant
68 | instant = date.toInstant();
69 | System.out.println(instant); // 2018-01-01T10:00:00.123Z
70 |
71 | // Se vc está pensando em colocar 9 letras "S" no SimpleDateFormat só para ver se Date não tem os dígitos 456789, dê uma olhada na classe
72 | // exemplos.part2.Cap08e09FormatacaoParsing, método parseFracaoSegundos() e veja que isso não funciona (o máximo de letras "S" que funciona corretamente
73 | // é 3)
74 |
75 | // ---------------------------------------------
76 | // Se vc quiser ter todos os dígitos dos nanossegundos no Instant, deve mantê-los separadamente:
77 | // valor com nanossegundos (9 casas decimais)
78 | instant = Instant.parse("2018-01-01T10:00:00.123456789Z");
79 | // converter para Date
80 | date = Date.from(instant);
81 | // guardar o valor dos nanossegundos
82 | int nano = instant.getNano();
83 | // converter de volta para Instant
84 | Instant instant2 = date
85 | // Date perdeu os dígitos 456789, então o Instant só vai ter .123
86 | .toInstant()
87 | // restaurar o valor original dos nanossegundos (.123456789)
88 | .with(ChronoField.NANO_OF_SECOND, nano);
89 | System.out.println(instant2); // 2018-01-01T10:00:00.123456789Z
90 | }
91 |
92 | static void dateVsLocalDate() {
93 | // usar data/hora atual simulada
94 | Date date = new Date(Setup.clock().millis());
95 | // Date só guarda o valor do timestamp. Só que dependendo do timezone, o timestamp pode corresponder a um dia e horário diferentes
96 | // Por isso, para converter para LocalDate é necessário escolher um timezone
97 | LocalDate localDate = date
98 | // converter para Instant
99 | .toInstant()
100 | // converter para um timezone
101 | .atZone(ZoneId.of("America/Sao_Paulo"))
102 | // obter o LocalDate
103 | .toLocalDate();
104 | System.out.println(localDate); // 2018-05-04
105 |
106 | // se mudar o timezone, podemos obter uma data diferente
107 | localDate = date.toInstant().atZone(ZoneId.of("Asia/Tokyo")).toLocalDate();
108 | System.out.println(localDate); // 2018-05-05
109 | // tudo porque Date não representa uma única data, e sim um timestamp (um valor que pode corresponder a uma data/hora diferente em cada timezone)
110 |
111 | // ---------------------------------------------------
112 | // O oposto também pode mudar, dependendo do timezone escolhido
113 |
114 | localDate = LocalDate.of(2018, 5, 4); // 4 de maio de 2018
115 | // para ter um Date (ou seja, um timestamp), precisamos saber o horário e o offset (sem isso, não tem como ter um valor de timestamp)
116 | date = Date.from(
117 | // setar algum horário
118 | localDate
119 | .atTime(10, 0)
120 | // setar timezone
121 | .atZone(ZoneId.of("America/Sao_Paulo"))
122 | // converter para Instant
123 | .toInstant());
124 | System.out.println(date); // Fri May 04 10:00:00 BRT 2018
125 | System.out.println(date.getTime()); // 1525438800000
126 |
127 | // se eu mudar o horário e o timezone, o resultado é completamente diferente. Ex: usar meia-noite em UTC
128 | date = Date.from(localDate.atStartOfDay(ZoneOffset.UTC).toInstant());
129 | System.out.println(date); // Thu May 03 21:00:00 BRT 2018 (meia-noite em UTC convertido para o timezone default da JVM - no caso, America/Sao_Paulo)
130 | System.out.println(date.getTime()); // 1525392000000
131 |
132 | // --------------------------------
133 | // para usar o timezone default da JVM, use ZoneId.systemDefault()
134 |
135 | // A partir do Java 9: LocalDate.ofInstant(date.toInstant(), ZoneId.of("..."));
136 | // https://docs.oracle.com/javase/9/docs/api/java/time/LocalDate.html#ofInstant-java.time.Instant-java.time.ZoneId-
137 | }
138 |
139 | static void conversoesCalendar() {
140 | // As conversões têm a mesma perda de precisão de date.toInstant(), conforme método dateVsInstant()
141 |
142 | // Calendar com data/hora "atual" simulada
143 | Calendar cal = Calendar.getInstance();
144 | cal.setTimeInMillis(Setup.clock().millis());
145 |
146 | // converter para Instant
147 | Instant instant = cal.toInstant();
148 |
149 | // converter para Calendar
150 | cal = Calendar.getInstance();
151 | // setar o valor do timestamp
152 | cal.setTimeInMillis(instant.toEpochMilli());
153 |
154 | // ------------------------------------------
155 | // conversões de/para ZonedDateTime
156 |
157 | // data/hora atual simulada: 4 de Maio de 2018, às 17:00 em São Paulo
158 | ZonedDateTime zdt = ZonedDateTime.now(Setup.clock());
159 | System.out.println(zdt); // 2018-05-04T17:00-03:00[America/Sao_Paulo]
160 | // converter para GregorianCalendar
161 | GregorianCalendar gcal = GregorianCalendar.from(zdt);
162 | // converter de volta para ZonedDateTime
163 | zdt = gcal.toZonedDateTime(); // possui mesma data/hora e timezone do Calendar
164 |
165 | // GregorianCalendar é uma subclasse de Calendar. Quando criamos um Calendar com getInstance(), o Locale default da JVM determina qual a subclasse que é
166 | // criada. Para a grande maioria dos locales, GregorianCalendar é retornado. A linha abaixo provavelmente imprime "class java.util.GregorianCalendar"
167 | // a menos que o Locale default da sua JVM seja th_TH ou ja_JP_JP
168 | System.out.println(Calendar.getInstance().getClass()); // grandes chances de ser class java.util.GregorianCalendar
169 | }
170 |
171 | static void timezoneVsZoneId() {
172 | // identificador da IANA, conversão OK
173 | TimeZone timeZone = TimeZone.getTimeZone("America/Sao_Paulo");
174 | // converter para ZoneId
175 | ZoneId zoneId = timeZone.toZoneId();
176 | System.out.println(zoneId); // America/Sao_Paulo
177 | // converter de volta para TimeZone
178 | timeZone = TimeZone.getTimeZone(zoneId);
179 | System.out.println(timeZone); // sun.util.calendar.ZoneInfo[id="America/Sao_Paulo",offset=-10800000....
180 |
181 | // -------------------------------------------
182 | // abreviações: TimeZone aceita, mas ao converter para ZoneId, os resultados são inesperados
183 |
184 | // Retorna timezone da Índia, mas IST também é usada em Israel e Irlanda (quem definiu este "default"?)
185 | System.out.println(TimeZone.getTimeZone("IST").toZoneId()); // Asia/Kolkata
186 | // Retorna o offset -05:00 (ou seja, nem sequer retornou um timezone)
187 | System.out.println(TimeZone.getTimeZone("EST").toZoneId()); // -05:00
188 |
189 | // Lembrando que tentar criar um ZoneId com abreviação diretamente dá erro
190 | try {
191 | ZoneId.of("EST");
192 | } catch (ZoneRulesException e) {
193 | System.out.println(e.getMessage()); // Unknown time-zone ID: EST
194 | }
195 | }
196 |
197 | static void javaSql() {
198 | // java.sql.Date possui um timestamp, então dependendo do timezone default, ele pode corresponder a uma data diferente
199 | // timestamp 1525464000000 -> 2018-05-04T17:00-03:00
200 | java.sql.Date date = new java.sql.Date(1525464000000L);
201 | TimeZone.setDefault(TimeZone.getTimeZone("America/Sao_Paulo"));
202 | System.out.println(date.toLocalDate()); // 2018-05-04
203 | TimeZone.setDefault(TimeZone.getTimeZone("Asia/Tokyo"));
204 | System.out.println(date.toLocalDate()); // 2018-05-05
205 |
206 | // converter LocalDate para java.sql.Date tem esse mesmo problema
207 | LocalDate localDate = LocalDate.of(2018, 5, 4); // 4 de maio de 2018
208 | // valueOf usa o timezone default da JVM e usa meia-noite como horário
209 | TimeZone.setDefault(TimeZone.getTimeZone("America/Sao_Paulo"));
210 | date = java.sql.Date.valueOf(localDate);
211 | System.out.println(date); // 2018-05-04
212 | System.out.println(date.getTime()); // 1525402800000
213 | TimeZone.setDefault(TimeZone.getTimeZone("Asia/Tokyo"));
214 | date = java.sql.Date.valueOf(localDate);
215 | System.out.println(date); // 2018-05-04
216 | System.out.println(date.getTime()); // 1525359600000
217 | // Apesar das datas impressas serem as mesmas (2018-05-04), o timestamp é completamente diferente. Isso acontece porque o valor do timestamp equivale à
218 | // 4 de maio de 2018, à meia-noite no timezone default que estava setado na JVM no momento em que valueOf foi chamado
219 |
220 | // java.sql.Time e java.sql.Timestamp sofrem do mesmo problema ao converter de/para as classes do java.time
221 |
222 | // voltar config default (para não impactar os outros métodos)
223 | setup();
224 | }
225 |
226 | static void quandoNaoConverter() {
227 | // não precisa fazer isso para obter o Instant atual
228 | Calendar cal = Calendar.getInstance();
229 | cal.setTime(new Date()); // getInstance já pega a data/hora atual, então esta linha já é redundante
230 | Instant instant = Instant.ofEpochMilli(cal.getTimeInMillis()); // troque tudo isso por Instant.now() e pronto!
231 |
232 | // "Ah, mas talvez fizeram assim porque `Instant` tem precisão de nanossegundos e eles só precisam dos milissegundos"
233 | // neste caso, você pode usar truncatedTo
234 | instant = Instant.now().truncatedTo(ChronoUnit.MILLIS); // trunca o Instant, mantendo apenas os milissegundos
235 | // se o valor do Instant original fosse 2018-08-17T12:15:10.123456789Z, o resultado de truncatedTo(ChronoUnit.MILLIS) seria 2018-08-17T12:15:10.123Z
236 |
237 | // outra opcão é criar um Clock que ignora frações de segundo menores que 1 milissegundo
238 | Clock clock = Clock.tick(Clock.systemDefaultZone(), Duration.ofMillis(1));
239 | instant = Instant.now(clock);
240 |
241 | // ---------------------------------------------------
242 | // não misture as APIs desnecessariamente
243 | ZoneId defaultJvm = TimeZone.getDefault().toZoneId(); // não
244 | defaultJvm = ZoneId.systemDefault(); // sim
245 |
246 | // ---------------------------------------------------
247 | // E não esqueça que, apesar de ter métodos de conversão (Date para Instant, etc), as APIs são incompatíveis entre si
248 | try {
249 | DateTimeFormatter formato = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss");
250 | // parse retorna um TemporalAccessor, que não é compatível com Date (por isso lança exceção)
251 | Date date = (Date) formato.parse("10/02/2018 17:30:00");
252 | } catch (ClassCastException e) {
253 | System.out.println(e.getMessage()); // java.time.format.Parsed cannot be cast to java.util.Date
254 | }
255 |
256 | /*
257 | * Particularmente, um dos casos em que misturo a API legada com java.time é quando estou usando bibliotecas que ainda precisam de Date ou Calendar e
258 | * tenho que fazer alguma manipulação nestes objetos. Por exemplo, suponha que estou usando uma API que retorna um Date, que eu preciso passar para
259 | * outra API, só que o valor recebido por esta deve ter o horário setado para o fim do dia:
260 | *
261 | * Date date = algumaAPI.getDate(); // API que retorna java.util.Date
262 | *
263 | * date = ... // manipular o Date (setar horário para fim do dia)
264 | *
265 | * outraAPI.fazAlgo(date); // passar o Date para outra API
266 | *
267 | * É possível fazer estes cálculos usando Calendar, mas neste caso eu prefiro converter o Date para o java.time (seja para LocalDate, Instant, ou
268 | * qualquer outra classe que seja mais fácil de trabalhar, dependendo do que preciso fazer), efetuar os cálculos e converter o resultado de volta para
269 | * Date.
270 | */
271 | }
272 |
273 | static void ultimoInstanteDoDia() {
274 | // tentar obter o último instante do dia 19/06/2009, em algum timezone
275 | LocalDate dia = LocalDate.of(2009, 6, 19);
276 |
277 | // não basta setar o horário para 23:59:59.999999999 e pronto, graças às bizarrices dos timezones
278 | ZonedDateTime fimDoDiaErrado = dia
279 | // setar horário para 23:59:59.999999999
280 | .atTime(LocalTime.MAX)
281 | // converter para o timezone de Bangladesh
282 | .atZone(ZoneId.of("Asia/Dhaka"));
283 | // O resultado é 00:59 do dia 20 (ou seja, nada parecido com "o último instante do dia 19")
284 | System.out.println(fimDoDiaErrado); // 2009-06-20T00:59:59.999999999+07:00[Asia/Dhaka]
285 |
286 | // Em Bangladesh, houve uma mudança de offset neste dia: às 23h do dia 19 o relógio foi adiantado em 1 hora, direto para meia-noite do dia 20
287 | // Por isso, 23:59 faz parte de um gap e ZonedDateTime fez o ajuste para a próxima hora válida (00:59)
288 |
289 | // O modo correto de se obter o último instante do dia é: primeiro obter o início do dia seguinte e depois subtrair 1 nanossegundo. Com isso eu
290 | // garanto que peguei o último instante daquele dia:
291 | ZonedDateTime fimDoDia = dia
292 | // início do dia seguinte no timezone
293 | .plusDays(1)
294 | .atStartOfDay(ZoneId.of("Asia/Dhaka"))
295 | // subtrair 1 nanossegundo
296 | .minusNanos(1);
297 | System.out.println(fimDoDia); // 2009-06-19T22:59:59.999999999+06:00[Asia/Dhaka]
298 | // para ter certeza que este é o último instante do dia 19, basta somar 1 nanossegundo e ver se o resultado é o início do dia 20
299 | System.out.println(fimDoDia.plusNanos(1)); // 2009-06-20T00:00+07:00[Asia/Dhaka]
300 | }
301 |
302 | static void simpleDateFormatVsDateTimeFormatter() throws ParseException {
303 | // alguns patterns funcionam do mesmo jeito
304 | SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy HH:mm");
305 | DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm");
306 | Date date = new Date(Setup.clock().millis());
307 | System.out.println(sdf.format(date)); // 04/05/2018 17:00
308 | System.out.println(OffsetDateTime.now(Setup.clock()).format(fmt)); // 04/05/2018 17:00
309 |
310 | // ----------------------------------------------------
311 | // mas nem todos os patterns são assim
312 | ZonedDateTime zdt = ZonedDateTime.now(Setup.clock());
313 |
314 | // alguns são diferentes
315 | System.out.println(zdt.format(DateTimeFormatter.ofPattern("uuuu"))); // 2018 (ano)
316 | System.out.println(new SimpleDateFormat("uuuu").format(date)); // 0005 (dia da semana -> 5 é sexta-feira)
317 |
318 | // outros não existem na API antiga
319 | // dia da semana, trimestre e nome do timezone
320 | fmt = DateTimeFormatter.ofPattern("eeee QQQQ VV", new Locale("pt", "BR"));
321 | System.out.println(zdt.format(fmt)); // Sexta-feira 2º trimestre America/Sao_Paulo
322 | // esses patterns não funcionam com SimpleDateFormat
323 | try {
324 | new SimpleDateFormat("eeee");
325 | } catch (IllegalArgumentException e) {
326 | System.out.println(e.getMessage()); // Illegal pattern character 'e'
327 | }
328 | try {
329 | new SimpleDateFormat("QQQQ");
330 | } catch (IllegalArgumentException e) {
331 | System.out.println(e.getMessage()); // Illegal pattern character 'Q'
332 | }
333 | try {
334 | new SimpleDateFormat("VV");
335 | } catch (IllegalArgumentException e) {
336 | System.out.println(e.getMessage()); // Illegal pattern character 'V'
337 | }
338 |
339 | // ----------------------------------------------------
340 | // outro exemplo: pattern "h" (hour-of-am-pm, com valores de 1 a 12)
341 | // este campo é ambíguo se não tivermos o identificador AM/PM
342 | sdf = new SimpleDateFormat("hh:mm");
343 | // faz parsing de 17:00, mesmo que o campo "h" tenha valores de 1 a 12
344 | System.out.println(sdf.parse("17:00")); // Thu Jan 01 17:00:00 BRT 1970
345 | // DateTimeFormatter, por sua vez, não aceita, nem se estiver em modo leniente
346 | fmt = DateTimeFormatter.ofPattern("hh:mm").withResolverStyle(ResolverStyle.LENIENT);
347 | try {
348 | // mesmo se o valor for válido (o campo aceita valores de 1 a 12), sem a informação AM/PM não é possível resolver o horário
349 | LocalTime.parse("10:00", fmt);
350 | } catch (DateTimeParseException e) {
351 | System.out.println(e.getMessage()); // Text '10:00' could not be parsed: Unable to obtain LocalTime from TemporalAccessor: {HourOfAmPm=10,
352 | // MinuteOfHour=0},ISO of type java.time.format.Parsed
353 | }
354 | // para resolver o parsing acima, há algumas alternativas:
355 |
356 | // 1- A String de entrada deve ter a informação se é AM ou PM
357 | fmt = DateTimeFormatter.ofPattern("hh:mm a", Locale.ENGLISH); // usar Locale, porque o campo AM/PM é locale sensitive
358 | System.out.println(LocalTime.parse("10:00 PM", fmt)); // 22:00
359 |
360 | // 2- setar valor predefinido para o campo AM/PM
361 | fmt = new DateTimeFormatterBuilder()
362 | // hora e minuto
363 | .appendPattern("hh:mm")
364 | // valor default para AM/PM -> 0 para AM e 1 para PM
365 | .parseDefaulting(ChronoField.AMPM_OF_DAY, 1L)
366 | .toFormatter();
367 | System.out.println(LocalTime.parse("10:00", fmt)); // 22:00
368 |
369 | // 3- usar "HH" (nesse caso, o campo tem valores de 0 a 23, ou seja, não preciso saber se é AM ou PM)
370 | fmt = DateTimeFormatter.ofPattern("HH:mm");
371 | System.out.println(LocalTime.parse("10:00", fmt)); // 10:00 <- se quiser 10 da noite, a string deve ser "22:00"
372 |
373 | // resumindo, o que funcionava com SimpleDateFormat não necessariamente vai funcionar com DateTimeFormatter (e vice-versa)
374 | }
375 | }
376 |
--------------------------------------------------------------------------------
/src/main/java/exemplos/part1/Capitulos1a6.java:
--------------------------------------------------------------------------------
1 | package exemplos.part1;
2 |
3 | import static exemplos.setup.Setup.clock;
4 | import static exemplos.setup.Setup.setup;
5 |
6 | import java.text.ParseException;
7 | import java.time.Duration;
8 | import java.time.Instant;
9 | import java.time.LocalDate;
10 | import java.time.LocalDateTime;
11 | import java.time.Period;
12 | import java.time.ZoneId;
13 | import java.time.ZoneOffset;
14 | import java.time.ZonedDateTime;
15 | import java.time.chrono.HijrahDate;
16 | import java.time.format.DateTimeFormatter;
17 | import java.time.format.DateTimeFormatterBuilder;
18 | import java.time.temporal.ChronoField;
19 | import java.time.temporal.ChronoUnit;
20 | import java.time.temporal.Temporal;
21 | import java.util.Arrays;
22 | import java.util.HashSet;
23 | import java.util.List;
24 | import java.util.Set;
25 | import java.util.stream.Collectors;
26 | import java.util.stream.Stream;
27 |
28 | import org.joda.time.Years;
29 |
30 | /**
31 | * A primeira parte do livro não possui código propriamente dito, mas fiz alguns códigos em Java para ilustrar os exemplos do livro.
32 | *
33 | * Se não está familiarizado com a API java.time não se preocupe em entender os códigos agora, pois estas classes são explicadas na parte 3. Os códigos que
34 | * estão aqui servem apenas para demonstrar alguns conceitos citados no livro.
35 | *
36 | * Como a primeira parte do livro é mais conceitual, não há tanto código associado, então esta classe cobre os capítulos 1 a 6.
37 | */
38 | public class Capitulos1a6 {
39 |
40 | static {
41 | // setar configurações, ver javadoc da classe Setup para mais detalhes
42 | setup();
43 | }
44 |
45 | public static void main(String[] args) throws ParseException {
46 | queDiaEhHojeQueHorasSao();
47 | inicioHorarioDeVeraoSP();
48 | fimHorarioDeVeraoSP();
49 | samoaPulaUmDia();
50 | dadoOffsetQualTimezone();
51 | mesmoInstantEmVariosTimezones();
52 | duracaoNegativa();
53 | somaDataDuracao();
54 | periodoComSemanas();
55 | somar2DiasA30DeMaio();
56 | somar24HorasVs1Dia();
57 | somarUmMes();
58 | calcularIdadeLocalDate();
59 | calcularIdadeLocalDateTime();
60 | algunsTimezonesComOffset0200();
61 | }
62 |
63 | static void queDiaEhHojeQueHorasSao() {
64 | // 4 de Maio de 2018, 17h em São Paulo
65 | ZonedDateTime z = ZonedDateTime.of(2018, 5, 4, 17, 0, 0, 0, ZoneId.of("America/Sao_Paulo"));
66 | System.out.println(z);
67 |
68 | // converter para fuso horário da California
69 | System.out.println(z.withZoneSameInstant(ZoneId.of("America/Los_Angeles")));
70 | // converter para fuso horário do Japão
71 | System.out.println(z.withZoneSameInstant(ZoneId.of("Asia/Tokyo")));
72 |
73 | // obter o número gigante 1525464000000 (que será explicado posteriormente)
74 | System.out.println(z.toInstant().toEpochMilli());
75 |
76 | // converter para o calendário islâmico
77 | HijrahDate hd = HijrahDate.from(z);
78 | System.out.println(hd);
79 |
80 | // quantas vezes a data/hora "4 de Maio de 2018, as 17 horas" ocorre no mundo?
81 | LocalDateTime dt = LocalDateTime.of(2018, 5, 4, 17, 0);
82 | Set set = new HashSet<>();
83 | // percorrer todos os timezones e adicionar o Instant correspondente no set
84 | ZoneId.getAvailableZoneIds().stream().map(ZoneId::of).map(dt::atZone).forEach(zdt -> {
85 | set.add(zdt.toInstant());
86 | });
87 | System.out.println(set.size()); // 38 - o resultado pode variar, pois as informações de timezone mudam o tempo todo
88 | set.stream().sorted().forEach(System.out::println);
89 | }
90 |
91 | static void inicioHorarioDeVeraoSP() {
92 | // timezone de São Paulo
93 | ZoneId spZone = ZoneId.of("America/Sao_Paulo");
94 | // 14 de Outubro de 2017, 23:59h (um minuto antes de começar o horário de verão)
95 | ZonedDateTime umMinutoAntesHV = ZonedDateTime.of(2017, 10, 14, 23, 59, 0, 0, spZone);
96 | // 1 minuto depois, começa o horário de verão
97 | ZonedDateTime inicioHV = umMinutoAntesHV.plusMinutes(1);
98 |
99 | // imprimir as datas no fuso de SP e o instante correspondente em UTC
100 | System.out.println(umMinutoAntesHV + ", UTC=" + umMinutoAntesHV.toInstant());
101 | System.out.println(inicioHV + ", UTC=" + inicioHV.toInstant());
102 |
103 | /*
104 | * A saida será:
105 | *
106 | * 2017-10-14T23:59-03:00[America/Sao_Paulo], UTC=2017-10-15T02:59:00Z
107 | *
108 | * 2017-10-15T01:00-02:00[America/Sao_Paulo], UTC=2017-10-15T03:00:00Z
109 | *
110 | * Repare que o horário de início foi ajustado automaticamente para 1 da manhã, pois à meia-noite o relógio é adiantado em 1 hora.
111 | *
112 | * Porém os instantes em UTC são contínuos, graças a mudança de offset.
113 | *
114 | */
115 |
116 | // comparando com o mesmo horário em Recife
117 | ZoneId recifeZone = ZoneId.of("America/Recife");
118 | ZonedDateTime recifeUmMinutoAntesHV = umMinutoAntesHV.withZoneSameInstant(recifeZone);
119 | ZonedDateTime recifeInicioHV = inicioHV.withZoneSameInstant(recifeZone);
120 | System.out.println(recifeUmMinutoAntesHV + ", UTC=" + recifeUmMinutoAntesHV.toInstant());
121 | System.out.println(recifeInicioHV + ", UTC=" + recifeInicioHV.toInstant());
122 | /*
123 | * A saída será:
124 | *
125 | * 2017-10-14T23:59-03:00[America/Recife], UTC=2017-10-15T02:59:00Z
126 | *
127 | * 2017-10-15T00:00-03:00[America/Recife], UTC=2017-10-15T03:00:00Z
128 | *
129 | * Repare que o relógio não foi adiantado e o offset permaneceu o mesmo (-03:00), pois Recife não tem horário de verão, e os instantes UTC são os mesmos
130 | * de São Paulo
131 | */
132 | }
133 |
134 | static void fimHorarioDeVeraoSP() {
135 | // timezone de São Paulo
136 | ZoneId spZone = ZoneId.of("America/Sao_Paulo");
137 | // 17 de Fevereiro de 2018, 23h (um minuto antes de acabar o horário de verão)
138 | ZonedDateTime umMinutoAntesFimHV = ZonedDateTime.of(2018, 2, 17, 23, 59, 0, 0, spZone);
139 | // 1 hora depois, acaba o horário de verão
140 | ZonedDateTime fimHV = umMinutoAntesFimHV.plusMinutes(1);
141 |
142 | // imprimir as datas no fuso de SP e o instante correspondente em UTC
143 | System.out.println(umMinutoAntesFimHV + ", UTC=" + umMinutoAntesFimHV.toInstant());
144 | System.out.println(fimHV + ", UTC=" + fimHV.toInstant());
145 |
146 | /*
147 | * A saida será:
148 | *
149 | * 2018-02-17T23:59-02:00[America/Sao_Paulo], UTC=2018-02-18T01:59:00Z
150 | *
151 | * 2018-02-17T23:00-03:00[America/Sao_Paulo], UTC=2018-02-18T02:00:00Z
152 | *
153 | * Repare que um minuto depois deveria resultar em meia-noite, porém como o horário de verão acabou, o relógio é atrasado em 1 hora e volta para 23h.
154 | *
155 | * Porém os instantes em UTC são contínuos, graças a mudança de offset.
156 | *
157 | */
158 |
159 | // comparando com o mesmo horário em Recife
160 | ZoneId recifeZone = ZoneId.of("America/Recife");
161 | ZonedDateTime recifeUmMinutoAntesFimHV = umMinutoAntesFimHV.withZoneSameInstant(recifeZone);
162 | ZonedDateTime recifeFimHV = fimHV.withZoneSameInstant(recifeZone);
163 | System.out.println(recifeUmMinutoAntesFimHV + ", UTC=" + recifeUmMinutoAntesFimHV.toInstant());
164 | System.out.println(recifeFimHV + ", UTC=" + recifeFimHV.toInstant());
165 | /*
166 | * A saída será:
167 | *
168 | * 2018-02-17T22:59-03:00[America/Recife], UTC=2018-02-18T01:59:00Z
169 | *
170 | * 2018-02-17T23:00-03:00[America/Recife], UTC=2018-02-18T02:00:00Z
171 | *
172 | * Repare que o relógio não foi atrasado e o offset permaneceu o mesmo (-03:00), pois Recife não tem horário de verão, e os instantes UTC são os mesmos
173 | * de São Paulo
174 | */
175 | }
176 |
177 | static void samoaPulaUmDia() {
178 | // timezone de Samoa
179 | ZoneId zone = ZoneId.of("Pacific/Apia");
180 |
181 | // 29 de Dezembro de 2011, 23:59 (1 minuto antes da mudança para o outro lado da Linha Internacional de Data)
182 | ZonedDateTime antesMudanca = ZonedDateTime.of(2011, 12, 29, 23, 59, 0, 0, zone);
183 | System.out.println(antesMudanca); // 2011-12-29T23:59-10:00[Pacific/Apia]
184 |
185 | // 1 minuto depois, offset muda de -10 para +14 e a data local muda de 29 para 31
186 | // todo o dia 31 de Dezembro foi pulado localmente
187 | System.out.println(antesMudanca.plusMinutes(1)); // 2011-12-31T00:00+14:00[Pacific/Apia]
188 |
189 | // mostrar que os instantes UTC sao continuos
190 | System.out.println(antesMudanca.toInstant()); // 2011-12-30T09:59:00Z
191 | System.out.println(antesMudanca.plusMinutes(1).toInstant()); // 2011-12-30T10:00:00Z
192 | }
193 |
194 | /*
195 | * Usei a versão 2018e do TZBD (https://data.iana.org/time-zones/releases/tzdata2018e.tar.gz)
196 | *
197 | * Se sua JVM está com uma versão diferente, pode ser que os resultados não sejam os mesmos.
198 | *
199 | * Para verificar a versão, você pode usar ZoneRulesProvider.getVersions("America/Sao_Paulo"), que retorna um map cujas chaves são as versões usadas. No meu
200 | * caso, a chave é "2018e", confirmando que estou usando a versão 2018e.
201 | */
202 | static void dadoOffsetQualTimezone() {
203 | Instant i = Instant.now(clock());
204 | // procurar timezones que usam o offset +02:0
205 | ZoneOffset offset = ZoneOffset.ofHours(2);
206 |
207 | // procurar timezones para 4 de Maio de 2018, às 17 horas em São Paulo (total: 57)
208 | getZonesByOffset(i, offset);
209 |
210 | // procurar timezones para 1 de Outubro de 2018, às 17 horas em São Paulo (total: 44)
211 | getZonesByOffset(i.plus(Duration.ofDays(180)), offset);
212 | }
213 |
214 | // dado um Instant e um offset, verifica quais os timezones que usam este offset naquele instante
215 | static void getZonesByOffset(Instant i, ZoneOffset offset) {
216 | Set set = new HashSet<>();
217 | ZoneId.getAvailableZoneIds().forEach(s -> {
218 | if (offset.equals(i.atZone(ZoneId.of(s)).getOffset())) {
219 | set.add(s);
220 | }
221 | });
222 | System.out.println("\n********\n" + i + "(" + i.atZone(ZoneId.of("America/Sao_Paulo")) + ")" + " with " + offset);
223 | System.out.println(set.size() + ": " + set.stream().sorted().collect(Collectors.joining(", ")));
224 | }
225 |
226 | static void mesmoInstantEmVariosTimezones() {
227 | // Instant representa um timestamp
228 | Instant instant = clock().instant();
229 |
230 | // o mesmo timestamp corresponde a uma data e hora diferentes, dependendo do timezone
231 | System.out.println(instant.atZone(ZoneId.of("America/Sao_Paulo")));
232 | System.out.println(instant.atZone(ZoneId.of("Europe/Berlin")));
233 | System.out.println(instant.atZone(ZoneId.of("Asia/Tokyo")));
234 | System.out.println(instant.atZone(ZoneId.of("Pacific/Honolulu")));
235 | /*
236 | * Saída:
237 | *
238 | * 2018-05-04T17:00-03:00[America/Sao_Paulo]
239 | *
240 | * 2018-05-04T22:00+02:00[Europe/Berlin]
241 | *
242 | * 2018-05-05T05:00+09:00[Asia/Tokyo]
243 | *
244 | * 2018-05-04T10:00-10:00[Pacific/Honolulu]
245 | */
246 | }
247 |
248 | static void duracaoNegativa() {
249 | // -1 hora e +3 minutos, o equivalente a -57 minutos
250 | System.out.println(Duration.parse("PT-1H3M").getSeconds());
251 | System.out.println(Duration.parse("PT57M").getSeconds());
252 | }
253 |
254 | static void somaDataDuracao() {
255 | // 4 de Maio de 2018 somada a uma duração de 2 dias resulta em 6 de Maio de 2018
256 | LocalDate quatroDeMaio2018 = LocalDate.of(2018, 5, 4);
257 | Period doisDias = Period.parse("P2D");
258 | LocalDate seisDeMaio2018 = quatroDeMaio2018.plus(doisDias);
259 | System.out.println(seisDeMaio2018); // 2018-05-06
260 |
261 | // Entre 4 de Maio de 2018 e 6 de Maio de 2018, há uma diferença (ou uma duração) de 2 dias
262 | Period diff = Period.between(quatroDeMaio2018, seisDeMaio2018);
263 | System.out.println(diff); // P2D
264 | System.out.println(diff.getDays()); // 2
265 | }
266 |
267 | /*
268 | * Períodos com semanas (W) misturadas com outras unidades (como dias, meses, etc) não são permitidas pela ISO 8601
269 | *
270 | * Porém, algumas APIs aceitam mesmo assim. O teste abaixo mostra como diferentes APIs tratam P1W1D (1 semana e 1 dia)
271 | */
272 | static void periodoComSemanas() {
273 | // java.time, ajusta para 8 dias
274 | System.out.println(Period.parse("P1W1D")); // P8D
275 |
276 | // Joda-Time, mantém as semanas separadas dos dias
277 | System.out.println(org.joda.time.Period.parse("P1W1D")); // P1W1D
278 | }
279 |
280 | static void somar2DiasA30DeMaio() {
281 | // 30 de maio de 2018
282 | LocalDate dt = LocalDate.of(2018, 5, 30);
283 | // somar 2 dias
284 | System.out.println(dt.plusDays(2)); // 2018-06-01 (1 de Junho de 2018)
285 | }
286 |
287 | static void somar24HorasVs1Dia() {
288 | // 4 de Maio de 2018, às 5 da tarde
289 | LocalDateTime dt = LocalDateTime.parse("2018-05-04T17:00");
290 |
291 | // somar 24 horas
292 | System.out.println(dt.plusHours(24)); // 2018-05-05T17:00
293 | // somar 1 dia
294 | System.out.println(dt.plusDays(1)); // 2018-05-05T17:00
295 |
296 | // timezone de São Paulo
297 | ZoneId spZone = ZoneId.of("America/Sao_Paulo");
298 | // 14 de Outubro de 2017, 10h (um dia antes de começar o horário de verão)
299 | ZonedDateTime antesHv = ZonedDateTime.of(2017, 10, 14, 10, 0, 0, 0, spZone);
300 | System.out.println(antesHv); // 2017-10-14T10:00-03:00[America/Sao_Paulo]
301 | // o mesmo dia, em UTC
302 | System.out.println(antesHv.toInstant()); // 2017-10-14T13:00:00Z
303 |
304 | // somar 1 dia
305 | System.out.println(antesHv.plusDays(1)); // 2017-10-15T10:00-02:00[America/Sao_Paulo]
306 | // em UTC
307 | System.out.println(antesHv.plusDays(1).toInstant()); // 2017-10-15T12:00:00Z
308 |
309 | // somar 24 horas
310 | System.out.println(antesHv.plusHours(24)); // 2017-10-15T11:00-02:00[America/Sao_Paulo]
311 | // em UTC
312 | System.out.println(antesHv.plusHours(24).toInstant()); // 2017-10-15T13:00:00Z
313 |
314 | // 17 de Fevereiro de 2018, 10h (um dia antes de acabar o horário de verão)
315 | ZonedDateTime antesFimHV = ZonedDateTime.of(2018, 2, 17, 10, 0, 0, 0, spZone);
316 | System.out.println(antesFimHV); // 2018-02-17T10:00-02:00[America/Sao_Paulo]
317 | // o mesmo dia, em UTC
318 | System.out.println(antesFimHV.toInstant()); // 2018-02-17T12:00:00Z
319 |
320 | // somar 1 dia
321 | System.out.println(antesFimHV.plusDays(1)); // 2018-02-18T10:00-03:00[America/Sao_Paulo]
322 | // em UTC
323 | System.out.println(antesFimHV.plusDays(1).toInstant()); // 2018-02-18T13:00:00Z
324 |
325 | // somar 24 horas
326 | System.out.println(antesFimHV.plusHours(24)); // 2018-02-18T09:00-03:00[America/Sao_Paulo]
327 | // em UTC
328 | System.out.println(antesFimHV.plusHours(24).toInstant()); // 2018-02-18T12:00:00Z
329 | }
330 |
331 | static void somarUmMes() {
332 | // 1 de Janeiro de 2018
333 | LocalDate jan = LocalDate.of(2018, 1, 1);
334 |
335 | // somar 1 mês = 1 de Fevereiro de 2018
336 | LocalDate fev = jan.plusMonths(1);
337 | System.out.println(fev); // 2018-02-01
338 |
339 | // diferença em dias
340 | long dias = ChronoUnit.DAYS.between(jan, fev);
341 | System.out.println(dias); // 31
342 |
343 | // somar 1 mês = 1 de Março de 2018
344 | LocalDate mar = fev.plusMonths(1);
345 | System.out.println(mar); // 2018-03-01
346 |
347 | // diferença em dias
348 | dias = ChronoUnit.DAYS.between(fev, mar);
349 | System.out.println(dias); // 28
350 | }
351 |
352 | public DateTimeFormatter criaFormatter(boolean incluirHorasOpcional, boolean incluirFracaoSegundos) {
353 | DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
354 | // usar formatador predefinido para data (yyyy-MM-dd)
355 | builder.append(DateTimeFormatter.ISO_LOCAL_DATE);
356 |
357 | if (incluirHorasOpcional) {
358 | // seção opcional com as horas
359 | builder.optionalStart().appendPattern(" HH:mm:ss");
360 | if (incluirFracaoSegundos) {
361 | builder.appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true);
362 | }
363 | // encerrar seção opcional
364 | builder.optionalEnd();
365 | }
366 | // criar o DateTimeFormatter
367 | return builder.toFormatter();
368 | }
369 |
370 | static Temporal ajusta(Temporal temporal) {
371 | return temporal
372 | // 3 meses no futuro
373 | .plus(3, ChronoUnit.MONTHS)
374 | // no dia 1
375 | .with(ChronoField.DAY_OF_MONTH, 1);
376 | }
377 |
378 | static void calcularIdadeLocalDate() {
379 | // data de nascimento: 1 de Janeiro de 2000
380 | LocalDate dataNascimento = LocalDate.of(2000, 1, 1);
381 |
382 | // no dia 31 de Dezembro de 2006, a idade é 6 anos
383 | long idade = ChronoUnit.YEARS.between(dataNascimento, LocalDate.of(2006, 12, 31));
384 | System.out.println(idade); // 6
385 |
386 | // somente em 1 de Janeiro de 2007, a idade é 7 anos
387 | idade = ChronoUnit.YEARS.between(dataNascimento, LocalDate.of(2007, 1, 1));
388 | System.out.println(idade); // 7
389 |
390 | // *********************************
391 | // data de nascimento: 29 de Fevereiro de 2000
392 | dataNascimento = LocalDate.of(2000, 2, 29);
393 |
394 | // em 28 de Fevereiro de 2007, não completou 7 anos
395 | idade = ChronoUnit.YEARS.between(dataNascimento, LocalDate.of(2007, 2, 28));
396 | System.out.println(idade); // 6
397 |
398 | // em 1 de Março de 2007, completou 7 anos
399 | idade = ChronoUnit.YEARS.between(dataNascimento, LocalDate.of(2007, 3, 1));
400 | System.out.println(idade); // 7
401 |
402 | // em 29 de Fevereiro de 2008, completou 8 anos
403 | idade = ChronoUnit.YEARS.between(dataNascimento, LocalDate.of(2008, 2, 29));
404 | System.out.println(idade); // 8
405 |
406 | // ***************************** teste com Joda-Time
407 | org.joda.time.LocalDate dtnasc = new org.joda.time.LocalDate(2000, 2, 29);
408 | // Joda-Time considera que em 28 de Fevereiro de 2007, completou 7 anos (diferente do java.time, que considera 6)
409 | System.out.println(Years.yearsBetween(dtnasc, new org.joda.time.LocalDate(2007, 2, 28)).getYears()); // 7
410 | System.out.println(Years.yearsBetween(dtnasc, new org.joda.time.LocalDate(2008, 2, 29)).getYears()); // 8
411 | }
412 |
413 | static void calcularIdadeLocalDateTime() {
414 | // data de nascimento: 1 de Janeiro de 2000, às 10h
415 | LocalDateTime dataNascimento = LocalDateTime.of(2000, 1, 1, 10, 0);
416 |
417 | // em 1 de Janeiro de 2007, às 09:59, ainda não completou 7 anos
418 | long idade = ChronoUnit.YEARS.between(dataNascimento, LocalDateTime.of(2007, 1, 1, 9, 59));
419 | System.out.println(idade); // 6
420 |
421 | // somente em 1 de Janeiro de 2007, às 10h, completa 7 anos
422 | idade = ChronoUnit.YEARS.between(dataNascimento, LocalDateTime.of(2007, 1, 1, 10, 0));
423 | System.out.println(idade); // 7
424 | }
425 |
426 | static void algunsTimezonesComOffset0200() {
427 | // 4 de Maio de 2018, às 17:00 em São Paulo
428 | ZonedDateTime maio = ZonedDateTime.now(clock());
429 | // 31 de Outubro de 2018, às 17:00 em São Paulo
430 | ZonedDateTime outubro = ZonedDateTime.of(2018, 10, 31, 17, 0, 0, 0, ZoneId.of("America/Sao_Paulo"));
431 | // 10 de Agosto de 2014, às 17:00 em São Paulo
432 | ZonedDateTime agosto2014 = ZonedDateTime.of(2014, 8, 10, 17, 0, 0, 0, ZoneId.of("America/Sao_Paulo"));
433 |
434 | List datas = Arrays.asList(agosto2014, maio, outubro);
435 |
436 | Stream.of("Africa/Ceuta", "Asia/Amman", "Africa/Cairo").map(ZoneId::of).forEach(zone -> {
437 | System.out.println("\n" + zone);
438 | datas.forEach(zdt -> {
439 | System.out.println("Em " + zdt + ", o offset é " + zdt.withZoneSameInstant(zone).getOffset() + " - DST="
440 | + zone.getRules().isDaylightSavings(zdt.toInstant()));
441 | });
442 | });
443 | }
444 | }
445 |
--------------------------------------------------------------------------------
/src/main/java/exemplos/part3/Cap15InstantEChronoFields.java:
--------------------------------------------------------------------------------
1 | package exemplos.part3;
2 |
3 | import static exemplos.setup.Setup.setup;
4 |
5 | import java.time.DateTimeException;
6 | import java.time.DayOfWeek;
7 | import java.time.Instant;
8 | import java.time.LocalDate;
9 | import java.time.LocalDateTime;
10 | import java.time.LocalTime;
11 | import java.time.MonthDay;
12 | import java.time.OffsetDateTime;
13 | import java.time.Year;
14 | import java.time.YearMonth;
15 | import java.time.ZoneId;
16 | import java.time.ZoneOffset;
17 | import java.time.ZonedDateTime;
18 | import java.time.format.DateTimeFormatter;
19 | import java.time.temporal.ChronoField;
20 | import java.time.temporal.TemporalAccessor;
21 | import java.time.temporal.TemporalQuery;
22 | import java.time.temporal.UnsupportedTemporalTypeException;
23 | import java.time.temporal.ValueRange;
24 | import java.util.Arrays;
25 | import java.util.Collections;
26 | import java.util.EnumSet;
27 | import java.util.List;
28 |
29 | import exemplos.setup.Setup;
30 |
31 | // trata da classe java.time.Instant e como usar java.time.temporal.TemporalField e java.time.temporal.ChronoField
32 | public class Cap15InstantEChronoFields {
33 |
34 | static {
35 | // setar configurações, ver javadoc da classe Setup para mais detalhes
36 | setup();
37 | }
38 |
39 | public static void main(String[] args) {
40 | testInstant();
41 | conversoes();
42 | comparacaoOrdenacao();
43 | usarChronoField();
44 | getVSgetLong();
45 | camposDoInstant();
46 | mudarCampos();
47 | criarTemporalQuery();
48 | usarMethodReferenceComoTemporalQuery();
49 | outrosTipos();
50 | verificarAnoBissexto();
51 | outrosExemplosTemporalQuery();
52 | }
53 |
54 | static void testInstant() {
55 | // instante (timestamp) atual
56 | Instant agora = Instant.now();
57 | // usando data/hora atual simulada
58 | agora = Instant.now(Setup.clock());
59 | System.out.println(agora); // 2018-05-04T20:00:00Z
60 |
61 | // obter o valor do timestamp
62 | long timestamp = agora.toEpochMilli(); // valor em milissegundos
63 | System.out.println(timestamp); // 1525464000000
64 |
65 | // ou
66 | long timestampSegundos = agora.getEpochSecond(); // valor em segundos
67 | int nanossegundos = agora.getNano(); // nanossegundos
68 | System.out.println(timestampSegundos + "." + nanossegundos); // 1525464000.0
69 |
70 | // obter um Instant a partir do timestamp
71 | // usar timestamp em milissegundos
72 | Instant instant = Instant.ofEpochMilli(1525464000000L);
73 | // usar timestamp em segundos
74 | Instant instant2 = Instant.ofEpochSecond(1525464000L);
75 | System.out.println(instant); // 2018-05-04T20:00:00Z
76 | System.out.println(instant2); // 2018-05-04T20:00:00Z
77 |
78 | // usar timestamp em segundos, mais os nanossegundos
79 | instant = Instant.ofEpochSecond(1525464000L, 123456789);
80 | System.out.println(instant); // 2018-05-04T20:00:00.123456789Z
81 | // mas toEpochMilli trunca o valor dos nanossegundos
82 | System.out.println(instant.toEpochMilli()); // 1525464000123
83 | }
84 |
85 | static void conversoes() {
86 | // 4 de maio de 2018
87 | LocalDate data = LocalDate.of(2018, 5, 4);
88 |
89 | // LocalDate só tem o dia, mês e ano. Para converter para Instant, precisamos saber o horário e o offset
90 |
91 | // usando um ZonedDateTime, o offset é calculado usando as regras do timezone
92 | // atStartOfDay seta o horário para meia-noite: 2018-05-04T00:00-03:00[America/Sao_Paulo]
93 | ZonedDateTime zdt = data.atStartOfDay(ZoneId.of("America/Sao_Paulo"));
94 | System.out.println(zdt.toInstant()); // 2018-05-04T03:00:00Z
95 | // usar um offset específico (em vez de um timezone): 2018-05-04T00:00+05:00
96 | OffsetDateTime odt = data.atTime(0, 0).atOffset(ZoneOffset.of("+05:00"));
97 | System.out.println(odt.toInstant()); // 2018-05-03T19:00:00Z
98 | }
99 |
100 | static void comparacaoOrdenacao() {
101 | Instant maio = OffsetDateTime.of(2018, 5, 1, 10, 0, 0, 0, ZoneOffset.UTC).toInstant();
102 | Instant dezembro = OffsetDateTime.of(2018, 12, 1, 10, 0, 0, 0, ZoneOffset.UTC).toInstant();
103 | Instant janeiro = OffsetDateTime.of(2018, 1, 1, 10, 0, 0, 0, ZoneOffset.UTC).toInstant();
104 |
105 | // isBefore e isAfter considera a ordem cronológica (internamente, compara o valor do timestamp)
106 | System.out.println(janeiro.isBefore(maio)); // true
107 | System.out.println(dezembro.isAfter(maio)); // true
108 |
109 | // ZonedDateTime que corresponde ao mesmo instante (ao mesmo timestamp)
110 | ZonedDateTime zdt = ZonedDateTime.of(2018, 5, 1, 7, 0, 0, 0, ZoneId.of("America/Sao_Paulo"));
111 | System.out.println(maio.equals(zdt.toInstant())); // true
112 |
113 | // ordenação considera a ordem cronológica
114 | List list = Arrays.asList(dezembro, maio, janeiro);
115 | Collections.sort(list);
116 | System.out.println(list); // [2018-01-01T10:00:00Z, 2018-05-01T10:00:00Z, 2018-12-01T10:00:00Z]
117 | }
118 |
119 | static void usarChronoField() {
120 | // 2018-05-04T17:00
121 | LocalDateTime data = LocalDateTime.of(2018, 5, 4, 17, 0);
122 | int mes = data.get(ChronoField.MONTH_OF_YEAR);
123 | System.out.println(mes); // 5 -> o mesmo que data.getMonthValue()
124 | int minuto = data.get(ChronoField.MINUTE_OF_HOUR);
125 | System.out.println(minuto); // 0 -> o mesmo que data.getMinute()
126 |
127 | // obter o minuto do dia (este campo não tem um getXXX equivalente, o único jeito de obtê-lo é usar ChronoField)
128 | int minutoDoDia = data.get(ChronoField.MINUTE_OF_DAY);
129 | System.out.println(minutoDoDia); // 1020 -> desde o início do dia (meia-noite), passaram-se 1020 minutos
130 |
131 | Instant instant = Instant.ofEpochSecond(1525464000L, 123456789);
132 | // ChronoField.MILLI_OF_SECOND retorna a fração de segundos em milissegundos
133 | int milissegundos = instant.get(ChronoField.MILLI_OF_SECOND);
134 | System.out.println(milissegundos); // 123 -> o mesmo que instant.getNano() / 1000000 (mas sem precisar fazer a conta)
135 | }
136 |
137 | static void getVSgetLong() {
138 | // 2018-05-04T17:00
139 | LocalDateTime dataHora = LocalDateTime.of(2018, 5, 4, 17, 0);
140 |
141 | try {
142 | // quantos nanossegundos se passaram desde a meia-noite, usar get() lança exceção
143 | int nanossegundosDoDia = dataHora.get(ChronoField.NANO_OF_DAY);
144 | System.out.println(nanossegundosDoDia); // não será executado, pois get() vai lançar exceção
145 | } catch (UnsupportedTemporalTypeException e) {
146 | System.out.println(e.getMessage()); // Invalid field 'NanoOfDay' for get() method, use getLong() instead
147 | }
148 |
149 | // obter os limites para os valores de NANO_OF_DAY
150 | ValueRange limites = dataHora.range(ChronoField.NANO_OF_DAY);
151 | System.out.println(limites); // 0 - 86399999999999
152 | /*
153 | * O valor máximo do campo é 86399999999999, que é mais do que um int suporta (por isso get() lança UnsupportedTemporalTypeException)
154 | *
155 | * O maior valor que um int pode ter é 2147483647 (valor da constante Integer.MAX_VALUE)
156 | *
157 | * Em vez de usar get() (que retorna um int), usamos getLong(), que retorna um long
158 | */
159 | long nanossegundosDoDia = dataHora.getLong(ChronoField.NANO_OF_DAY);
160 | System.out.println(nanossegundosDoDia); // 61200000000000
161 |
162 | // O método range ajusta os valores mínimo e máximo de acordo com o contexto. Ex:
163 | // verificar dia do mês para datas em fevereiro
164 | System.out.println(LocalDate.of(2018, 2, 1).range(ChronoField.DAY_OF_MONTH)); // 1 - 28
165 | System.out.println(LocalDate.of(2020, 2, 1).range(ChronoField.DAY_OF_MONTH)); // 1 - 29
166 | // Repare que o valor máximo para o dia do mês pode ser 28 ou 29 (pois depende se o ano é bissexto ou não)
167 | }
168 |
169 | static void camposDoInstant() {
170 | // 2018-05-04T20:00:00Z
171 | Instant instant = Instant.ofEpochSecond(1525464000L);
172 | try {
173 | // tentar obter a hora
174 | System.out.println(instant.get(ChronoField.HOUR_OF_DAY));
175 | } catch (UnsupportedTemporalTypeException e) {
176 | System.out.println(e.getMessage()); // Unsupported field: HourOfDay
177 | /*
178 | * Isso acontece porque a classe Instant não representa uma data e hora específicas, e sim um timestamp: um valor que corresponde a uma data e hora
179 | * diferentes em cada timezone. Por isso não é possível obter um valor para as horas, e nem para qualquer outro campo de data ou hora, como o dia,
180 | * mês, minutos etc. Para obter estes valores, temos que converter o Instant para um timezone ou offset, usando os métodos atZone(ZoneId) e
181 | * atOffset(ZoneOffset), conforme abaixo
182 | */
183 | }
184 |
185 | // converter para um timezone
186 | ZonedDateTime sp = instant.atZone(ZoneId.of("America/Sao_Paulo"));
187 | System.out.println(sp); // 2018-05-04T17:00-03:00[America/Sao_Paulo]
188 | // converter para um offset
189 | OffsetDateTime odt = instant.atOffset(ZoneOffset.of("+05:00"));
190 | System.out.println(odt); // 2018-05-05T01:00+05:00
191 | // agora sim é possível usar o ChronoField
192 | System.out.println(sp.get(ChronoField.HOUR_OF_DAY)); // 17
193 | System.out.println(odt.get(ChronoField.HOUR_OF_DAY)); // 1
194 |
195 | // se quiser a data e hora em UTC, use ZoneOffset.UTC
196 | OffsetDateTime utcDateTime = instant.atOffset(ZoneOffset.UTC);
197 | // use getHour() para obter a hora (ou .get(ChronoField.HOUR_OF_DAY))
198 | System.out.println(utcDateTime.getHour()); // 20
199 |
200 | // antes de usar get() ou getLong(), verifique se o campo é suportado
201 | if (instant.isSupported(ChronoField.HOUR_OF_DAY)) {
202 | // nesse caso, não entra no if, porque Instant não suporta o campo HOUR_OF_DAY
203 | System.out.println(instant.get(ChronoField.HOUR_OF_DAY));
204 | // Lembrando que mesmo que o campo seja suportado, o método get() ainda pode lançar uma exceção, caso o valor exceda os limites de um int.
205 | }
206 | }
207 |
208 | static void mudarCampos() {
209 | // 2018-05-04T20:00:00Z
210 | Instant instant = Instant.ofEpochSecond(1525464000L);
211 | // mudar os milissegundos
212 | instant = instant.with(ChronoField.MILLI_OF_SECOND, 123);
213 | // with sempre retorna outra instância, então não esqueça de atribuir o retorno do método em alguma variável
214 | System.out.println(instant); // 2018-05-04T20:00:00.123Z
215 |
216 | // 17:00
217 | LocalTime hora = LocalTime.of(17, 0);
218 | // mudar para o centésimo segundo do dia
219 | hora = hora.with(ChronoField.SECOND_OF_DAY, 100);
220 | System.out.println(hora); // 00:01:40
221 | }
222 |
223 | static void criarTemporalQuery() {
224 | // criar TemporalQuery ("temporal" é um TemporalAccessor)
225 | TemporalQuery fimDeSemana = temporal -> {
226 | // valor numérico do dia da semana
227 | int diaDaSemana = temporal.get(ChronoField.DAY_OF_WEEK);
228 | // comparar com o valor numérico de sábado e domingo
229 | return diaDaSemana == DayOfWeek.SATURDAY.getValue() || diaDaSemana == DayOfWeek.SUNDAY.getValue();
230 | };
231 | // 4 de maio de 2018
232 | LocalDate data = LocalDate.of(2018, 5, 4);
233 | System.out.println(data.query(fimDeSemana)); // false
234 |
235 | // TemporalQuery funciona com qualquer classe que implemente TemporalAccessor. Ou seja, eu posso usá-lo com qualquer tipo de data e hora da API, desde
236 | // que os campos sendo usados sejam suportados:
237 | // 6 de maio de 2018 (domingo)
238 | LocalDateTime dataHora = LocalDateTime.of(2018, 5, 6, 17, 0);
239 | System.out.println(dataHora.query(fimDeSemana)); // true
240 | // converte para um timezone, data continua a mesma
241 | ZonedDateTime dataHoraSP = dataHora.atZone(ZoneId.of("America/Sao_Paulo"));
242 | System.out.println(dataHoraSP.query(fimDeSemana)); // true
243 | try {
244 | // tentar usar com um LocalTime
245 | LocalTime hora = LocalTime.now();
246 | System.out.println(hora.query(fimDeSemana)); // exceção
247 | } catch (UnsupportedTemporalTypeException e) {
248 | // LocalTime não tem campos de data, então DAY_OF_WEEK não é suportado
249 | System.out.println(e.getMessage()); // Unsupported field: DayOfWeek
250 | }
251 | }
252 |
253 | static void usarMethodReferenceComoTemporalQuery() {
254 | // 4 de maio de 2018
255 | LocalDate data = LocalDate.of(2018, 5, 4);
256 | // usar method reference como um TemporalQuery
257 | // (útil se você já tiver essa classe no seu código legado, assim não precisa duplicar o código em uma TemporalQuery)
258 | boolean fds = data.query(DateUtils::isFimDeSemana);
259 | System.out.println(fds); // false
260 | }
261 |
262 | static class DateUtils {
263 | // Como DayOfWeek é um enum, posso usá-lo em um EnumSet
264 | private static final EnumSet FIM_DE_SEMANA = EnumSet.of(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY);
265 |
266 | public static boolean isFimDeSemana(TemporalAccessor temporal) {
267 | // obter um DayOfWeek ao invés do valor numérico do campo
268 | DayOfWeek diaDaSemana = DayOfWeek.from(temporal);
269 | // verificar se é sábado ou domingo
270 | return FIM_DE_SEMANA.contains(diaDaSemana);
271 | }
272 | }
273 |
274 | // -------------------------------------------------
275 | // Outros tipos de data e hora
276 | static void outrosTipos() {
277 | // maio de 2018 (apenas o mês e ano, sem nenhuma informação sobre o dia)
278 | // Um uso para YearMonth é a data de validade de cartão de crédito (só possui o mês e ano)
279 | YearMonth maio = YearMonth.of(2018, 5);
280 | LocalDate dt = maio.atDay(1); // 1 de maio de 2018
281 | System.out.println(dt); // 2018-05-01
282 | dt = maio.atEndOfMonth(); // último dia do mês (já considerando regras do ano bissexto se o mês for fevereiro)
283 | System.out.println(dt); // 2018-05-31
284 |
285 | // janeiro de 2020
286 | YearMonth jan2020 = maio
287 | // mudar o mês para janeiro
288 | .withMonth(1)
289 | // somar 2 anos
290 | .plusYears(2);
291 | System.out.println(jan2020); // 2020-01 (formato ISO 8601 para "ano e mês sem o dia")
292 |
293 | // MonthDay possui apenas dia e mês (sem nenhuma informação sobre o ano)
294 | // É útil para datas recorrentes. Ex:
295 | MonthDay natal = MonthDay.of(12, 25); // 25 de dezembro
296 | System.out.println(natal); // --12-25 (formato ISO 8601 para "mês e dia sem o ano")
297 | // Os "--" na frente é para evitar ambiguidade com ano-mês: se fosse "12-25" poderia ser confundido com "ano 12 e mês 25" (inválido)
298 |
299 | // Natal deste ano -> usando a classe java.time.Year, que representa somente um ano (sem nenhuma informação sobre dia ou mês)
300 | int anoAtual = Year.now().getValue();
301 | // mas para este exemplo, vou usar a data/hora atual simulada
302 | anoAtual = Year.now(Setup.clock()).getValue(); // 2018
303 | LocalDate natalDesteAno = natal.atYear(anoAtual);
304 | System.out.println(natalDesteAno); // 2018-12-25
305 |
306 | // todas as classes possuem um método from() para serem criadas
307 | ZonedDateTime zdt = ZonedDateTime.now(Setup.clock());
308 | YearMonth ym = YearMonth.from(zdt); // extrai apenas o ano e mês do ZonedDateTime
309 | System.out.println(ym); // 2018-05
310 |
311 | MonthDay md = MonthDay.from(zdt);// extrai apenas o dia e mês do ZonedDateTime
312 | System.out.println(md); // --05-04
313 |
314 | DayOfWeek dow = DayOfWeek.from(zdt); // seria uma alternativa se ZonedDateTime não tivesse o método getDayOfWeek()
315 | System.out.println(dow); // FRIDAY
316 | }
317 |
318 | // várias formas de verificar se um ano é bissexto
319 | static void verificarAnoBissexto() {
320 | int ano = 2020;
321 | // Se você tiver apenas o valor numérico do ano e só quiser saber se é bissexto ou não, o jeito mais simples é:
322 | boolean bissexto = Year.isLeap(ano);
323 | System.out.println(bissexto); // true
324 |
325 | // PS: vou usar o mesmo ano nos próximos exemplos, então o resultado também será "true"
326 |
327 | // outra forma é criar um Year (vale a pena se vc vai usar este objeto para outras coisas depois, senão use o método acima)
328 | Year year = Year.of(ano);
329 | bissexto = year.isLeap(); // true
330 | /*
331 | * Este método vale a pena, por exemplo, se você quiser verificar o ano atual: Year.now().isLeap() verifica se o ano atual é bissexto
332 | *
333 | * É melhor do que fazer Year.isLeap(Year.now().getValue()), por exemplo.
334 | *
335 | * Mas se você já tiver o valor numérico do ano (em um int ou long), aí o melhor é usar Year.isLeap(ano) mesmo
336 | */
337 |
338 | // se você já tiver um ZonedDateTime, OffsetDateTime ou LocalDateTime, basta converter para LocalDate e usar o método isLeapYear()
339 | ZonedDateTime zdt = ZonedDateTime.of(ano, 5, 1, 10, 0, 0, 0, ZoneId.systemDefault());
340 | bissexto = zdt.toLocalDate().isLeapYear(); // true
341 | // OU passar o valor do ano diretamente para Year
342 | bissexto = Year.isLeap(zdt.getYear()); // true
343 | // PS: se vc já tiver uma instância de LocalDate, use isLeapYear() diretamente -> não precisa fazer Year.isLeap(localdate.getYear())
344 |
345 | // se você já tiver um YearMonth, basta usar o método isLeapYear()
346 | YearMonth ym = YearMonth.of(ano, 1);
347 | bissexto = ym.isLeapYear(); // true
348 |
349 | // ****************************************************************************************************
350 | // Os exemplos acima são as formas mais simples e as que eu recomendo usar (escolha a mais adequada para a sua situação)
351 |
352 | // Ainda há outras mais "ortodoxas" e deixo aqui apenas como curiosidade (não recomendo usar em produção, já que os exemplos acima são bem melhores)
353 |
354 | // tentar criar "29 de fevereiro" no ano em questão
355 | try {
356 | LocalDate.of(ano, 2, 29);
357 | bissexto = true;
358 | // se o ano for bissexto, a exceção não é lançada
359 | } catch (DateTimeException e) {
360 | bissexto = false;
361 | }
362 |
363 | // criar YearMonth com mês igual a fevereiro e verificar se a quantidade de dias deste mês é 29
364 | bissexto = YearMonth.of(ano, 2).lengthOfMonth() == 29; // lengthOfMonth() leva em conta se o ano é bissexto
365 | System.out.println(bissexto); // true
366 |
367 | // criar um MonthDay para 29 de fevereiro e verificar o ano
368 | MonthDay md = MonthDay.of(2, 29);
369 | bissexto = md.isValidYear(ano); // true
370 | }
371 |
372 | // mais alguns exemplos de uso de TemporalQuery
373 | static void outrosExemplosTemporalQuery() {
374 | // saudação baseada no horário
375 | TemporalQuery saudacao = temporal -> {
376 | int hora = temporal.get(ChronoField.HOUR_OF_DAY);
377 | if (hora < 12) { // considera que de madrugada também é "Bom dia
378 | return "Bom dia";
379 | }
380 |
381 | if (12 <= hora && hora < 18) {
382 | return "Boa tarde";
383 | }
384 |
385 | return "Boa noite";
386 | };
387 | // usar data/hora atual simulada: 4 de Maio de 2018, às 17:00 em São Paulo
388 | ZonedDateTime dataHoraSP = ZonedDateTime.now(Setup.clock());
389 | System.out.println(dataHoraSP.query(saudacao)); // Boa tarde
390 | // em Tóquio, corresponde a 5 de maio, às 05:00 da manhã
391 | ZonedDateTime dataHoraTokyo = dataHoraSP.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
392 | System.out.println(dataHoraTokyo.query(saudacao)); // Bom dia
393 |
394 | // reaproveitar a query anterior para incrementar a mensagem
395 | DateTimeFormatter formatter = DateTimeFormatter.ofPattern(", 'agora são' HH:mm:ss");
396 | TemporalQuery saudacao2 = temporal -> {
397 | String msg = temporal.query(saudacao); // obter a saudação
398 | return msg + formatter.format(temporal); // mostrar o horário, de acordo com pattern definido por DateTimeFormatter
399 | };
400 | System.out.println(dataHoraSP.query(saudacao2)); // Boa tarde, agora são 17:00:00
401 | System.out.println(dataHoraTokyo.query(saudacao2)); // Bom dia, agora são 05:00:00
402 |
403 | // Lembrando que TemporalQuery recebe um TemporalAccessor (ou seja, qualquer classe de data e hora) e pode retornar qualquer coisa que você precisar
404 | // (não precisa se limitar às classes nativas do Java). O único detalhe é que, como TemporalAccessor é uma interface-base para todos os tipos, nem
405 | // sempre todos os campos estarão disponíveis. Por exemplo, LocalDate não tem campos de horário, então não funciona com as queries acima.
406 | // Se quiser, você pode usar isSupported() para verificar se o campo é suportado, e lançar exceção ou mostrar uma mensagem de erro quando não for.
407 | }
408 | }
409 |
--------------------------------------------------------------------------------