Лабораторна робота 2

Робота датами та текстом. Локалізація

1 Завдання на лабораторну роботу

1.1 Індивідуальне завдання

Спроєктувати та реалізувати класи для представлення сутностей третьої лабораторної роботи курсу "Основи програмування Java". Рішення повинне базуватися на раніше створеній ієрархії класів.

Слід створити похідний клас, який представляє основну сутність. Як базовий використати клас, створений у попередній лабораторній роботі. Клас повинен бути доповненим можливостями підтримки різних локалізацій, зокрема, української та американської. Необхідно передбачити переклад тексту, виведення чисел, а також дат і часу з урахуванням різних локалізацій. Додати (модифікувати) пошук слів у коментарях (або іншому тексті) за допомогою регулярних виразів. Здійснити сортування сутностей за алфавітом з використанням класу Collator.

Створити похідний клас від класу, який представляє другу сутність, в якому додати поле – час і дата, коли відбувається певна подія. Для представлення часу й дати використовувати класи пакету java.time (з урахуванням поясного часу). Підрахувати проміжки часу між подіями та знайти й вивести найменший з проміжків. Якщо клас, який представляв другу сутність індивідуального завдання, не містив поля, типу String, слід додати поле – коментар до події. Для нового (або такого, що існує) текстового поля передбачити можливість виведення українською або англійською мовою, залежно від локалізації.

Програма повинна демонструвати:

  • відтворення реалізації завдань лабораторних робіт № 3 і № 4 курсу "Основи програмування Java";
  • форматування числових даних різними варіантами, а також з урахуванням локалізації;
  • виведення даних про дати й час подій з урахуванням локалізації;
  • виведення тексту українською та англійською мовою;
  • можливості сортування за алфавітом з використанням класу Collator;
  • підрахування та виведення проміжків часу між подіями, пов'язаними з другою сутністю завдання; знаходження й виведення найменшого з проміжків;
  • варіанти складного пошуку в тексті (із застосуванням регулярних виразів), зокрема, пошук фрагмента тексту на початку (наприкінці) слова.

1.2 Уведення дати

Реалізувати програму, в якій користувач вводить рядок. Програма перевіряє, чи відповідає рядок представленню дати, прийнятому в Україні. Перевірка здійснюється за допомогою регулярних виразів. Якщо рядок не відповідає вимогам, виводиться повідомлення про помилку. В іншому випадку створюються і виводяться на консоль об'єкти Date, GregorianCalendar і LocalDate.

1.3 Перевірка номера телефону

Розробити програму перевірки правильності того, що рядок є номером телефону оператора Київстар. Слід скористатися регулярними виразами.

1.4 Перевірка рядка пароля

Розробити програму перевірки відповідності пароля вимогам:

  • пароль може містити літери латинського алфавіту, цифри та спеціальні символи: _ - *;
  • має бути мінімум одна маленька літера;
  • має бути мінімум одна велика літера;
  • має бути мінімум одна цифра;
  • має бути мінімум один спеціальний символ.

Слід скористатися регулярними виразами.

1.5 Отримання масиву підрядків (додаткове завдання)

Рядок довжиною понад 20 символів містить літери та цифри. Отримати з цього рядка масив підрядків, які містять літери між цифрами (групами цифр), визначити цифри як розділювачі.

2 Методичні вказівки

2.1 Робота з датами й часом

2.1.1 Загальні концепції

Робота з датами та часом має велике значення в інформаційних системах. Дата і час є зазвичай важливою інформацією, яка зберігається в базах даних, а також у сховищах даних.

Практично всі програмні платформи надають засоби для роботи з датами та часом. Зокрема, в стандарті Java робота з датами та часом реалізована на трьох рівнях:

  • використання класу Date;
  • використання класу Calendar та його нащадків;
  • використання пакету java.time.

2.1.2 Використання класу Date

Класи Calendar і Date з пакету java.util надають методи для роботи з датою та часом. Об'єкт класу Date зберігає кількість мілісекунд, які минули з 1 січня 1970 р. 00:00:00 за Гринвічем. Це "день народження" Unix, його називають "Epoch".

Клас Date реалізує два конструктори. Конструктор Date() заносить в об'єкт поточну дату і час у форматі поточних регіональних налаштувань:

Date date1 = new Date();

Конструктор Date(long millisec) створює об'єкт використовуючи вказану кількість мілісекунд. Наприклад, отримати кількість мілісекунд, які пройшли з моменту Epoch, можна за допомогою статичного методу System.currentTimeMillis():

Date date2 = new Date(System.currentTimeMillis());

Методи класу Date:

  • long getTime() – повертає значення, що зберігається в об'єкті;
  • void setTime(long newTime) – встановлює нове значення;
  • boolean after(Date when) – повертає true, якщо час when менше значення, що зберігається в об'єкті;
  • boolean before(Date when) – повертає true, якщо час when більше значення, що зберігається в об'єкті.

Багато конструкторів і методів класу Date вважаються застарілими, оскільки клас не забезпечує можливості інтернаціоналізації застосунків.

2.1.3 Використання класу Calendar

Для визначення дат слід використовувати клас Calendar.

Перетворення мілісекунд, що зберігаються в об'єктах класу Date, у поточний час і дату, здійснюється методами класу Calendar. Клас Calendar – це абстрактний клас, який надає методи перетворення певного моменту часу на набір полів календаря (рік, місяць, день місяця, година тощо), а також для роботи з полями календаря.

Клас Calendar визначає цілі константи MONDAYSUNDAY (дні тижня), а також методи, які дозволяють прочитати або встановити перший день тижня, час, часовий пояс тощо.

Під час роботи з класом Calendar для визначення місяців використовують константи в діапазоні 0..11. Щоб запобігти плутанині, доцільно використовувати константи визначені в класі Calendar:

public static final int JANUARY = 0;
public static final int FEBRUARY = 1;
public static final int MARCH = 2;
public static final int APRIL = 3;
public static final int MAY = 4;
public static final int JUNE = 5;
public static final int JULY = 6;
public static final int AUGUST = 7;
public static final int SEPTEMBER = 8;
public static final int OCTOBER = 9;
public static final int NOVEMBER = 10;
public static final int DECEMBER = 11

Java надає лише одну реалізацію класу Calendar – клас GregorianCalendar, похідний від Calendar. Можна створити об'єкт класу GregorianCalendar за допомогою статичного методу getInstance():

Calendar gregorianCalendar = Calendar.getInstance(); // або Calendar gregorianCalendar = GregorianCalendar.getInstance();

Клас GregorianCalendar надає конструктори, які створюють об'єкт-календар через визначення дати й часу цілими значеннями:

GregorianCalendar()
GregorianCalendar(int year, int month, int date) 
GregorianCalendar(int year, int month, int date, int hour, int minute) 
GregorianCalendar(int year, int month, int date, int hour, int minute, int second)

Можна навести приклад роботи з класами Date і Calendar:

package ua.inf.iwanoff.java.advanced.second;

import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Date;

public class DateTest {
    public static void main(String[] args) {
        // Установка часу з використанням екземпляра Date:
        Date date = new Date(System.currentTimeMillis());
        Calendar calendar = GregorianCalendar.getInstance();
        calendar.setTime(date);
        System.out.println(calendar.getTime());

        // Виклик методів getter і setter об'єкта Calendar:
        calendar.set(Calendar.MONTH, Calendar.AUGUST);
        calendar.set(Calendar.DAY_OF_MONTH, 24);
        calendar.set(Calendar.YEAR, 1991);
        calendar.set(Calendar.HOUR, 21);
        calendar.set(Calendar.MINUTE, 00);
        calendar.set(Calendar.SECOND, 01);
        System.out.println(calendar.getTime());
        System.out.println("The YEAR is: " + calendar.get(Calendar.YEAR));
        System.out.println("The MONTH is: " + calendar.get(Calendar.MONTH));
        System.out.println("The DAY is: " + calendar.get(Calendar.DATE));
        System.out.println("The HOUR is: " + calendar.get(Calendar.HOUR));
        System.out.println("The MINUTE is: " + calendar.get(Calendar.MINUTE));
        System.out.println("The SECOND is: " + calendar.get(Calendar.SECOND));
        System.out.println("The AM_PM indicator is: " + calendar.get(Calendar.AM_PM));
    }
}

