C, PHP, VB, .NET

Дневникът на Филип Петров


* Работа с дата и час

Публикувано на 09 декември 2015 в раздел ПИК3 Java.

В общи линии още от своето създаване Java винаги е търпяла критики към своята библиотека за работа с дата и час. Стандартните класове java.util.Date и java.util.Calendar имат редица проблеми:

  • Сбъркан дизайн: например Date не е дата, а е всъщност timestamp, месеците започват от 0. Дори да свикнете с това, дните пък си започват от 1. И т.н.;
  • Много трудна работа с часови зони;
  • Не можете да имате стандартни неща като дати без години, часове без отчитане на секунди и т.н.;
  • Няма вградена синхронизация за работа в многонишкова среда.

През годините паралелно с JDK се е развивала една библиотека наречена Joda, която решава тези проблеми. В крайна сметка с Java 8 се въвежда пакета java.time, който е с работно име JSR 310 и е базиран на библиотеката Joda, но има известни разлики с нея. В тази статия ще разгледаме именно JSR 310, защото от тук нататък това ще е стандартната библиотека за работа с дата и час в Java.

Четирите най-основни класа за работа с java.time са:

  • LocalDateTime/ZonedDateTime - включва в себе си едновременно дата и час;
  • LocalDate - включва само дата;
  • LocalTime - включва само час;
  • Instant - клас за работа с timestamp по Unix епохата.

Заедно с примерите, които ще даваме, ще показваме и други класове, които ще ни помагат за осъществяване на една или друга функционалност. Нека създадем обект за дата и час с текущото време:

LocalDateTime ldt = LocalDateTime.now();
System.out.println(ldt); // 2015-12-08T10:21:35.551

Така създадената дата и час ще вземе текущото време на машината, на която се изпълнява - в примера сме пуснали кода на компютър в Англия и часовата зона е по Гринуич. Ако искате да използвате времева зона, която е различна от локалната на машината, трябва да я подадете като параметър на метод now:

ldt = LocalDateTime.now(ZoneId.of("Europe/Sofia"));
System.out.println(ldt); // 2015-12-08T12:21:35.624

Така зададените дата и час ще показват текущото време в София, обаче обекта сам вътре в себе си не пази часовата зона, по която е бил създаден! ZoneId има смисъл за LocalDateTime само и единствено при създаването на обекта чрез използване на метод now(). Затова и времето е "локално". Ако искаме да пазим зоната и след това да я сменяме, използваме клас ZonedDateTime. Той е подобен на LocalDateTime (има почти същите методи), но допълнително пази часовата зона.

ZoneId sofiaZoneId =  ZoneId.of("Europe/Sofia");
LocalDateTime currentTimeInSofia =  LocalDateTime.now(sofiaZoneId);
// Въпреки, че първоначално указахме зона, сега пак трябва изрично да я укажем!!!
// Причината е, че LocalDateTime не я пази!
ZonedDateTime zdtInSofia = ZonedDateTime.of(currentTimeInSofia, sofiaZoneId);
System.out.println(zdtInSofia); // 2015-12-08T12:56:48.438+02:00[Europe/Sofia]
ZonedDateTime zdtInParis = zdtInSofia.withZoneSameInstant(ZoneId.of("Europe/Paris"));
System.out.println(zdtInParis); // 2015-12-08T11:56:48.438+01:00[Europe/Paris]

Естествено не е задължително да използвате метод "now()" - можете да задавате произволни дата и час. Например ако искаме да зададем тази Коледа по обяд (2 часа), можем да направим следното:

ldt = LocalDateTime.of(2015, 12, 25, 12, 00);
System.out.println(ldt); // 2015-12-25T12:00

Метод of е дефиниран по много начини. Както виждате в този случай използвахме само дата и час, но без секунди и наносекунди.

