JavaRush /Java блог /Random UA /Як не загубитися в часі - DateTime та Calendar

Як не загубитися в часі - DateTime та Calendar

Стаття з групи Random UA
Вітання! Сьогодні ми почнемо працювати з новим типом даних, з яким раніше не стикалися, а саме — з датами. Як не загубитися в часі - DateTime і Calendar - 1Що таке дата, думаю, пояснювати не потрібно:) В принципі, записати поточну дату і час у Java цілком можливо у звичайний рядок.
public class Main {
   public static void main(String[] args) {

       String date = "11 червня 2018 року";
       System.out.println(date);
   }
}
Але такий підхід має багато недоліків. Клас Stringстворено до роботи з текстом, і методи в нього відповідні. Якщо нам потрібно буде якось керувати датою (додати до неї 2 години, наприклад), Stringтут не впорається. Або, наприклад, вивести в консоль поточну дату та час на момент компіляції програми. Тут Stringтеж не допоможе: поки ти напишеш код і запустиш його - час зміниться і консоль буде виведено неактуальне. Тому Java його творцями були передбачені кілька класів до роботи з датами і часом. Перший з них – це класjava.util.Date

Клас Date Java

Ми вказали для нього повну назву, оскільки в іншому пакеті Java є ще клас java.sql.Date. Не переплутай! Перше, що потрібно про нього знати, він зберігає дату в мілісекундах , які пройшли з 1 січня 1970 року. Для цієї дати є навіть окрема назва - "Unix-час" Досить цікавий спосіб, згоден? :) Друге, що варто запам'ятати: якщо створити об'єкт Dateз порожнім конструктором - результатом буде поточна дата і час на момент створення об'єкта . Пам'ятаєш, ми писали, що для дати у форматі Stringтаке завдання буде проблематичним? Клас Dateїї легко вирішує.
public class Main {
   public static void main(String[] args) {

       Date date = new Date();
       System.out.println(date);
   }
}
Запусти цей код кілька разів, і побачиш, як час щоразу змінюватиметься:) Це можливо саме завдяки зберіганню в мілісекундах: вони є найменшою одиницею часу, тому результати настільки точні. Існує й інший конструктор для Date: можна вказати точну кількість мілісекунд, яка пройшла з 00:00 1 січня 1970 до необхідної дати, і вона буде створена:
public class Main {
   public static void main(String[] args) {

       Date date = new Date(1212121212121L);
       System.out.println(date);
   }
}
Виведення в консоль:

Fri May 30 08:20:12 MSD 2008
В нас вийшло 30 травня 2008 року. "Fri" означає день тижня - "Friday" (п'ятниця), а MSD - "Moscow Daylight Saving" (московський літній час). Мілісекунди передаються у форматі long, оскільки їх кількість найчастіше не влазить у int. Отже, які операції з датами нам можуть знадобитися у роботі? Ну, найочевидніше, звісно — порівняння . Визначити чи була одна дата пізніше чи раніше за іншу. Це можна зробити по-різному. Наприклад, можна викликати метод Date.getTime(). Він поверне кількість мілісекунд, що пройшли з півночі 1 січня 1970 року. Просто викличемо його у двох об'єктів Date і порівняємо між собою:
public class Main {
   public static void main(String[] args) {

       Date date1 = new Date();

       Date date2 = new Date();

       System.out.println((date1.getTime() > date2.getTime())?
               "date1 пізніше date2" : "date1 раніше date2");
   }
}
Висновок:

date1 раньше date2
Але є й зручніший спосіб, саме — використовувати спеціальні методи класу Date: before(), after()і equals(). Усі вони повертають результат у форматі boolean. Метод before()перевіряє, чи була наша дата раніше тієї, яку ми передаємо як аргумент:
public class Main {
   public static void main(String[] args) throws InterruptedException {

       Date date1 = new Date();

       Thread.sleep(2000);//припинимо роботу програми на 2 секунди
       Date date2 = new Date();

       System.out.println(date1.before(date2));
   }
}
Виведення в консоль:

true
Схожим чином працює і метод after(), він перевіряє, чи була наша дата пізніше тієї, яку ми передаємо як аргумент:
public class Main {
   public static void main(String[] args) throws InterruptedException {

       Date date1 = new Date();

       Thread.sleep(2000);//припинимо роботу програми на 2 секунди
       Date date2 = new Date();

       System.out.println(date1.after(date2));
   }
}
Виведення в консоль:

false
У прикладах ми “присипляємо” програму на 2 секунди, щоб дві дати гарантовано відрізнялися. На швидких комп'ютерах час між створенням date1і date2може бути меншим за одну мілісекунду, і в такому випадку обидва методи — і before(), і after()— повертатимуть false. А ось метод equals()у такій ситуації поверне true! Адже він порівнює саме кількість мілісекунд, що пройшли з 00:00 1 січня 1970 року для кожної дати. Об'єкти вважатимуться рівними тільки в тому випадку, якщо збігаються аж до мілісекунди:
public static void main(String[] args) {

   Date date1 = new Date();
   Date date2 = new Date();

   System.out.println(date1.getTime());
   System.out.println(date2.getTime());

   System.out.println(date1.equals(date2));
}
Ось ще на що треба звернути увагу. Якщо ти відкриєш документацію класу Dateна сайті Oracle, то побачиш, що багато його методів та конструкторів були позначені словом Deprecated(“нерекомендований”). Ось, подивися: Class Date Ось що самі творці Java говорять про ті частини класів, які стали deprecated: “Програмний елемент, анотований @Deprecated, є тим, що програмістам не рекомендується використовувати, як правило, тому, що це небезпечно, або тому, що є найкраща альтернатива.” Не означає, що цими методами взагалі не можна користуватися. Більше того, якщо ти сам спробуєш запустити код з їх використанням в IDEA - він, швидше за все, буде працювати Візьмемо для прикладу deprecated методDate.getHours(), який повертає кількість годинників з об'єкта Date.
public static void main(String[] args) {

   Date date1 = new Date();

   System.out.println(date1.getHours());

}
Якщо на момент запуску коду у вас, наприклад, час 14:21 він виведе число 14. Як бачите, deprecated-метод закреслено, але він цілком собі працює. Це методи не стали прибирати зовсім, щоб не зламати купу вже написаного з використанням коду. Тобто ці методи не "зламані" і не "віддалені", просто їх не рекомендують використовувати через наявність більш зручної альтернативи. Про неї, до речі, написано прямо в документації: Як не загубитися в часі - DateTime і Calendar - 2Більшість методів класу Date було перенесено до його покращеної, розширеної версії — класу Calendar. З ним ми й познайомимося далі:) Як не загубитися в часі - DateTime та Calendar - 3

Java Calendar

У версії Java 1.1 з'явився новий клас Calendar. Він зробив роботу з дат в Java дещо простіше, ніж вона виглядала раніше. Єдиною реалізацією класу Calendar, з якою ми і працюватимемо, є клас GregorianCalendar(він реалізує Григоріанський календар, за яким живе більшість країн світу). Його основна зручність полягає в тому, що він вміє працювати з датами у зручнішому форматі. Наприклад, він може:
  • Додати до поточної дати місяць або день
  • Перевірити, чи рік високосним;
  • Отримати окремі компоненти дати (наприклад, одержати з цілої дати номер місяця)
  • А також усередині нього розроблена дуже зручна система констант (багато з них ми побачимо нижче).