2.1.4 Використання засобів java.time

Стандартні засоби Java (до JDK 7 включно) для роботи з датами та часом мають низку недоліків, пов'язаних з незручністю їх використання, що призвело до появи альтернативних (нестандартних) бібліотек, таких як популярна серед програмістів бібліотека Joda-Time. Для того, щоб виправити цю ситуацію, в Java 8 додані нові класи та засоби підтримки роботи з датами та календарем. Нова бібліотека дати та часу значною мірою схожа на Joda-Time.

У пакеті java.time визначені класи для подання дати та часу:

  • LocalDate (день, місяць, рік);
  • LocalTime (тільки час доби);
  • LocalDateTime (дата і час).

Ці три класи використовують у випадках, коли не враховується поясний час. Можна створити об'єкти за допомогою статичних функцій now(), які повертають поточне значення часу (дати):

LocalDate date = LocalDate.now();
LocalTime time = LocalTime.now();
LocalDateTime dateTime = LocalDateTime.now();

Інші способи створення об'єкта – це використання статичної функції of(), яка приймає значення годин, хвилин і секунд як параметри:

time = LocalTime.of(11, 15, 0);
date = LocalDate.of(2024, 3, 8);
dateTime = LocalDateTime.of(2024, Month.MARCH, 8, 11, 15, 0);
dateTime = LocalDateTime.of(date, time); // інший варіант

Примітка: на відміну від класу Calendar, класи пакету java.time використовують номери місяців у діапазоні 1..12.

Як видно з прикладу, для визначення місяців можна використовувати як цілі значення, так і константи переліку java.time.Month. З об'єкта типу LocalDate, що існує, можна створити об'єкт типу LocalDateTime за допомогою методу atTime():

dateTime = date.atTime(time);

Використовуючи методи plus() та minus(), можна змінити значення дати та часу об'єктів, що існують. Можна встановити одиницю вимірювання за допомогою переліку java.time.temporal.ChronoUnit. Наведений нижче приклад визначає час на дві години пізніше, ніж поточний:

LocalTime now = LocalTime.now();
LocalTime later = now.plus(2, ChronoUnit.HOURS);
System.out.println(later);

Існують також методи додавання та віднімання plusHours(), minusHours(), plusMinutes(), minusMinutes(), plusSeconds(), minusSeconds(), plusNanos() та minusNanos() з цілими параметрами. Аналогічно, клас LocalDate надає методи plusDays(), plusMonths(), minusDays() і minusMonths(). Наприклад:

LocalDate today = LocalDate.now();
LocalDate thirtyDaysFromNow = today.plusDays(30);
LocalDate nextMonth = today.plusMonths(1);
LocalDate aMonthAgo = today.minusMonths(1);

Ідентифікація часових поясів здійснюється засобами класу ZoneId. Найпростіший шлях отримати ідентифікатор часового поясу, встановленого в системі, – це використання статичної функції systemDefault():

ZoneId myZone = ZoneId.systemDefault();

Можна отримати ідентифікатор часового поясу за допомогою константи типу рядка. Множину можливих констант можна отримати за допомогою статичного методу ZoneId.getAvailableZoneIds(). Наведений нижче код дозволяє показати константи для європейських поясів:

import java.time.ZoneId;

public class AvailableZoneIds {
    public static void main(String[] args) {
        for (String zone: ZoneId.getAvailableZoneIds()) {
            if (zone.startsWith("Europe")) {
                System.out.println(zone);
            }
        }
    }
}

Перелічені константи можуть бути використані в методі of(), наприклад:

ZoneId germanZone = ZoneId.of("Europe/Berlin");

Для роботи з часом з урахуванням часового поясу використовують клас ZonedDateTime. Його використання аналогічне LocalTime, однак якщо визначити об'єкт типу ZoneId як параметр методу now(), можна працювати з даними про час відповідного часового поясу:

ZonedDateTime germanTime = ZonedDateTime.now(germanZone); 
System.out.println(germanTime);

Для роботи з поясним часом також можна використовувати клас Clock.

Clock clock = Clock.system(ZoneId.of("Europe/Kiev"));
LocalTime kyivTime = LocalTime.now(clock);
System.out.println(kyivTime);

Java 8 визначає два класи, Period та Duration, що дозволяють визначати інтервали між датами та часом відповідно. Для цього використовуються методи between():

Period p = Period.between(date1, date2);
Duration d = Duration.between(time1, time2);

Об'єкти цих класів також можуть бути створені за допомогою статичних методів, наприклад:

Duration twoHours = Duration.ofHours(2);
Duration tenMinutes = Duration.ofMinutes(10);
Duration thirtySecs = Duration.ofSeconds(30);

Клас java.time.temporal.TemporalAdjusters містить корисні методи, такі як firstDayOfMonth(), firstDayOfNextMonth(), firstInMonth(DayOfWeek), lastDayOfMont(), next(DayOfWeek), nextOrSame(DayOfWeek), previous(DayOfWeek), previousOrSame(DayOfWeek)тощо.

Клас Instant представляє момент часу, виміряний в наносекундах. Використовуючи подання в наносекундах, нова бібліотека забезпечує сумісність з попередніми засобами. Класи Date і Calendar попередніх версій Java надають метод toInstant(), результат якого може бути використаний для створення об'єктів LocalDateTime або ZonedDateTime. Наведений нижче приклад демонструє можливості перетворення різних форм подання дати й часу:

package ua.inf.iwanoff.java.advanced.second;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

public class DateConversion {
    static Date fromLocalDateTimeDate(LocalDateTime localDateTime) {
        return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
    }
    static LocalDateTime fromDateToLocalDateTime(Date date) {
        return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
    }

    public static void main(String[] args) {
        LocalDateTime dateTime = LocalDateTime.now();
        System.out.println(dateTime);
        Date date = fromLocalDateTimeDate(dateTime);
        System.out.println(date);
        Calendar calendar = GregorianCalendar.getInstance();
        calendar.setTime(date);
        calendar.add(Calendar.YEAR, 1);
        date.setTime(calendar.getTimeInMillis());
        System.out.println(date);
        dateTime = fromDateToLocalDateTime(date);
        System.out.println(dateTime);
    }
}

2.2 Робота з текстом у Java

2.2.1 Загальні концепції роботи з рядками

У курсі "Основи програмування Java" було розглянуто основні концепції роботи з класом String. Зокрема, були визначені такі основні особливості роботи з рядками:

  • рядок містить у собі послідовність Unicode-символів;
  • рядки фактично фігурують в усіх програмах мовою Java;
  • можна створити рядок як за допомогою конструктора, так і шляхом безпосереднього присвоєння рядкового літерала (послідовності символів у лапках) посиланню типу String;
  • можна отримати подання кожного об'єкта (або змінної примітивного типу) шляхом додавання рядка за допомогою оператора + ліворуч або праворуч;
  • масиви рядків можна усталено сортувати за алфавітом;
  • рядок типу String не можна змінити після його створення.

Для ефективної заміни окремих символів рядків та додавання нових символів слід використовувати спеціальні класи StringBuffer і StringBuilder.

Далі ми розглянемо додаткові можливості роботи з текстовими даними – текстові блоки, локалізація, сортування, форматування, ітерація та регулярні вирази.

2.2.2 Робота з текстовими блоками

Робота з рядками викликає певні незручності, коли один об'єкт типу String містить багато переходів на новий рядок, а також інших символів, для яких треба використовувати спеціальні керувальні послідовності. Для більшої зручності під час роботи з такими рядками у версії Java 15 додано спеціальну синтаксичну конструкцію – текстовий блок (Text Block).

