В этой статье я хочу рассказать о классе MessageFormat, который используется в задаче 2412 (task2412). Класс MessageFormat предназначен для сборки строк по шаблону. Вы пишете текст, оставляя в нем места для подстановки вида {0} и {1}, а класс подставляет туда переданные аргументы и попутно форматирует их: число превращает в число с разделителем разрядов, дату в дату, и все это по правилам текущей локали. Живет он в пакете java.text, и придуман был именно ради интернационализации: шаблон целиком выносится в ресурсы и переводится вместе с порядком слов. Данный класс Java принимает набор объектов, форматирует их, а затем вставляет форматированные строки в шаблон в соответствующих местах. Это своего рода альтернатива (или даже дополнение) к статическому методу String.format.

Кратко

  • MessageFormat собирает строку по шаблону, подставляя аргументы в места вида {0}, {1}, {2}.
  • В фигурных скобках указывают не только номер аргумента, но и тип форматирования: number, date, time, choice, а к нему еще и стиль, например short или integer.
  • Работать можно двумя способами: разово через статический MessageFormat.format() или через объект new MessageFormat(шаблон), если шаблон нужен много раз.
  • Один и тот же аргумент разрешается вставить в шаблон несколько раз и каждый раз отформатировать по-своему.
  • ChoiceFormat выбирает вариант текста по числовому диапазону, это штатный способ получить правильные формы множественного числа.
  • Результат зависит от локали, поэтому вывод примеров ниже на другой машине может отличаться.
Логотип Java: чашка кофе с паром и надпись Core JAVA

Статический метод MessageFormat.format

Для начала простой пример использования данного Java класса без создания объекта, а используя статический метод:

        int planet = 7;
        String event = "a disturbance in the Force";
        String result = MessageFormat.format(
                "At {0, time, medium} on {0, date}, there was {1} on planet {2, number, integer}.",
                new Date(), event, planet);
        System.out.println(result);
Здесь вызывается статический метод MessageFormat.format, в который передаются аргументами шаблон строки и, собственно, объекты, которые будут вставлены в места, ограниченные скобками {}. В скобках задается позиция объекта начиная с 0, а также тип форматирования, если таковой имеется.

Что можно писать в фигурных скобках

Форма записи такая: {номер}, {номер, тип} или {номер, тип, стиль}. Типов всего четыре:
Тип Стили Чем форматируется
{0}, без типа нет Значение подставляется как есть, через toString()
number integer, currency, percent или свой шаблон NumberFormat для текущей локали
date short, medium, long, full или свой шаблон DateFormat.getDateInstance() для текущей локали
time short, medium, long, full или свой шаблон DateFormat.getTimeInstance() для текущей локали
choice шаблон ChoiceFormat ChoiceFormat, выбирает вариант текста по числу
Полная таблица типов и стилей есть в документации Oracle. Вывод будет такой:

At 21:25:54 on 28 апр. 2018 г., there was a disturbance in the Force on planet 7.

Локаль решает все

Если вы запустите первый пример у себя, вывод может отличаться от напечатанного выше. Причина в локали. Посмотрите еще раз на таблицу: каждый тип форматирования создает свой Format «для текущей локали», то есть для той, что стоит в системе по умолчанию. На русских настройках дата напечаталась как 28 апр. 2018 г., а число 1273 во втором примере получит неразрывный пробел между разрядами. На американских настройках те же строки дадут Apr 28, 2018 и 1,273. Локаль можно не отдавать на волю случая, а задать явно, вторым аргументом конструктора:

        MessageFormat form = new MessageFormat(
                "At {0, time, short} on {0, date, medium}, there was {1}.",
                Locale.US);
        System.out.println(form.format(
                new Object[]{new Date(), "a disturbance in the Force"}));
