├── .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 | --------------------------------------------------------------------------------