Синтаксис текстового блоку такий: блок починається з трьох символів """, далі обов'язково слід перейти на новий рядок, далі в одному або кількох рядках розташувати необхідний текст і закрити блок за допомогою """. Компілятор автоматично створює з такого блоку об'єкт типу String:

String block = """
        Hello, block!""";

У попередньому прикладі простіше було б скористатися звичайним літералом. Реальна необхідність виникає, коли текст повинен займати декілька рядків, розташовувати форматований текст, використовувати всередині лапки тощо. Наприклад:

String block = """
        Hello, block!
        This is the same string.
        We can add use "quotes" without backslash.
            We can indent.""";

Дуже часто в такий спосіб у сирцевий код Java додають, наприклад, фрагменти HTML-тексту, або програмного коду іншими мовами програмування.

2.3 Локалізація

2.3.1 Загальні відомості

Інтернаціоналізація (internationalization, i18n) – це принцип проєктування й реалізації програмного забезпечення так, щоб забезпечити можливість подальшої адаптації до різних мов та регіонів без конструктивних змін. Дотримання вимог інтернаціоналізації максимально спрощує майбутній процес адаптації програмного продукту до регіональних на національних вимог.

Локалізація (localization, l10n) – це процес адаптації програмного забезпечення до національних стандартів та правил, таких як, наприклад:

  • мова;
  • правила сортування за абеткою;
  • формат представлення чисел, дати та часу;
  • часовий пояс;
  • формат валюти;
  • одиниці вимірювання;
  • напрямок написання тексту;
  • формат паперу.

У Java для встановлення та зберігання інформації про локалізацію використовують об'єкти класу java.util.Locale. Існує кілька варіантів створення об'єкта типу Locale. Найпростіший варіант – отримати локалізацію, усталено визначену для віртуальної машини Java:

Locale locale = Locale.getDefault();

Можна визначити локалізацію за допомогою конструкторів, наприклад:

Locale locale1 = new Locale("en", "US");
Locale locale2 = new Locale("en", "GB");
Locale locale3 = new Locale("uk"); // встановлено мову, регіон не встановлено