Ради этого класс и создавался. В настоящем многоязычном приложении шаблон не пишут в коде, а держат в ResourceBundle: для каждого языка свой файл со своей строкой, а MessageFormat подставит аргументы туда, куда требует порядок слов именно этого языка. Код при этом не меняется вообще. Отдельно про даты. Типы date и time создают под капотом DateFormat, а он, если верить документации, принимает только Date или число миллисекунд от начала эпохи. Передать туда LocalDate или LocalDateTime из пакета java.time не получится, будет IllegalArgumentException. Поэтому в примерах и стоит старый new Date(). Если у вас в коде java.time, значение придется либо превратить в Date через Date.from(instant), либо заранее отформатировать через DateTimeFormatter и подставить в шаблон уже готовой строкой.

Объект MessageFormat вместо статического вызова

В следующем примере уже создается объект класса MessageFormat:

        int fileCount = 1273;
        String diskName = "MyDisk";
        Object[] testArgs = {fileCount, diskName};

        MessageFormat form = new MessageFormat(
                "The disk \"{1}\" contains {0} file(s).");

        System.out.println(form.format(testArgs));
При создании объекта класса MessageFormat в его конструктор передается шаблон строки. Далее, при вызове метода format у объекта, туда в качестве аргумента передается массив объектов, которые будут вставлены в шаблон строки. Вывод будет такой:

The disk "MyDisk" contains 1 273 file(s).

Выбор текста по числу: ChoiceFormat

Также возможно сделать так, что в зависимости от значения переменной будет выбираться необходимый текст. Своего рода реализация оператора if...else, только с помощью класса ChoiceFormat. Вот код:

        MessageFormat form = new MessageFormat("Я могу {1} {0}.");
        int count = 2;
        String exercise = "подтянуться";
        Object[] testArgs = {count, exercise};

        double[] filelimits = {0,2,5};
        String[] filepart = {"{0} раз","{0} раза","{0} раз"};
        ChoiceFormat fileform = new ChoiceFormat(filelimits, filepart);
        form.setFormatByArgumentIndex(0, fileform);
        
        System.out.println(form.format(testArgs));
Данный код в зависимости от значения переменной count меняет вывод строки следующим образом:
  • если count = 1 вывод будет такой

    
    Я могу подтянуться 1 раз.
    
  • если count = 2 вывод будет такой

    
    Я могу подтянуться 2 раза.
    
  • если count = 7 вывод будет такой

    
    Я могу подтянуться 7 раз.
    
В данном коде создается массив double[] filelimits, в котором указываются пределы значений, при которых будет меняться вывод строк. А в массиве String[] filepart указываются те самые варианты строк, которые могут быть использованы. Условия выбора варианта строки от значения переменной определяются следующим образом: выберется вариант filepart[j], если filelimits[j] <= count < filelimits[j+1]. Далее создается объект ChoiceFormat fileform и в его конструктор передаются массивы double[] filelimits и String[] filepart. С помощью метода form.setFormatByArgumentIndex(0, fileform) мы говорим объекту MessageFormat form, что когда вызовут метод format, то для индекса 0 в шаблоне строки используй форматирование, которое было задано в объекте ChoiceFormat fileform.

Свой формат для каждого места подстановки: setFormats

Хорошо. У вас уже в принципе могла появиться мысль никогда не использовать Java класс MessageFormat, но знайте, что это еще не все примочки. Имеется вот такой код:

        MessageFormat pattform = new MessageFormat("There {0} on {1}.\n{2} {2}");
        int count = 0;
        Date date = new Date();
        Object[] testArgs = {count, "ADisk", date, date};

        double[] filelimits = {0,1,2};
        String[] filepart = {"are no files","is one file","are {0} files"};
        ChoiceFormat fileform = new ChoiceFormat(filelimits, filepart);

        Format[] testFormats
                = {fileform, null, DateFormat.getDateInstance(DateFormat.SHORT), DateFormat.getTimeInstance(DateFormat.SHORT)};
        pattform.setFormats(testFormats);
        
        System.out.println(pattform.format(testArgs));