LocalDateTime е immutable - веднъж създаден, не може да бъде променян. Това е огромно предимство при работа в многонишкова среда, но за сметка на това, подобно на работата със String, може да ни предизвика известни неудобства - ако искаме да променим дадена дата и час, трябва да генерираме нов обект. Един удобен начин за правене на това е с withXXX(int) методите - година (withYear), месец (withMonth), ден (withDayOfMonth), час (withHour) и т.н. Те генерират нов обект на базата на стария, като се променя една негова характеристика( Нека променим горната дата да бъде Коледа на 2016 г.:

ldt = ldt.withYear(2016);
System.out.println(ldt); // 2016-12-25T12:00

Тези методи могат да се използват подобно на генератори на функции. Например ако искаме да променим датата да бъде 3-ти март в 10 часа сутринта, ще направим следното:

ldt = ldt.withMonth(3).withDayOfMonth(3).withHour(10);
System.out.println(ldt); // 2016-03-03T10:00

По-общия метод with() приема обект работещ по интерфейс java.time.temporal.TemporalAdjuster. Заедно с него има един utility клас java.time.temporal.TemporalAdjusters, който предлага редица полезни за нас методи, част от които са firstDayOfMonth - първия ден на текущия месец, firstDayOfNextMonth - първи ден на следващия месец,  firstDayOfYear - първи ден на тази година, firstDayOfNextYear (първи ден на следващата година), firstOfMonth(DayOfWeek) - първия ден от този месец, който е понеделник /вторник/ и т.н. спрямо подадения параметър, lastDayOfMonth - последния ден на текущия месец, lastDayOfYear - последния ден на текущата година, lastInMonth(DayOfWeek) - последния понеделник /вторник/ и т.н. на текущия месец спрямо подадения параметър, next(DayOfWeek) - следващия понеделник /вторник/ и т.н. след текущата дата, nextOrSame(DayOfWeek) - следващия понеделник /вторник/ и т.н. след текущата дата или текущия ден, ако самия той е такъв, previous/previousOrSame(DayOfWeek) - аналогични на next и nextOrSame, и т.н.

Ето един практически пример - искаме да намерим първият петък на следващия месец точно в 12 часа на обяд. Ще направим следното:

LocalDateTime firstFridayOfNextMonth = LocalDateTime
    .now()
    .with(TemporalAdjusters.firstDayOfNextMonth())
    .with(TemporalAdjusters.nextOrSame(DayOfWeek.FRIDAY))
    .withHour(12)
    .withMinute(0)
    .withSecond(0)
    .withNano(0);
System.out.println(firstFridayOfNextMonth);

Естествено има и редица get методи, с които да извиквате части от датата и часа:

System.out.println("Year: "+ldt.getYear());        // 2016
System.out.println("Month: "+ldt.getMonth());      //    3
System.out.println("Day: "+ldt.getDayOfMonth());   //    3
System.out.println("Hour: "+ldt.getHour());        //   10
System.out.println("Minutes: "+ldt.getMinute());   //    0
System.out.println("Seconds: "+ldt.getSecond());   //    0
System.out.println("Nanoseconds: "+ldt.getNano()); //    0

Също така има поредица от функции plusXXX и minusXXX, с които можете да генерирате нов обект, който е изместен напред или назад във времето. Например ако се интересуваме каква ще е датата и часа след 2 седмици без 5 часа спрямо текущото време, можем да направим следното:

ldt = LocalDateTime.now().plusWeeks(2).minusHours(5);
System.out.println(ldt);

Класовете java.time.LocalDate и java.time.LocalTime работят по същия начин като LocalDateTime, но (логично) имат по-малко функционалности спрямо него. Ето например как можем да създадем обекти с рождената ми дата, днешната дата и текущия час:

LocalDate myBirthday = LocalDate.of(1983, 3, 26);
LocalDate currentDate = LocalDate.now();
LocalTime currentTime = LocalTime.now();
System.out.println(currentDate); // 2015-12-08
System.out.println(currentTime); // 11:20:23.493
System.out.println(myBirthday);  // 1983-03-26

Можете да използвате и допълнителни методи за генериране на такива обекти. Например ако искаме да видим кой е бил 100-ния ден на 1989 г. или колко е часа по време на 10 000-та секунда от деня, ще направим следното:

LocalDate hundredthDay1989 = LocalDate.ofYearDay(1989, 100);
System.out.println(hundredthDay1989);    // 1989-04-10
LocalTime tenThousandthSecond = LocalTime.ofSecondOfDay(10000);
System.out.println(tenThousandthSecond); // 02:46:40

Тези класове имат аналогичните на LocalDateTime методи plusXXX, minusXXX и withXXX. Тук можем да се досетивм, че след като показахме как можем да преместваме дата и/или час на даден обект, би трябвало да можем да правим и аритметика с дати. Това се осъществява чрез специален клас java.time.Period. Ето например как мога да изчисля на колко години съм, на колко месеци и на колко дни съм:

Period myLifePeriod = Period.between(myBirthday, currentDate);
System.out.println(myLifePeriod.getYears());  // 32
System.out.println(myLifePeriod.getMonths()); //  8
System.out.println(myLifePeriod.getDays());   // 12

Понякога това не ни е достатъчно. Например бих искал да изчисля на точно колко дни съм, т.е. да преобразувам годината и месеците в дни. Това може да се осъществи чрез enum java.time.temporal.ChronoUnit. За конкретния пример:

long myLifeInDays = ChronoUnit.DAYS.between(myBirthday, currentDate);
System.out.println("I am "+myLifeInDays+" days old"); // I am 11945 days old

Тези периоди и разлики могат да бъдат използвани обратно за методите plusXXX и minusXXX. Нека например имаме коледен базар, който започва на 20 декември 2015 г. и трае една седмица. Можем да изчислим крайната дата на този базар по следния начин:

LocalDate bazarStartDate = LocalDate.of(2015, 12, 20);
Period bazarDuration = Period.ofWeeks(1); // 1 седмица
LocalDate bazarEndDate = bazarStartDate.plus(bazarDuration);
System.out.println("Starts "+bazarStartDate);
System.out.println("Ends "+bazarEndDate);
System.out.println("Duration is "+bazarDuration.get(ChronoUnit.DAYS)+" days");

Резултатът ще бъде:

Starts 2015-12-20
Ends 2015-12-27
Duration is 7 days

Забележете за начина, по който е реализиран get метода на клас Period - той може да приема различните константи от ChronoUnit, с което вътрешно превръща периода в подходящи за вас мерни единици.

Клас java.time.Duration е подобен на Period, но вече не работи с години, месеци и дни, а с часове, минути, секунди и наносекунди. Например ако искаме да видим колко секунди остават от сегашния момент до полунощ, можем да използваме:

Duration toMidnight = Duration.between(LocalTime.MIDNIGHT, LocalTime.now());
System.out.println(toMidnight.get(ChronoUnit.SECONDS)); // 42110

Тук при употребата на Duration много уместно се намесват и времевите зони. Например:

Задача. Ще летя със самолет от София за Ню Йорк. Знам, че с прекачването пътуването ми ще трае 12 часа и 35 минути. Качвам се на самолета на 2015-12-20 в 16:00 следобяд. Колко ще е часа в Ню Йорк по време на пристигането ми по тяхната часова зона?

Решение: Ще се възползваме от ZonedDateTime

// Дата и час на заминаване
LocalDateTime leaveLDT = LocalDateTime.of(2015, 12, 20, 16, 00);
// Указваме, че става дума за софийската часова зона
// Тук няма отместване на часа - ще продължи да си е 16 часа,
// независимо къде географски се намира компютъра, на който се изпълнява кода
ZonedDateTime leaveZDT_SF = ZonedDateTime.of(leaveLDT, ZoneId.of("Europe/Sofia"));
// I am leaving at 2015-12-20T16:00+02:00[Europe/Sofia] Sofia time
System.out.println("I am leaving at "+leaveZDT_SF+" Sofia time");
// Колко време ще се лети?
Duration flightDuration = Duration.ofHours(12).plusMinutes(35);
// Отместваме часовата зона на Ню Йорк и добавяме времетраенето на полета
ZonedDateTime arriveZDT_NY = 
   leaveZDT_SF
      .withZoneSameInstant(ZoneId.of("America/New_York"))
      .plus(flightDuration);
// I arrive at 2015-12-20T21:35-05:00[America/New_York] NY time
System.out.println("I arrive at "+arriveZDT_NY+" NY time");

Нека сега се запознаем с "парсване" на дати и часове. Прочели сме дата и час в някакъв String и искаме да го превърнем в обект LocalDateTime - няма по-лесно нещо с новата библиотека. За целта ще използваме метод parse, който като втори параметър приема обект от клас java.time.format.DateTimeFormater:

// Някаква дата и час записани в произволен формат
String myDT = "Jan 3, 2016 @12:09";
// Създаваме Pattern отговарящ на този формат
DateTimeFormatter dtformat = DateTimeFormatter.ofPattern("MMM d, y @HH:mm", java.util.Locale.ENGLISH);
try{
   LocalDateTime myLDT = LocalDateTime.parse(myDT, dtformat);
   // 2016-01-03T12:09
   System.out.println(myLDT);
}
catch(DateTimeParseException e){
   System.out.println("Can't parse");
}

Същият клас може да се използва и за обратното - преобразуване на обект дата и/или час в текстови низ. Например:

java.util.Locale bgLocale = new java.util.Locale("bg", "BG");
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd MMMM yyyy, HH:mm", bgLocale);
LocalDateTime currentDT = LocalDateTime.now(ZoneId.of("Europe/Sofia"));
// 09 Декември 2015, 14:05
System.out.println(currentDT.format(formatter));

Единственото сложно тук е да свикнете с правенето на Patterns. Ще трябва да разгледате различните възможности в официалната документация на адрес:

https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html#patterns

Накрая ще демонстрираме клас java.time.format.DateTimeFormatterBuilder. С негова помощ можем по-лесно да съставяме много сложни маски от смесици от текст и дати/часове. Например:

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;
import java.time.format.TextStyle;
public class Test{
 public static void main(String[] args){
    DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
    DateTimeFormatter formatter = builder.appendLiteral("Случи се на ")
      .appendValue(ChronoField.DAY_OF_MONTH)
      .appendLiteral(" ")
      .appendText(ChronoField.MONTH_OF_YEAR, TextStyle.FULL)
      .appendLiteral(" (")
      .appendText(ChronoField.DAY_OF_WEEK, TextStyle.FULL_STANDALONE)
      .appendLiteral("), ")
      .appendPattern("u")
      .appendLiteral(" г. около ")
      .appendValue(ChronoField.HOUR_OF_DAY)
      .appendLiteral(" часа и ")
      .appendText(ChronoField.MINUTE_OF_HOUR, TextStyle.NARROW_STANDALONE)
      .appendLiteral(" минути.")
      .toFormatter(); 
    LocalDateTime dateTime = LocalDateTime.of(2014, 12, 10, 12, 30); 
    System.out.println(dateTime.format(formatter));
 }
}

Програмата ще отпечата текста "Случи се на 10 Декември (Сряда), 2014 г. около 12 часа и 30 минути.". Оставяме ви сами да си поиграете и да разучите класовете ChronoField и TextStyle, както и да обясните всяки един от демонстрираните методи на DateTimeFormatterBuilder.

Във всеки един от представените класове всъщност има още множество методи, които не сме разгледали - например булевите isBefore и isAfter за сравнение на дати. Не демонстрирахме и клас Instant, който е за работа с дата и час по unix timestamp формата. Има още огромен куп функционалности, които заслужават нашето внимание, но обема на една статия не го побира. Надявам се, че все пак успях да ви убедя, че java.time е една отлична библиотека, която достойно поставя Java на челните позиции сред другите езици що се отнася до работа с дата и час.

 



Добави коментар

Адресът на електронната поща няма да се публикува


*