Для визначення мов та країн, клас Locale використовує ідентифікатори відповідно до стандарту IETF BCP 47 (https://en.wikipedia.org/wiki/IETF_language_tag).

Можна також скористатися статичним методом forLanguageTag(), наприклад:

Locale locale4 = Locale.forLanguageTag("en-US");

Можна "побудувати" об'єкт за допомогою вкладеного класу Locale.Builder:

Locale Locale5 = new Locale.Builder().setLanguage("en").setRegion("US").build();

Платформа Java не вимагає лише однієї локалізації для всієї програми. Така гнучкість дозволяє розробляти багатомовні застосунки. Для деяких країн регіональні параметри встановлюють за допомогою констант, наприклад: Locale.US, Locale.FRANCE, Locale.CANADA. Для інших країн об'єкт Locale потрібно створити за допомогою конструктора, наприклад, new Locale("uk", "UA").

Для локалізації можна отримати інформацію про мову та регіон:

Locale locale = new Locale("uk", "UA");
System.out.println(locale.getCountry()); //код регіону
System.out.println(locale.getDisplayCountry()); //назва регіону
System.out.println(locale.getLanguage()); //код мови регіону
System.out.println(locale.getDisplayLanguage()); //назва мови регіону

Є можливість встановлювати необхідну усталену локалізацію для застосунку, наприклад:

Locale.setDefault(Locale.US);

Клас SimpleTimeZone, що реалізує абстрактний клас TimeZone, дозволяє працювати з часовими поясами в Григоріанському календарі. Цей клас враховує літній час.

Клас GregorianCalendar має конструктори, що дозволяють визначити календар за часовим поясом та локалізації:

GregorianCalendar(Locale locale)
GregorianCalendar(TimeZone timeZone) 
GregorianCalendar(TimeZone timeZone, Locale locale)

2.3.2 Створення застосунків з підтримкою декількох мов

Найбільш важливим використанням інтернаціоналізації та локалізації є створення застосунків, які підтримують виведення текстів (повідомлень) різними мовами без істотного перероблювання коду, бажано, без перекомпіляції. В такому випадку важливо зберігати рядки різними мовами окремо від сирцевого коду. Стандартне рішення для Java-застосунків – використання можливостей класу java.util.ResourceBundle. Цей клас відповідає за "в'язку ресурсів" – логічно зв'язаний набір даних, зокрема, файлів.

Клас ResourceBundle – абстрактний. На стандартному рівні реалізовано два похідних класи: PropertyResourceBundle і ListResourceBundle. Клас PropertyResourceBundle забезпечує роботу з файлом властивостей. Файл властивостей – це звичайний текстовий файл, який містить імена і значення властивостей (рядки тексту). Файли властивостей не є частиною сирцевого коду Java. Клас ListResourceBundle керує ресурсами зі списку; він отримує дані з файлу .class. Це дозволяє зберігати будь-які специфічні для локалізації об'єкти, а не тільки рядки.

Щоб отримати відповідний файл ресурсу, слід викликати метод ResourceBundle.getBundle(). Це фабричний метод, який намагається створити об'єкт ListResourceBundle, і якщо відповідні ресурси відсутні, створює об'єкт PropertyResourceBundle. Якщо в'язку ресурсів не знайдено, генерується виняток MissingResourceException.

Файли властивостей, які відносяться до однієї в'язки, мають спільне базове ім'я і різні "суфікси". Наприклад, "strings_en.properties", "strings_uk.properties" тощо. Можна також використовувати складніші суфікси: "strings_en_US.properties", "strings_uk_UA.properties" тощо.

Формат файлів дуже простий. Це текстові файли, які складаються з таких рядків:

властивість=значення

Наприклад, можна створити такі файли з рядками для англійської та німецької локалізації, відповідно words_en.properties

word1=one
word2=two
word3=three

і words_de.properties:

word1=eins
word2=zwei
word3=drei

Засоби середовища IntelliJ IDEA дозволяють одночасно створити всю в'язку файлів властивостей. У проєкті IntelliJ IDEA в'язку ресурсів слід розташувати у теці out | production | <ім'я проєкту>.

Примітка: у Maven-проєктах файли властивостей розташовують у теці resources проєкту; Maven-проєкти будуть розглянуті пізніше.

Для того, щоб зручніше редагувати файли властивостей у проєктах IntelliJ IDEA, можна (але не обов'язково) додатково завантажити плагін Resource Bundle Editor.

Існує проблема, пов'язана з використанням кирилиці у файлах властивостей. Такі файли повинні використовувати кодову таблицю ISO 8859-1, яка не підтримує символів кирилиці. Тому замість безпосереднього використання кириличних символів необхідно вказувати їхні коди таблиці Unicode. Наприклад, у файлі words_uk.properties замість тексту

word1=один
word2=два
word3=три

слід розташувати текст:

word1=\u043E\u0434\u0438\u043D
word2=\u0434\u0432\u0430
word3=\u0442\u0440\u0438

Такий текст дуже незручно перекодовувати й уводити вручну. Існують спеціальні утиліти для перекодування файлів .properties, які містять кириличні символи, наприклад, native2ascii. Але найпростіший шлях – скористатися вбудованими можливостями IntelliJ IDEA. Спочатку в налаштуваннях Settings... | Editors| File Encodings після рядка Default encoding for properties files вибрати опцію Transparent native-to-ascii conversion. Після цього кириличний текст, який ми вводимо або редагуємо у файлах властивостей, автоматично буде перекодований у необхідне представлення.

У програмному коді, який використовує підготовлені ресурси, спочатку слід створити об'єкт типу ResourceBundle з одночасним завантаженням в'язки за допомогою функції getBundle(), вказавши як параметри базове ім'я в'язки та необхідну локалізацію, а потім отримувати необхідні рядки, викликаючи метод getString() з параметром-властивістю.

Припустимо, у програмі слід виводити рядок "Hello, world!", або "Привіт, світ!", залежно від локалізації. Створюємо в'язку ресурсів з двох файлів. Файл hello_en.properties:

message=Hello, world!

Текст файлу hello_uk.properties, який ми вводимо в середовищі IntelliJ IDEA, буде таким:

message=Привіт, світ!

Насправді вміст файлу hello_uk.properties буде таким:

message=\u041F\u0440\u0438\u0432\u0456\u0442, \u0441\u0432\u0456\u0442!

Код програми, яка послідовно виводить повідомлення з використанням різних локалізацій, матиме такий вигляд:

package ua.inf.iwanoff.java.advanced.second;

import java.util.Locale;
import java.util.ResourceBundle;

public class HelloResourceBundle {
    public static void main(String[] args) {
        ResourceBundle bundle = ResourceBundle.getBundle("hello", new Locale("en"));
        String msg = bundle.getString("message");
        System.out.println(msg);
        bundle = ResourceBundle.getBundle("hello", new Locale("uk"));
        msg = bundle.getString("message");
        System.out.println(msg);
    }
}

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

2.4 Використання пакета java.text

2.4.1 Сортування рядків

Пакет java.text надає класи та інтерфейси для обробки тексту, дат та числових значень незалежно від мови та інших національних особливостей. Це означає, що програма може бути написана для незалежної від мови роботи, а ресурси, які визначають індивідуальну локалізацію, можуть підключатися динамічно. Це дозволяє гнучко додавати нові локалізації програми пізніше.

Класи цього пакету призначені для форматування дат, чисел та повідомлень, аналізу, пошуку та сортування рядків, а також ітеративного проходження символів, слів, пропозицій та рядків. Цей пакет містить три основні групи класів та інтерфейсів, які відповідно розв'язують такі групи задач:

  • сортування рядків;
  • форматування та розбір на лексеми;
  • ітерація за текстом.

Роботу сортування забезпечує клас Collator ("порівнювач"). За допомогою екземпляра класу Collator можна здійснювати сортування рядків із динамічним визначенням локалізації. Наприклад, якщо сортувати тексти українською мовою звичайними засобами, виникає проблема зі специфічними українськими літерами, такими як ґ, є, і та ї, оскільки їх коди розташовані окремо від кодів інших символів українського алфавіту. Для коректного сортування масивів рядків з українським текстом необхідно створити екземпляр класу Collator за допомогою функції getInstance(), параметром якої є необхідна локалізація.

Наведений нижче приклад демонструє сортування масиву рядків спочатку без використання класу Collator, а потім зі створенням об'єкта типу Collator, що враховує українську локалізацію. Для визначення порядку сортування створюємо безіменний внутрішній клас.

import java.text.Collator;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Locale;

public class SortDemo {

    public static void main(String[] args) {
        String[] words = { "воля", "воїн", "возити" };
        // Здійснюємо усталене сортування:
        Arrays.sort(words);
        System.out.println(Arrays.asList(words)); // [возити, воля, воїн]
        // Здійснюємо сортування з урахуванням локалізації:
        Arrays.sort(words, new Comparator<String>() {
            Collator collator = Collator.getInstance(new Locale("uk"));
            
            @Override
            public int compare(String s1, String s2) {
                return collator.compare(s1, s2);
            }
        });
        System.out.println(Arrays.asList(words)); // [возити, воїн, воля]
    }

}

2.4.2 Форматування

Форматування даних з урахуванням локалізації надає абстрактний клас java.text.Format. Похідні класи NumberFormat, DateFormat і MessageFormat дозволяють форматувати числа, дати й повідомлення відповідно. У класі Format оголошено метод format(), який перетворює параметр у рядок у визначеному форматі.

Наведений нижче приклад демонструє виведення значення типу double, враховуючи вказану локалізацію:

import java.text.NumberFormat;
import java.util.Locale;
import java.util.Scanner;

public class NumberPrinter {

    public static void main(String[] args) {
        NumberFormat form = NumberFormat.getInstance(new Locale("uk"));
        Scanner scan = new Scanner(System.in);
        double x = scan.nextDouble();
        System.out.println(form.format(x));
    }

}

Для форматування цілих можна отримати об'єкт за допомогою методу getIntegerInstance(). Аналогічно можна отримати getCurrencyInstance() для виведення грошових сум, getPercentInstance() для виведення відсотків тощо.

Для того, щоб конвертувати інформацію в регіональні стандарти, слід створити об'єкт класу NumberFormat з конструктором, який приймає як параметр об'єкт класу Locale, або з конструктором без параметрів (з усталеною локалізацією):

NumberFormat nf = NumberFormat.getInstance(new Locale("uk"));
NumberFormat nf = NumberFormat.getInstance();

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

import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Locale;

public class DemoNumberFormat {

    public static void main(String[] args) {
        NumberFormat nfGe = NumberFormat.getInstance(Locale.GERMAN);
        NumberFormat nfUs = NumberFormat.getInstance(Locale.US);
        NumberFormat nfFr = NumberFormat.getInstance(Locale.FRANCE);
        NumberFormat nfUa = NumberFormat.getInstance(new Locale("uk", "UA"));
        double iGe = 0, iUs = 0, iFr = 0, iUa = 0;
        String str = "1.245,999";
        try {
            //перетворення рядка на німецький стандарт:
            System.out.println(iGe = nfGe.parse(str).doubleValue());
            //перетворення рядка в американський стандарт:
            System.out.println(iUs = nfUs.parse(str).doubleValue());
            //перетворення рядка у французький стандарт:
            System.out.println(iFr = nfFr.parse(str).doubleValue());
            //перетворення рядка на український стандарт: 
            System.out.println(iUa = nfUa.parse(str).doubleValue());  
        }
        catch (ParseException e) {
            e.printStackTrace();
        }
        System.out.println();
        String sUs = nfUs.format(iGe);//перетворення числа з німецького в американський стандарт    
        String sFr = nfFr.format(iGe);//перетворення числа з німецького у французький стандарт
        String sUa = nfUa.format(iGe);//перетворення числа з німецького на український стандарт
        System.out.println(sUs + "\n" + sFr + "\n" + sUa);
    }

}

Тут для перетворення рядків в число і назад використовуються методи NumberFormat parse(String source) і String format(double number) відповідно.

Задачу підтримки національних особливостей у відображенні дати та часу в різних країнах та регіонах світу допомагає розв'язати клас java.text.DateFormat. Під час створення об'єкта цього класу може бути визначена конкретна локалізація або використана усталена локалізація для даного середовища:

DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM, new Locale("Uk"));
DateFormat df = DateFormat.getDateInstance();    

Абстрактний клас DateFormat і його підклас SimpleDateFormat пакету java.text містять методи, що дозволяють здійснювати різні способи форматування подання дати та часу. Клас DateFormat пропонує такі стилі уявлення дати та часу:

  • стиль SHORT представляє дату та час у короткому числовому вигляді: 27.04.01 17:32;
  • стиль MEDIUM задає рік чотирма цифрами та показує секунди: 27.04.2001 17:32:45;
  • стиль LONG представляє місяць словом і додає часовий пояс: 27 квітень 2001 р. 17:32:45 GMT+03.-00;
  • стиль FULL збігається зі стилем LONG;
  • стиль DEFAULT збігається зі стилем MEDIUM.

Наприклад, наведений нижче код створює форматований рядок дати з усталеним форматом для поточного регіону:

DateFormat dateFormatter = DateFormat.getDateInstance(DateFormat.DEFAULT);
Date today = new Date();
String formattedDate = dateFormatter.format(today);
System.out.println(formattedDate);    

Крім методу DateFormat.getDateInstance() можуть бути використані методи DateFormat.getTimeInstance() і DateFormat.getDateTimeInstance() для форматування часу, а також для форматування і дати, і часу.

Аналогічно можна форматувати дати й час пакету java.time.

Дочірній клас SimpleDateFormat абстрактного класу DateFormat можна використовувати для визначення власних форматів користувача. Під час створення об'єкта класу SimpleDateFormat можна задати в конструкторі шаблон, який визначає будь-який інший формат, наприклад:

SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy hh mm");
System.out.println(sdf.format(new Date()));    

У шаблоні літера d означає цифру дня місяця, M – цифру місяця, у – цифру року, h – цифру години, m – цифру хвилин.

Наприклад, наведений нижче код виведе дату у форматі "04/29/2013":

Date today = new Date();
SimpleDateFormat formatter = new SimpleDateFormat("MM/dd/yyyy");
String formattedDate = formatter.format(today);
System.out.println(formattedDate);

Шаблон "EEEE d MMMM yyyy" задасть формат виведення дати як "четвер 16 травня 2011".

Для налаштування символів для будь-якого компонента дати або часу використовують клас DateFormatSymbols:

DateFormatSymbols symbols = new DateFormatSymbols();
String[] oddMonthAbbreviations = new String[] {"Ja","Fe","Mh","Ap","My","Jn","Jy","Au","Se","Oc","No","De" };
symbols.setShortMonths(oddMonthAbbreviations);
formatter = new SimpleDateFormat("MMM dd, yyyy", symbols);
formattedDate = formatter.format(today);
System.out.println(formattedDate);    

Конструктор SimpleDateFormat приймає рядок шаблону та об'єкт DateFormatSymbols.

Отримати подання поточної дати у всіх можливих регіональних стандартах можна так:

Date d = new Date();
Locale[] locales = DateFormat.getAvailableLocales();
for (Locale loc : locales) {
    DateFormat df = DateFormat.getDateInstance(DateFormat.FULL, loc);
    System.out.println(loc.toString() + "-> " + df.format(d));
}

Приклад виведення дати у форматі, заданому користувачем:

import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.text.*;

public class UserDefinedDateFormat {

    public static void main(String args[]) {
        try {
            String[] months = { "січня", "лютого", "березня", "квітня", "травня", "червня",
                                "липня", "серпня", "вересня", "жовтня", "листопада", "грудня" };
            DateFormatSymbols dfs = new DateFormatSymbols(new Locale("uk"));
            dfs.setMonths(months);
            SimpleDateFormat sdf = new SimpleDateFormat("d MMMM yyyy 'р.'", dfs);
            System.out.println("Сьогодні: " +  sdf.format(new Date()));
            Calendar c = Calendar.getInstance();
            c.set(2024, 5, 1);
            System.out.println("Початок літа: " + sdf.format(c.getTime()));
        }
        catch (Exception e) {
            System.out.println(e);
        }
    }

}

Якщо цю програму завантажити на виконання, в першому рядку буде виведено поточну дату (наприклад "1 березня 2024 р."), а в другому – "Початок літа: 1 червня 2024 р."

Клас java.util.Formatter відповідає за форматування на рівні представлення даних з точки зору ширини поля виведення, вирівнювання, наявності знака + перед числами тощо.

Статичний метод format() класу String дозволяє отримати рядок, який представляє дані відповідно до формату, наприклад:

    double d = 3.5;
    int i = 12;
    String result = String.format("%f %d%n", d, i);
    System.out.print(result);

Аналогічний метод оголошено у класах PrintStream та PrintWriter. Крім того, у цих класах оголошено метод printf() з параметрами, ідентичними параметрам методу format(), який здійснює форматований висновок потоку. Таким чином можна отримати безпосереднє форматування виведення даних за допомогою функції System.out.printf():

    double d = 3.5;
    int i = 12;
    System.out.printf("%f %d%n", d, i);    

Під час форматування використовують такі специфікатори формату:

Специфікатор формату Форматування, що виконується
%a

Шістнадцяткове значення з рухомою крапкою

%b

Логічне (булеве) значення аргументу

%c

Символьне подання аргументу

%d

Десяткове ціле значення аргументу

%h

Хеш-код аргументу

%e

Експоненціальне подання аргументу

%f

Десяткове значення з рухомою крапкою

%g

Коротший варіант з двох: %e або %f

%o

Вісімкове ціле значення аргументу

%n

Вставка символу нового рядка

%t

Час та дата

%x

Шістнадцяткове ціле значення аргументу

%%

Вставка символу %

Також можливі специфікатори з великими літерами: %A (еквівалентно %a). Форматування з допомогою великих літер забезпечує переведення символів у верхній регістр.

import java.util.Formatter;

public class SimpleFormatString {

    public static void main(String[] args) {
        Formatter f = new Formatter(); 
        f.format("%s %c %nОснови розробки застосунків Java %S ", "Модуль",'2',"se");  
        System.out.print(f); 
    }

}    

Для даних з рухомою крапкою (специфікатори %f, %e, %g), а також для рядків (специфікатор %s) може бути застосований специфікатор точності. Він задає кількість символів, що виводяться. Наприклад, специфікатор %10.3f виводить число із шириною поля 10 символів і з трьома десятковими знаками (усталена точність дорівнює шести десятковим знакам). Застосований до рядків специфікатор точності визначає максимальну довжину поля виведення. Наприклад, %.15s виводить рядок довжиною 15 символів, специфікатор %3.7s – рядок довжиною щонайменше трьох і більше ніж сім символів. Якщо рядок є довшим, кінцеві символи відкидаються. Можливе завдання прапорів, які дозволяють здійснити додаткові можливості форматування:

import java.util.Formatter;

public class SimpleFormatString {

    public static void main(String[] args) {
        Formatter f = new Formatter(); 
        f.format("|%10.2f|", 123.123);  // вирівнювання праворуч 
        System.out.println(f);
        f = new Formatter();            // вирівнювання ліворуч
        f.format("|%-10.2f|", 123.123); // застосування прапора '-' 
        System.out.println(f);
        f = new Formatter();
        f.format("%,.2f", 123456789.34);// застосування прапора ',' 
        System.out.println(f);
        f = new Formatter();
        f.format("%.4f", 1111.1111111); // Завдання точності представлення для чисел 
        System.out.println(f);
        f = new Formatter();
        f.format("%.6s", "Робота з текстовими даними."); // Завдання точності представлення для рядків
        System.out.println(f);
    }

}

Існують також специфікатори для форматування дати та часу, які можуть використовуватися тільки для типів long, Long, Calendar, Date, наприклад:

import java.util.*;

public class Time {

  public static void main(String args[]) {
    Formatter f = new Formatter();
    Calendar calendar = Calendar.getInstance(); 
    f.format("%tr", calendar);// виведення у 12-годинному часовому форматі 
    System.out.println(f); 
    f = new Formatter();
    f.format("%tc", calendar);// повноформатне виведення часу та дати 
    System.out.println(f); 
    f = new Formatter();
    f.format("%tl:%tM", calendar, calendar);// виведення поточної години та хвилини 
    System.out.println(f); 
    f = new Formatter();
    f.format("%tB %tb %tm", calendar, calendar, calendar);// різні варіанти виведення місяця 
    System.out.println(f);
  }

}

2.4.3 Ітерація за символами рядка

Інтерфейс java.text.CharacterIterator надає засоби для двоспрямованого проходження рядка за допомогою ітератора. Клас StringCharacterIterator реалізує інтерфейс java.text.CharacterIterator. Методи getBeginIndex() і getEndIndex() дозволяють повернути індекси першого й останнього символів рядка. Індекс поточного символу можна отримати за допомогою методу getIndex(). Виклик методу setIndex(int index) переміщує ітератор у нове положення. За допомогою методів previous() і next() можна переміщати ітератор на попередню і наступну позиції, при досягненні меж діапазону ці методи повернуть DONE. Методи first() і last() встановлюють ітератор на першу та останню позиції відповідно, повернувши символ, що знаходиться на цій позиції. У наведеному нижче прикладі використовуємо ітератор CharacterIterator класу StringCharacterIterator для проходження рядка від початкового індексу до кінця рядка.

import java.text.CharacterIterator;
import java.text.StringCharacterIterator;

public class StringCharacterExample {
    private static final String text = "Jackdaws love my big sphinx of quartz";

    public static void main(String[] args) {
        CharacterIterator it = new StringCharacterIterator(text);
        for (char ch = it.first(); ch != CharacterIterator.DONE; ch = it.next()) {
            System.out.print(ch);
        }
    }

}

Оскільки клас Character враховує локалізацію його доцільно використовувати замість char у застосунках, розрахованих на інтернаціоналізацію та локалізацію.

2.5 Регулярні вирази

Регулярні вирази – це спосіб описати багато рядків на основі загальних характеристик. Регулярні вирази можна використовувати для пошуку, редагування або роботи з текстом та даними. Синтаксис регулярних виразів однаковий у різних мовах програмування.

У Java робота з регулярними виразами реалізована в пакеті java.util.regex, зокрема, класами Pattern і Matcher. Також визначено спеціальний клас-виняток PatternSyntaxException.

Об'єкт Pattern – це скомпільоване подання регулярних виразів. Цей клас не надає відкритих конструкторів. Щоб створити об'єкт типу Pattern, спочатку необхідно викликати один зі статичних методів compile(), кожен з яких створює об'єкт Pattern і повертає посилання на нього. Ці методи приймають регулярний вираз як перший аргумент. Наприклад:

Pattern pattern = Pattern.compile(ex); // ex – рядок, який визначає регулярний вираз

Після створення об'єкта Pattern його використовують для ініціалізації об'єкта Matcher:

Matcher matcher = pattern.matcher(s); // s – рядок який потрібно перевірити

Далі можна використовувати функції класу Matcher для порівняння рядків (matches()), пошуку підрядка (find()) тощо.

У найпростішому випадку регулярний вираз – це рядок, що містить послідовність символів. Це означає, що під час порівняння рядків перевіряється їх еквівалентність, а під час пошуку здійснюється знаходження повного тексту регулярного виразу. Результатом наведеної нижче програми є виведення у консольне вікно значення true:

package ua.inf.iwanoff.java.advanced.second;

import java.util.regex.*;

public class FirstRegex {

    public static void main(String[] args) {
        Pattern pattern = Pattern.compile("text");
        Matcher matcher = pattern.matcher("text");
        System.out.println(matcher.matches());
    }

}

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

Замість порівняння можна шукати перше входження шаблону в рядок. Наприклад:

    Pattern pattern = Pattern.compile("text");
    Matcher matcher = pattern.matcher("textual");
    System.out.println(matcher.find());    

Тепер результат буде true. Результат також буде true, якщо ми перевіримо такі рядки як "the text", "context" тощо.

Для того, щоб послідовно знайти всі входження підрядки, можна скористатися таким циклом:

    while (matcher.find()) {
        System.out.printf("Знайдено текст" + " \"%s\", починаючи з позиції"
                  + "%d і закінчуючи позицією %d.%n", matcher.group(),
                  matcher.start(), matcher.end());
    }

У наведеному прикладі функція group() повертає послідовність символів, які задовольняють умовам пошуку, функції start() і end() повертають позиції початку та кінця знайденої послідовності у рядку.

До регулярних виразів можна додавати керувальні послідовності, які починаються зі зворотної косою риси (\ – зворотний слеш, backslash). Наприклад, можна визначати символи за їх кодом:

Представлення Пояснення Кодування
\0n n – вісімкове число від 0 до 377 8-бітове
\xdd d – шістнадцяткова цифра
\udddd 16-бітове (Unicode)

Можна також використовувати керувальні послідовності:

Представлення Символ
\t Табуляція
\v Вертикальна табуляція
\r Повернення курсора
\n Новий рядок
\f Кінець сторінки
\a Дзвінок
\e Escape-символ
\b Забій (backspace)
Примітка: Забій повинен знаходитися всередині квадратних дужок (інакше інтерпретується як границя слова).
\cA\cZ Ctrl+A ... Ctrl+Z
Еквівалентно \x01 ... \x1A (\cA = \001, \cZ = \032)

Для створення складних регулярних виразів використовують звані метасимволи – спеціальні символи, які можна використовувати для створення символьних класів, квантифікаторів, керувальних послідовностей тощо. Це такі символи:

[ ] \ ^ $ . | ? * + ( ) { }

Значення символів залежить від їхнього розташування у виразі. Щоб отримати відображення метасимволу як звичайного символу тексту, йому повинен передувати зворотний слеш (\). Щоб отримати безпосередньо символ \, потрібно використовувати два символи зворотної косої риски (\\).

Набір символів у квадратних дужках [ ] – це так званий символьний клас. Він дозволяє вказати, що на цьому місці у рядку може стояти один із перелічених символів. Наприклад, [abc] задає можливість появи в тексті одного із трьох зазначених символів, [1234567890] визначає відповідність однієї із цифр. Можна вказати діапазон символів: наприклад, [A-Za-z] відповідає всім літерам латинського алфавіту. Якщо потрібно вказати символи, які не входять до вказаного набору, то використовують символ ^ усередині квадратних дужок. Наприклад, [^0-9] – це будь-який символ окрім цифр.

Приклад використання виразу [ ]: "[bcr]at" відповідають рядки "bat", "cat" і "rat"; виразу "[^bcr]at" ці рядки не відповідатимуть, а рядок "hat" – буде.

Для створення єдиного символьного класу, що поєднує два або більше різних символьних класів, використовують об'єднання. Для створення об'єднання один клас вкладають всередину іншого: [0-3[7-9]], що відповідає цифрам 0, 1, 2, 3, 7, 8, 9.

Можливе створення перетину – єдиного символьного класу, який визначає рядки, що підходять обом вкладеним класам. Наприклад, вираз [0-9&&[234]] відповідає цифрам 2, 3 та 4.

Віднімання використовують для заперечення одного або декількох символів класу, наприклад, вираз [0-9&&[^345]] створює клас, який відповідає цифрам від 0 до 9, крім цифр 3, 4 і 5.

Для позначення деяких символьних класів також використовують керувальні послідовності:

Представлення Еквівалент Значення
\d [0-9] Цифра
\D [^\d] Будь-який символ крім цифри
\w [A-Za-zа-Яа-Я0-9_] Символи, що утворюють "слово" (літери, цифри та символ підкреслення)
\W [^\w] Символи, що не утворюють "слова"
\s [ \t\v\r\n\f] Розділювач
\S [^\s] Не розділювач

У фігурних дужках можна визначати кількість повторення символів, наприклад:

  • {2} (саме два символи),
  • {2, 4} (від двох до чотирьох),
  • {1,} (один або більше).
  • крапка – саме один (будь-який) символ,
  • * нуль або більше,
  • + один або більше,
  • ? нуль або один.

Наприклад, вираз "A*" відповідає будь-якій кількості послідовно розташованих символів A. Такому шаблону буде відповідати й порожній рядок. Вираз "A+" відповідає рядку з як мінімум одним символом A. Вираз "A {1,4}" відповідає рядкам "A", "AA", "AAA", "AAAA". Вирази "AB?" відповідатимуть рядкам "A" і "AB". Виразу "." відповідає будь-який рядок, що складається з одного символу. Вираз ".+" буде відповідати будь-якому тексту (один або більше довільних символів). Виразу "A.+" відповідає будь-який рядок, який починається на літеру "A".

Існують спеціальні символи, які відповідають не будь-якому символу, що перевіряється в рядку, а деякому місцю в цьому рядку. Символом ^ визначається початок рядка символом $ – кінець рядка. Наприклад, вираз "^this" відповідає рядку, який починається на "this".

Послідовність \b вказує на межу слова, тобто на місце між словом та пропуском. Вираз "\bis" відповідає другому "is" у рядку "this is". Вираз "\b\w\w\w\w\b" відповідає слову із чотирьох букв. Послідовність "\B" відповідає всім місцям, окрім межі слова. Вираз "\Bis" відповідає першому "is" у "this is".

Можна використовувати символ "|" ("АБО"), коли необхідно вказати декілька варіантів, яким може відповідати рядок:

Pattern pattern = Pattern.compile("Ivanov|Petrov");
Matcher matcher = pattern.matcher("These are Ivanov and Petrov and another Ivanov");
while (matcher.find()) {
    System.out.printf( "\"%s\"", matcher.group());
}

У наведеному вище прикладі буде знайдено відповідність кожному з варіантів ("Ivanov" "Petrov" "Ivanov").

Якщо символ "|" використовується не окремо, а у поєднанні з іншими елементами, то вираз із цим символом потрібно брати у круглі дужки.

Деякі функції класу String використовують регулярні вирази. Наприклад, функції replaceFirst() і replaceAll() повертають рядки, для яких пошук та заміна реалізовані регулярними виразами.

Наприклад, потрібно видалити пропуски на початку та в кінці рядка:

Pattern p = Pattern.compile("^\\s+"); // всі пропуски на початку рядка
String s = p.matcher("  These are Ivanov and Petrov and another Ivanov  ").replaceFirst("");
p = Pattern.compile("\\s+$");         // всі пропуски в кінці рядка
s = p.matcher(s).replaceAll("");
System.out.println(s);

3 Приклади програм

3.1 Перегляд доступних локалізацій

Метод getAvailableLocales() дозволяє переглянути доступні локалізації. Метод toString() повертає скорочене подання локалізації. За допомогою функції getDisplayName() можна отримати більш детальну інформацію:

package ua.inf.iwanoff.java.advanced.second;

import java.util.Locale;
import java.text.NumberFormat;

public class AvailableLocales {

    public static void main(String[] args) {
        Locale list[] = NumberFormat.getAvailableLocales();
        for (Locale locale : list) {
            System.out.printf("%-25s %s%n", locale.toString(), locale.getDisplayName());
        }
    }

}

3.2 Перший підрядок, який відповідає шаблону

Наведена нижче програма знаходить перший підрядок, який відповідає шаблону.

package ua.inf.iwanoff.java.advanced.second;

import java.util.Scanner;
import java.util.regex.Pattern;
import java.util.regex.Matcher;

public class RegexTest {

    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        System.out.printf("%nВведіть регулярний вираз: ");
        String ex = scan.nextLine();
        Pattern pattern = Pattern.compile(ex);
        System.out.printf("Введіть рядок для пошуку: ");
        Matcher matcher = pattern.matcher(scan.nextLine());
        if (matcher.find()) {
            System.out.printf("Знайдено текст " + "\"%s\", починаючи з позиції "
                       + "%d і закінчуючи позицією %d.%n", matcher.group(),
                       matcher.start(), matcher.end());
        }
    }

}