Здесь основная фишка в массиве Format[] testFormats. В данный массив мы заносим объекты (реализующие абстрактный класс Format), которые отвечают за форматирование объектов, определенных в массиве Object[] testArgs. Если объект не нуждается в форматировании, то в массиве Format[] testFormats записывается null. Далее с помощью метода pattform.setFormats(testFormats) мы говорим объекту MessageFormat pattform, какое форматирование применять к местам подстановки в шаблоне строки. Важная деталь: этот метод раздает форматы по порядку мест подстановки в шаблоне, а не по номерам аргументов. В шаблоне "There {0} on {1}.\n{2} {2}" таких мест четыре, поэтому и в массиве Format[] testFormats четыре элемента, хотя разных аргументов всего три: {2} встречается дважды и форматируется двумя разными способами, сначала как дата, потом как время. Для данного примера вывод будет такой:

There are no files on ADisk.
28.04.18 22:10
И совет напоследок. Документация Oracle рекомендует вместо setFormats брать метод setFormatsByArgumentIndex: порядок мест подстановки в шаблоне при переводе на другой язык почти всегда меняется, а номера аргументов остаются прежними. Тот же пример через него выглядел бы так:

        Format[] testFormats
                = {fileform, null, DateFormat.getDateInstance(DateFormat.SHORT)};
        pattform.setFormatsByArgumentIndex(testFormats);
Теперь элементы массива соответствуют аргументам: нулевому, первому и второму. За удобство приходится платить: оба вхождения {2} получат один и тот же формат, и развести дату и время, как в примере выше, уже не выйдет. Так что setFormats тоже нужен, просто применять его стоит осознанно. На этом все, и я надеюсь, что при решении задачи 2412 у вас не возникнет сложностей.

Вопросы и ответы

Чем MessageFormat отличается от String.format?

Разметкой шаблона и назначением. В String.format место подстановки задается типом значения (%s, %d) и заполняется строго по порядку аргументов. В MessageFormat место подстановки задается номером аргумента, поэтому при переводе на другой язык порядок слов в предложении можно менять, не трогая код. Плюс у MessageFormat есть choice для форм множественного числа, чего в String.format нет.

Как вставить в шаблон фигурную скобку или апостроф?

Через одинарные кавычки. Пара одинарных кавычек экранирует любые символы: шаблон '{0}' выдаст текст {0}, а не подстановку. Сам апостроф записывается двумя одинарными кавычками подряд. Это главная ловушка класса: строка вида Don't {0} сломается, потому что апостроф в слове откроет экранирование, и {0} останется в тексте как есть. Писать надо Don''t {0}.

Можно ли использовать один аргумент в шаблоне несколько раз?

Да, и это одно из главных отличий от String.format. Номер в скобках это индекс в массиве аргументов, а не порядковый номер подстановки, поэтому {2} можно написать хоть пять раз. Более того, каждое вхождение можно отформатировать по-своему: в последнем примере статьи один и тот же объект Date выводится сначала как дата, потом как время.

Как получить правильные формы множественного числа?

Для этого и нужен ChoiceFormat: вы задаете границы диапазонов и вариант текста для каждого. Учтите ограничение: выбор идет по числовым границам, а не по правилам языка. Схема из примера выше верна для 1, 2, 5 и 7, но на 22 она даст «22 раз» вместо «22 раза», потому что 22 попадает в последний диапазон. Для полностью правильных русских форм границы приходится расписывать подробнее или брать библиотеку с поддержкой правил ICU.

Работает ли MessageFormat с LocalDate и LocalDateTime?

Напрямую нет. Тип date в шаблоне создает DateFormat, а он, по документации, принимает только Date или число миллисекунд от начала эпохи. Передадите LocalDate, получите IllegalArgumentException. Обходных путей два: превратить значение в Date через Date.from(instant) либо заранее отформатировать его через DateTimeFormatter и подставить в шаблон уже готовой строкой.

Потокобезопасен ли MessageFormat?

Нет. Документация говорит прямо: объекты MessageFormat не синхронизированы, и для каждого потока рекомендуется создавать свой экземпляр, а при общем доступе синхронизировать его самостоятельно. Держать один объект в статическом поле и звать из нескольких потоков нельзя. Статический MessageFormat.format() этой проблемы не имеет: он создает новый объект на каждый вызов.

Читайте также