Ще однією важливою відмінністю класу Calendarє те, що в ньому реалізована константа Calendar.Era: ти можеш встановити для дати еру BC ("Before Christ" - до Різдва Христового, тобто "до нашої ери") або AC ("After Christ" - " Наша ера"). Давайте розглянемо все це на прикладах. Створимо календар із датою 25 січня 2017 року:
public static void main(String[] args) {

  Calendar calendar = new GregorianCalendar(2017, 0 , 25);
}
Місяці в класі Calendar(як і в Date, до речі) починаються з нуля, тому ми передали число 0 як другий аргумент. Головне при роботі з класом Calendar— розуміти, що це календар , а не окрема дата. Як не загубитися в часі - DateTime та Calendar - 3Дата — це кілька чисел, що позначають конкретний проміжок часу. А календар - це цілий пристрій, за допомогою якого можна багато чого робити з датами:) Це досить добре видно, якщо спробувати вивести об'єкт Calendar в консоль: Висновок:

java.util.GregorianCalendar[time=?,areFieldsSet=false,areAllFieldsSet=false,lenient=true,zone=sun.util.calendar.ZoneInfo[id="Europe/Moscow",offset=10800000,dstSavings=0,useDaylight=false,transitions=79,lastRule=null],firstDayOfWeek=2,minimalDaysInFirstWeek=1,ERA=?,YEAR=2017,MONTH=0,WEEK_OF_YEAR=?,WEEK_OF_MONTH=?,DAY_OF_MONTH=25,DAY_OF_YEAR=?,DAY_OF_WEEK=?,DAY_OF_WEEK_IN_MONTH=?,AM_PM=0,HOUR=0,HOUR_OF_DAY=0,MINUTE=0,SECOND=0,MILLISECOND=?,ZONE_OFFSET=?,DST_OFFSET=?]
Бачиш, скільки інформації! У календаря є купа властивостей, які не має звичайна дата, і всі вони виводяться в консоль (так працює метод toString()у класі Calendar). Якщо під час роботи тобі потрібно отримати з календаря просту дату, тобто. об'єкт Date— це робиться за допомогою методу Calendar.getTime()(назва не логічна, але тут вже нічого не поробиш):
public static void main(String[] args) {

   Calendar calendar = new GregorianCalendar(2017, 0 , 25);
   Date date = calendar.getTime();
   System.out.println(date);
}
Висновок:

Wed Jan 25 00:00:00 MSK 2017
Ось тепер ми спростабо календар до звичайної дати. Поїхали далі. Крім цифрових позначень місяців у класі Calendarможна використовувати константи. Константи - це статичні поля класу Calendarз встановленим значенням, яке не можна змінити. Цей варіант насправді навіть кращий, оскільки таке написання покращує читання коду.
public static void main(String[] args) {
   GregorianCalendar calendar = new GregorianCalendar(2017, Calendar.JANUARY , 25);
}
Calendar.JANUARY- Одна з констант для позначення місяця. При такому варіанті іменування ніхто не забуде, наприклад, що цифра "3" означає квітень, а не звичний нам третій місяць - березень. Просто пишеш Calendar.APRIL- і все:) Усі поля календаря (число, місяць, хвабони, секунди тощо) можна встановлювати окремо за допомогою методу set(). Він дуже зручний, оскільки в класі Calendarдля кожного поля виділено свою константу, і підсумковий код буде виглядати максимально просто. Наприклад, у минулому прикладі ми створабо дату, але не встановабо для неї поточний час. Давай встановимо час 19:42:12
public static void main(String[] args) {
   Calendar calendar = new GregorianCalendar();
   calendar.set(Calendar.YEAR, 2017);
   calendar.set(Calendar.MONTH, 0);
   calendar.set(Calendar.DAY_OF_MONTH, 25);
   calendar.set(Calendar.HOUR_OF_DAY, 19);
   calendar.set(Calendar.MINUTE, 42);
   calendar.set(Calendar.SECOND, 12);

   System.out.println(calendar.getTime());
}
Висновок:

Wed Jan 25 19:42:12 MSK 2017
Ми викликаємо метод set(), передаємо до нього константу (залежно від того поля, яке хочемо змінити) та нове значення для цього поля. Виходить, що метод set()- такий собі "супер-сеттер", який вміє встановлювати значення не для одного поля, а для безлічі полів:) Додавання та віднімання значень у класі Calendarздійснюється за допомогою методу add(). У нього необхідно передати те поле, яке ти хочеш змінити, і число - скільки саме ти хочеш додати/зменшити від поточного значення. Наприклад, повернемо дату, яку ми створабо, на 2 місяці тому:
public static void main(String[] args) {
   Calendar calendar = new GregorianCalendar(2017, Calendar.JANUARY , 25);
   calendar.set(Calendar.HOUR, 19);
   calendar.set(Calendar.MINUTE, 42);
   calendar.set(Calendar.SECOND, 12);

   calendar.add(Calendar.MONTH, -2);//щоб відібрати значення - метод потрібно передати негативне число
   System.out.println(calendar.getTime());
}
Висновок:

Fri Nov 25 19:42:12 MSK 2016
Чудово! Ми повернули дату на 2 місяці тому. В результаті змінився не лише місяць, а й рік, з 2017 на 2016 рік. Підрахунок поточного року при перенесенні дат, звичайно, виконується автоматично і його не треба контролювати вручну. Але якщо для якихось цілей тобі потрібно відключити цю поведінку, то можна й так. Спеціальний метод roll()може додавати й зменшувати значення, не торкаючись у своїй інші значення. Наприклад, ось так:
public static void main(String[] args) {
   Calendar calendar = new GregorianCalendar(2017, Calendar.JANUARY , 25);
   calendar.set(Calendar.HOUR, 10);
   calendar.set(Calendar.MINUTE, 42);
   calendar.set(Calendar.SECOND, 12);

   calendar.roll(Calendar.MONTH, -2);
   System.out.println(calendar.getTime());
}
Ми зробабо те саме, що й у попередньому прикладі — відібрали 2 місяці від поточної дати. Але тепер код спрацював по-іншому: місяць змінився з січня на листопад, але рік як був 2017-м, так і залишився! Висновок:

Sat Nov 25 10:42:12 MSK 2017
Далі. Як ми й казали вище, всі поля об'єкта Calendarможна отримати окремо. За це відповідає метод get():
public static void main(String[] args) {
   GregorianCalendar calendar = new GregorianCalendar(2017, Calendar.JANUARY , 25);
   calendar.set(Calendar.HOUR, 10);
   calendar.set(Calendar.MINUTE, 42);
   calendar.set(Calendar.SECOND, 12);

   System.out.println("Рік:" + calendar.get(Calendar.YEAR));
   System.out.println("Місяць:" + calendar.get(Calendar.MONTH));
   System.out.println("Порядковий номер тижня на місяці:" + calendar.get(Calendar.WEEK_OF_MONTH));//порядковий номер тижня на місяці

   System.out.println("Кількість: " + calendar.get(Calendar.DAY_OF_MONTH));

   System.out.println("Годинник:" + calendar.get(Calendar.HOUR));
   System.out.println("Хвабони:" + calendar.get(Calendar.MINUTE));
   System.out.println("Секунди:" + calendar.get(Calendar.SECOND));
   System.out.println(Мілісекунди: + calendar.get(Calendar.MILLISECOND));

}
Висновок:

Год: 2017 
Месяц: 0 
Порядковый номер недели в месяце: 4 
Число: 25 
Часы: 10 
Минуты: 42 
Секунды: 12 
Миллисекунды: 0
Тобто крім "супер-сеттера" в класі Calendarє ще й "супер-геттер" Ще один цікавий момент - це, звичайно, робота з ерами. Для створення дати "до нашої ери" потрібно використовувати поле Calendar.Era Наприклад, створимо дату, що позначає битву при Каннах, в якій Ганнібал переміг військо Риму. Це сталося 2 серпня 216 р. до зв. е.:
public static void main(String[] args) {
   GregorianCalendar cannes = new GregorianCalendar(216, Calendar.AUGUST, 2);
   cannes.set(Calendar.ERA, GregorianCalendar.BC);

   DateFormat df = new SimpleDateFormat("dd MMM yyy GG");
   System.out.println(df.format(cannes.getTime()));
}
Тут ми використовували клас SimpleDateFormat, щоб вивести дату у більш зрозумілому нам форматі (літери “GG” відповідають саме за виведення ери). Висновок:

02 авг 216 до н.э.
У класі Calendarє ще багато методів та констант, почитай про них у документації:

Переклад рядка у Date

Для перекладу String в Date можна скористатися допоміжним класом Java - SimpleDateFormat . Це клас, який потрібен для приведення дати в визначальний формат. Як не загубитися в часі - DateTime та Calendar - 5У свою чергу, він дуже схожий на DateFormat . Єдина помітна різниця між ними полягає в тому, що SimpleDateFormat можна використовувати для форматування (перетворення дати в рядок) та для парсингу рядка в дату з підтримкою мовного стандарту, тоді як DateFormat не підтримує мовний стандарт. Крім того, DateFormat – це абстрактний клас, який забезпечує базову підтримку для форматування та аналізу дат, а SimpleDateFormat – це конкретний клас, який розширює клас DateFormat. Ось так виглядає приклад створення об'єкта SimpleDateFormat та форматування Date:
SimpleDateFormat formater = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = new Date(1212121212121L);

System.out.println(formatter.format(date));
У наведеному вище прикладі ми використовували шаблон "yyyy-MM-dd HH:mm:ss", який означає:
  • 4 цифри на рік (yyyy);
  • 2 цифри на місяць (ММ);
  • 2 цифри на день (dd);
  • 2 цифри для годинника у 24-годинному форматі (HH);
  • 2 цифри на хвабони (mm);
  • 2 цифри за секунди (ss).
Знаки поділу та порядок розміщення символів шаблону зберігаються. Виведення в консоль:
2008-05-30 08:20:12
Шаблонних букв для класу SimpleDateFormat досить багато. Щоб ти не заплутався, ми зібрали їх у таблицю:
Символ Опис приклад
G ера (в англійській локалізації - AD і BC) н.е.
y рік (4-х значне число) 2020
yy рік (останні 2 цифри) 20
yyyy рік (4-х значне число) 2020
M номер місяця (без лідируючих нулів) 8
MM номер місяця (з лідируючими нулями, якщо порядковий номер місяця < 10) 04
MMM трилітерне скорочення місяця (відповідно до локалізації) січ
MMMM повна назва місяця Червень
w тиждень у році (без лідируючих нулів) 4
ww тиждень у році (з лідируючими нулями) 04
W тиждень у місяці (без лідируючих нулів) 3
WW тиждень у місяці (з лідируючим нулем) 03
D день у році 67
d день місяця (без лідируючих нулів) 9
dd день місяця (з лідируючими нулями) 09
F день тижня на місяці (без лідируючих нулів) 9
FF день тижня на місяці (з лідируючими нулями) 09
E день тижня (скорочення) Вт
EEEE день тижня (повністю) п'ятниця
u номер дня тижня (без лідируючих нулів) 5
uu номер дня тижня (з лідируючими нулями) 05
a маркер AM/PM AM
H годинник у 24-годинному форматі без лідируючих нулів 6
HH годинник у 24-годинному форматі з лідируючим нулем 06
k кількість годин у 24-годинному форматі 18
K кількість годин у 12-годинному форматі 6
h час у 12-годинному форматі без лідируючих нулів 6
hh час у 12-годинному форматі з лідируючим нулем 06
m хвабони без лідируючих нулів 32
mm хвабони з лідируючим нулем 32
s секунди без лідируючих нулів 11
ss секунди з лідируючим нулем 11
S мілісекунди 297
z часовий пояс EET
Z часовий пояс у форматі RFC 822 300
Приклади комбінацій символів шаблонів:
Шаблон приклад
dd-MM-yyyy 01-11-2020
yyyy-MM-dd 2019-10-01
HH:mm:ss.SSS 23:59.59.999
yyyy-MM-dd HH:mm:ss 2018-11-30 03:09:02
yyyy-MM-dd HH:mm:ss.SSS 2016-03-01 01:20:47.999
yyyy-MM-dd HH:mm:ss.SSS Z 2013-13-13 23:59:59.999 +0100
Якщо трохи помаботися з форматом, то можна стати власником java.text.ParseException, а це не особливо приємне досягнення. Ну що ж, невеликий екскурс SimpleDateFormat закінчено - повернемося до перекладу java string to date . SimpleDateFormat дає нам такі можливості і ми розглянемо цей процес поетапно.
  1. Створюємо рядок, з якого потрібно задати дату:

    String strDate = "Sat, April 4, 2020";
  2. Створюємо новий об'єкт SimpleDateFormat із шаблоном, який збігається з тим, що у нас у рядку (інакше розпарсувати не вийде):

    SimpleDateFormat formatter = new SimpleDateFormat("EEE, MMMM d, yyyy", Locale.ENGLISH);

    Як ви бачите, у нас тут виник аргумент Locale. Якщо ми його опустимо, він буде використовувати значення Locale за умовчанням, яке завжди є англійською.

    Якщо мовний стандарт не збігається з вхідним рядком, то рядкові дані, прив'язані до мови, як у нас Mon або April , не будуть розпізнані та викликатимуть падіння - java.text.ParseException, навіть якщо шаблон підходить.

    Проте, можна не вказувати формат, якщо ми використовуємо шаблон, який не прив'язаний до мови. Як приклад - yyyy-MM-dd HH:mm:ss

  3. Створюємо дату за допомогою формату, який у свою чергу парсить її з вхідного рядка:

    try {
      Date date = formatter.parse(strDate);
      System.out.println(date);
    }
    catch (ParseException e) {
      e.printStackTrace();
    }

    Виведення в консоль:

    
    Sat Apr 04 00:00:00 EEST 2020

    Хммм….Але формат уже не той!

    Щоб зробити той самий формат, знову використовуємо форматтер:

    System.out.println(formatter.format(date));

    Виведення в консоль:

    
    Sat, April 4, 2020