3.3 Перевірка формату телефонного номера

Припустимо, необхідно написати програму, яка перевіряє введений рядок на відповідність формату номера стаціонарного телефону Харківської телефонної мережі. Вважати, що номери мають бути записані як групи цифр, розділені дефісами. Телефонний номер без коду країни й міста може бути семизначним або шестизначним. Обов'язково повинні бути введені лише останні шість або сім цифр. Врахувати, що номер може містити код країни (три цифри), зі знаком "+" на початку і код міста (57- для семизначних номерів або 572- для шестизначних номерів).

package ua.inf.iwanoff.java.advanced.second;

import java.util.Scanner;
import java.util.regex.Pattern;
import java.util.regex.Matcher;

public class RegexTest {

    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        System.out.printf("%nВведіть номер телефону: ");
        String phone = scan.nextLine();
        Pattern p = Pattern.compile("\\+?(380-)?((57-)?\\d{3}|(572-)?\\d{2})-\\d{2}-\\d{2}$");
        Matcher m = p.matcher(phone);
        if (m.matches()) {
            System.out.println("\"" + phone + "\" - правильний формат номеру");
        }
        else {
            System.out.println("\"" + phone + "\" - хибний формат номеру");
        }
    }
 
}

3.4 Виведення слів речення

Припустимо, необхідно здійснити введення речення з клавіатури та виведення його слів в окремих рядках. Скористуємося методом split() класу String:

package ua.inf.iwanoff.java.advanced.second;

import java.util.Scanner;

public class UsingSplit {

    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        String sentence = scan.nextLine();
        String[] words = sentence.split("\\s");
        for (String word : words) {
            System.out.println(word);
        }
    }

}

У виклику методу split() вказано регулярний вираз, який визначає розділювач між словами.

3.5 Класи "Країна" та "Перепис населення"

У прикладах курсу "Основи програмування Java" було розглянуто ієрархію класів для представлення країни та переписів населення. У лабораторній роботі № 3 цього курсу було створено абстрактний клас AbstractCountry, а також конкретні класи Census і CountryWithArray. Далі, в лабораторній роботі № 4 було створено класи CountryWithList і CountryWithSet. І наостанок в лабораторній роботі № 5 було створено абстрактний клас CountryWithFile, а також конкретні класи CountryWithTextFile і CountryWithDataFile.

Тепер, використовуючи класи CountryWithList і Census, ми створимо застосунок, який

  • відтворює сортування, визначене в прикладі роботи № 3 курсу "Основи програмування Java";
  • здійснює форматування числових даних різними варіантами, а також з урахуванням локалізації;
  • здійснює виведення даних про дати й час подій з урахуванням локалізації;
  • здійснює виведення тексту українською та англійською мовою;
  • реалізує можливості сортування за алфавітом з використанням класу Collator;

До початку реалізації програми доцільно визначити в'язку ресурсів, в якій розташувати всі необхідні рядки. Файл censuses_en.properties:

name=Ukraine
area=Area
unit=sq.km.
density=Population density
census=Census
year=Year
population=Population
comments=Comments
commentsDate=Date and time of adding comments
firstPostwarCensus=The first postwar census
populationIncreases=Population increases
noComments=No comments
lastSovietCensus=The last soviet census
firstCensusInIndependentUkraine=The first census in the independent Ukraine
yearWithMaximumPopulation=The year with the maximum population
SortingByPopulation=Sorting by population
SortingByComments=Sorting by comments

Код файлу censuses_uk.properties, введений у середовищі IntelliJ IDEA:

name=Україна
area=Територія
unit=кв. км.
density=Густота населення
census=Перепис
year=Рік
population=Населення
comments=Коментар
commentsDate=Дата і час додавання коментаря
firstPostwarCensus=Перший післявоєнний перепис
populationIncreases=Нас побільшало
noComments=Просто перепис
lastSovietCensus=Останній радянський перепис
firstCensusInIndependentUkraine=Перший перепис у незалежній Україні
yearWithMaximumPopulation=Рік з найбільшим населенням
SortingByPopulation=Сортування за кількістю населення
SortingByComments=Сортування за алфавітом коментарів

Реальний вміст файлу censuses_uk.properties буде іншим і міститиме коди українських літер.

У новому класі CensusWithDates додаємо поле timeOfEditing типу ZonedDateTime для зберігання дати й часу редагування коментарів. Для того, щоб можна було здійснювати роботу з різними мовами, у класі CensusWithDates доцільно перевизначити метод getComments(), в якому поле comments застосовуватиметься не безпосередньо, а як ключ для пошуку іншого рядка у файлах в'язки ресурсів. Крім того, перевизначаємо методи toString() і testCensus() так, щоб виведення даних здійснювалося з урахуванням поточної локалізації. У функції main() послідовно змінюємо поточну локалізацію й виводимо результати. Сирцевий код класу CensusWithDates буде таким:

package ua.inf.iwanoff.java.advanced.second;

import ua.inf.iwanoff.java.advanced.first.CensusWithStreams;
import java.time.Month;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Locale;
import java.util.ResourceBundle;

/**
 * Клас відповідає за представлення перепису населення.
 * Враховується локалізація
 */
public class CensusWithDates extends CensusWithStreams {
    private ZonedDateTime timeOfEditing;

    /**
     * Конструктор ініціалізує об'єкт усталеними значеннями
     */
    public CensusWithDates() {
    }

    /**
     * Конструктор ініціалізує об'єкт вказаними значеннями
     *
     * @param year          рік перепису
     * @param population    кількість населення
     * @param comments      текст коментаря
     * @param timeOfEditing час редагування коментаря
     */
    public CensusWithDates(int year, int population, String comments, ZonedDateTime timeOfEditing) {
        super(year, population, comments);
        this.timeOfEditing = timeOfEditing;
    }