SimpleDateFormat та Calendar

SimpleDateFormat дозволить тобі форматувати всі створювані об'єкти Date та Calendar для подальшого використання. Розглянемо такий цікавий момент, як робота з ерами. Для створення дати "до нашої ери" потрібно використовувати поле Calendar.Era Наприклад, створимо дату, що означає битву при Каннах, в якій Ганнібал переміг військо Риму. Це сталося 2 серпня 216 р. до зв. е.:
public static void main(String[] args) {
   GregorianCalendar cannes = new GregorianCalendar(216, Calendar.AUGUST, 2);
   cannes.set(Calendar.ERA, GregorianCalendar.BC);

   DateFormat df = new SimpleDateFormat("dd MMM yyy GG");
   System.out.println(df.format(cannes.getTime()));
}
Тут ми використовували клас SimpleDateFormat, щоб вивести дату у більш зрозумілому нам форматі (як зазначено вище, літери “GG” відповідають саме за виведення ери). Висновок:

02 авг 216 до н.э.

Java Date Format

А ось ще один випадок. Припустимо, що цей формат дати нас не влаштовує:

Sat Nov 25 10:42:12 MSK 2017
Так ось. За допомогою наших можливостей у java date format його можна поміняти його на свій власний, без особливих складнощів:
public static void main(String[] args) {

   SimpleDateFormat dateFormat = new SimpleDateFormat("EEEE, d MMMM yyyy");
   Calendar calendar = new GregorianCalendar(2017, Calendar.JANUARY , 25);
   calendar.set(Calendar.HOUR, 10);
   calendar.set(Calendar.MINUTE, 42);
   calendar.set(Calendar.SECOND, 12);

   calendar.roll(Calendar.MONTH, -2);
   System.out.println(dateFormat.format(calendar.getTime()));
}
Висновок:

суббота, 25 Ноябрь 2017
Набагато краще, так? :)
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