    /**
     * Повертає кількість населення
     * @return час редагування коментаря у вигляді ZonedDateTime
     */
    public ZonedDateTime getTimeOfEditing() {
        return timeOfEditing;
    }

    /**
     * Встановлює час редагування коментаря
     * @param timeOfEditing час редагування коментаря у вигляді ZonedDateTime
     */
    public void setTimeOfEditing(ZonedDateTime timeOfEditing) {
        this.timeOfEditing = timeOfEditing;
    }

    /**
     * Повертає рядок коментаря до перепису, використовуючи в'язку ресурсів
     * @return коментар перепису у вигляді рядка
     */
    @Override
    public String getComments() {
        ResourceBundle bundle = ResourceBundle.getBundle("censuses");
        return bundle.getString(super.getComments());
    }

    /**
     * Надає подання перепису у вигляді рядка
     *
     * @return подання перепису у вигляді рядка
     */
    @Override
    public String toString() {
        ResourceBundle bundle = ResourceBundle.getBundle("censuses");
        DateTimeFormatter dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).
                withLocale(Locale.getDefault());
        return String.format(Locale.getDefault(), bundle.getString("census") + ". "
                + bundle.getString("year") + ": %d, "
                + bundle.getString("population") + ": %d, "
                + bundle.getString("comments") + ": %s, "
                + bundle.getString("commentsDate") + ": %s ",
                getYear(), getPopulation(), getComments(), getTimeOfEditing().format(dateFormatter));
    }
    /**
     * Здійснює тестування створення й виведення даних об'єкта
     */
    @Override
    protected void testCensus() {
        setYear(2001);
        setPopulation(48475100);
        setComments("firstCensusInIndependentUkraine");
        setTimeOfEditing(ZonedDateTime.of(2024, Month.MARCH.getValue(), 1, 10, 0, 0, 0, ZoneId.of("Europe/Kiev")));
        System.out.println(this);
    }
    
    /**
     * Програма тестування можливості виведення
     * з урахуванням різних локалізацій
     * @param args аргументи командного рядка (не використовуються)
     */
    public static void main(String[] args) {
        Locale.setDefault(Locale.US);
        new CensusWithDates().testCensus();
        Locale.setDefault(new Locale("uk"));
        new CensusWithDates().testCensus();
    }
}

У новому класі CountryWithLocalization перевизначаємо методи toString(), sortByComments(), createCountry() і testCountry(). У функції main() послідовно змінюємо поточну локалізацію й виводимо результати. Сирцевий код класу CountryWithLocalization буде таким:

package ua.inf.iwanoff.java.advanced.second;

import ua.inf.iwanoff.java.third.AbstractCountry;
import ua.inf.iwanoff.java.advanced.first.CountryWithStreams;
import ua.inf.iwanoff.java.third.Census;

import java.text.Collator;
import java.text.NumberFormat;
import java.time.Month;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Locale;
import java.util.ResourceBundle;

/**
 * Клас для представлення країни, в якій здійснюється перепис населення.
 * Враховується локалізація
 */
public class CountryWithLocalization extends CountryWithStreams {
    /**
     * Надає подання країни у вигляді рядка
     * @return подання країни у вигляді рядка
     */
    @Override
    public String toString() {
        ResourceBundle bundle = ResourceBundle.getBundle("censuses");
        StringBuilder result = new StringBuilder(getName() + ". " + bundle.getString("area")
                + ": " + getArea() + " " + bundle.getString("unit"));
        for (int i = 0; i < censusesCount(); i++) {
            result.append("\n").append(getCensus(i));
        }
        return result + "";
    }

    /**
     * Здійснює сортування послідовності переписів за алфавітом коментарів
     */
    @Override
    public void sortByComments() {
        Census[] arr = getCensuses();
        Arrays.sort(arr, new Comparator<Census>() {
            Collator collator = Collator.getInstance(Locale.getDefault());

            @Override
            public int compare(Census c1, Census c2) {
                return collator.compare(c1.getComments(), c2.getComments());
            }
        });
        setCensuses(arr);
    }

    /**
     * Допоміжна функція створення нового об'єкта "Країна"
     * @return посилання на новий об'єкт "Країна"
     */
    @Override
    public AbstractCountry createCountry() {
        ResourceBundle bundle = ResourceBundle.getBundle("censuses");
        setName(bundle.getString("name"));
        setArea(603628);
        System.out.println(addCensus(new CensusWithDates(1959, 41869000, "firstPostwarCensus",
                ZonedDateTime.of(2024, Month.MARCH.getValue(), 1, 10, 0, 0, 0, ZoneId.of("Europe/Kiev")))));
        System.out.println(addCensus(new CensusWithDates(1970, 47126500, "populationIncreases",
                ZonedDateTime.of(2024, Month.MARCH.getValue(), 3, 10, 30, 0, 0, ZoneId.of("Europe/Kiev")))));
        System.out.println(addCensus(new CensusWithDates(1979, 49754600, "noComments",
                ZonedDateTime.of(2024, Month.MARCH.getValue(), 1, 11, 30, 0, 0, ZoneId.of("Europe/Kiev")))));
        System.out.println(addCensus(new CensusWithDates(1989, 51706700, "lastSovietCensus",
                ZonedDateTime.of(2024, Month.MARCH.getValue(), 2, 14, 0, 0, 0, ZoneId.of("Europe/Kiev")))));
        System.out.println(addCensus(new CensusWithDates(2001, 48475100, "firstCensusInIndependentUkraine",
                ZonedDateTime.of(2024, Month.MARCH.getValue(), 4, 10, 0, 0, 0, ZoneId.of("Europe/Kiev")))));
        return this;
    }

    /**
     * Здійснює тестування методів класу
     */
    @Override
    public void testCountry() {
        ResourceBundle bundle = ResourceBundle.getBundle("censuses");
        NumberFormat numberFormat = NumberFormat.getInstance(Locale.getDefault());
        System.out.println(bundle.getString("yearWithMaximumPopulation") + ": " + maxYear() + "."
                + bundle.getString("density") + ": " + numberFormat.format(density(maxYear())) + "\n");

        sortByPopulation();
        System.out.println("\n" + bundle.getString("SortingByPopulation"));
        System.out.println(this);

        sortByComments();
        System.out.println("\n" + bundle.getString("SortingByComments"));
        System.out.println(this);
    }

    /**
     * Демонстрація роботи програми
     * @param args аргументи командного рядка (не використовуються)
     */
    public static void main(String[] args) {
        Locale.setDefault(Locale.US);
        new CountryWithLocalization().createCountry().testCountry();
        Locale.setDefault(new Locale("uk"));
        new CountryWithLocalization().createCountry().testCountry();

    }
}

4 Вправи для контролю

  1. Вивести на екран інформацію про всі часові пояси.
  2. Прочитати з клавіатури рядок та перевірити за допомогою регулярних виразів правильність представлення грошової суми.
  3. Прочитати з клавіатури рядок та перевірити за допомогою регулярних виразів правильність представлення часу.

5 Контрольні запитання

  1. Назвіть основні компоненти платформи Java SE.
  2. У чому різниця між класами Calendar та Date?
  3. Як створити екземпляр класу GregorianCalendar?
  4. Які засоби для роботи з календарем надає Java 8?
  5. Які засоби для роботи з часовими поясами надає Java 8?
  6. Яка різниця між інтернаціоналізацією та локалізацією?
  7. Як клас ResourceBundle використовується для інтернаціоналізації та локалізації застосунків?
  8. Яке загальне призначення пакета java.text?
  9. У чому переваги сортування за допомогою класу Collator?
  10. У чому полягає форматування даних?
  11. Як здійснюється форматування даних з урахуванням локалізації?
  12. Що таке регулярний вираз?
  13. Як у Java здійснюється робота з регулярними виразами?
  14. Які існують способи поділу тексту на лексеми?
  15. У чому перевага використання методу split() в порівнянні з засобами StringTokenizer?

 

up