Форматирование в библиотеке JSTL — теги fmt

В прошлой статье начал разбираться с тегами из библиотеки JSTL (Jakarta Standard Tag Library или ранее JavaServer Pages Standard Tag Library). Тогда речь шла о core-тэгах, а теперь пришло время форматирования и тегов fmt.

Чтобы начать пользоваться этой библиотекой, в файл pom.xml нужно добавить зависимость.

        <dependency>
            <groupId>jstl</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>

И на страницах jsp подключаем библиотеку следующим образом:

<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>

Это позволит нам использовать теги для форматирования, и все они будут начинаться с указанного префикса fmt.

Форматирование даты <fmt:formatDate>

В этот тег отдаём объект типа java.util.Date в атрибут value и получаем отформатированное отображение. Это обязательный атрибут, но у этого тега довольно много дополнительных настроек:

Для примера взял дату чарта из своего пет-проекта и оформил её в разные стили:

  <b>дата без форматирования: </b></td><td>${chartDate}</td></tr>
  <b>только дата:</b> <fmt:formatDate type="date" value="${chartDate}" />
  <b>только время:</b> <fmt:formatDate type="time" value="${chartDate}" />

  <b>Короткий формат:</b> <fmt:formatDate type="both" dateStyle="short" timeStyle="short" value="${chartDate}" />
  <b>Средний формат:</b> <fmt:formatDate type="both" dateStyle="medium" timeStyle="medium" value="${chartDate}" />
  <b>Длинный формат:</b> <fmt:formatDate type="both" dateStyle="long" timeStyle="long" value="${chartDate}" />

  <b>SQL формат даты: </b> <fmt:formatDate pattern="yyyy-MM-dd"  value="${chartDate}" />
  <b>Мой формат даты: </b> <fmt:formatDate pattern="dd.MM HH:mm"  value="${chartDate}" />
  <b>Мой формат даты: </b> <fmt:formatDate pattern="MMM yyyy G"  value="${chartDate}" />

Если отформатированную дату потом нужно использовать ещё раз, можно сохранить её в переменную с помощью атрибута var:

<fmt:formatDate pattern="dd.MM HH:mm" value="${chartDate}" var="savedDate" />
<c:out value="${savedDate}" />

Парсинг даты из строки <fmt:parseDate>

Парный тег для formatDate с такими же атрибутами. В value на этот раз размещаем строку, которую необходимо запарсить.

<fmt:parseDate value="${chartDateStr}" var="savedDate" pattern="yyyy-MM-dd" />
<c:out value="${savedDate}" />

Форматирование чисел <fmt:formatNumber>

Аналоги прошлых тегов существует и для чисел. Если нужно натсроить группировку символов в числе, задать разделитесь (точку или запятую), добавить символ валюты и вообще настроить локаль для вывода числовых значений, то это нужный тег.

Кроме логичного атрибута value, где помещается число для форматирования, у тега есть ещё несколько необязательных, но важных атрибутов:

  • type со значениями number, currency или percent позволяет задать тип числа
  • pattern для установки шаблона форматирования
  • maxIntegerDigits и minIntegerDigits задают максимум и минимум чисел до запятой
  • maxFractionDigits и minFractionDigits делают то же с дробной частью
  • groupingUsed устанавливает необходимость разделять группы цифр для читабельности

В моём проекте форматирования чисел не было, поэтому для примеров я поиграл со случайным числом из всех цифр подряд:

<c:set var="number" value="12345.6789"/>

Число до форматирования: ${number} <br>
Проценты: <fmt:formatNumber value="${number}" type="percent" /> <br>
Валюта: <fmt:formatNumber value="${number}" type="currency" /> <br>

Применение шаблона без группировки: <fmt:formatNumber value="${number}" 
                  maxIntegerDigits="10" minIntegerDigits="1"
                  maxFractionDigits="10" minFractionDigits="1" 
                  pattern="#,###.##" groupingUsed="false"/> <br>
Шаблон с группировкой: <fmt:formatNumber value="${number}" 
                  maxIntegerDigits="10" minIntegerDigits="1"
                  maxFractionDigits="10" minFractionDigits="1" 
                  pattern="#,##0.00"/> <br>

Ограничение на количество цифр: <fmt:formatNumber value="${number}" 
                  maxIntegerDigits="2" maxFractionDigits="2"/> <br>
Нехватка цифр: <fmt:formatNumber value="${number}" 
                  minIntegerDigits="6" minFractionDigits="6" /> <br>

На выводе получаем следующее:

Число до форматирования: 12345.6789
Проценты: 1 234 568 %
Валюта: 12 345,68 ¤
Применение шаблона без группировки: 12345.6789
Шаблон с группировкой: 12,345.6789
Ограничение на количество цифр: 45.68

До форматирования число выводилось в точности таким, каким я его ввёл. Без группировки все цифры слепились вместе, а с группировкой группы, заданные шаблоном (в моём случае по три цифры) разделены пробелом.

Если цифр меньше, чем минимальное количество, то недостача восполняется нулями. И наоборот, если цифр слишком много, то лишние обрубаются. И тут надо быть осторожными, потому что в дробной части происходит округление, а в целой числа просто исчезают.

При составлении шаблонов нужно помнить, что в шаблоне точка значит отделение целой от дробной части, а запятая — это разделение групп цифр.

<fmt:formatNumber value="${number}" pattern="#,##0.00"/> <br>
<fmt:formatNumber value="${number}" pattern="# ##0,00"/> <br>

То есть в этом коде можно было бы ожидать вывода в стиле 12,345.68 и 12 345,68. Но вместо этого запятая во втором случае воспринимается, как разделение групп, и шаблон дарит нам странный вариант 1 23 45 без дробной части.

Чтобы поменять эти символы, нужно менять не шаблон, а локаль.

Задание локали с <fmt:setLocale>

Итак, в прошлом примере случились некоторые проблемы с разделительными символами. Да и символ валюты у меня в браузере выбрался каким-то странным круглешочком.

Чтобы исправить ситуацию, нужно указать нужную локаль.

<fmt:setLocale value="ru_RU" />
<fmt:formatNumber value="${number}" pattern="#,##0.00"/> <br>
Валюта: <fmt:formatNumber value="${number}" type="currency" /> <br>
<fmt:setLocale value="en_US" />
<fmt:formatNumber value="${number}" pattern="#,##0.00"/> <br>
Валюта: <fmt:formatNumber value="${number}" type="currency" /> <br>

И на выводе получаем:

12 345,68
Валюта: 12 345,68 ₽
12,345.68
Валюта: $12,345.68

Один и тот же шаблон интерпретируется по-разному в зависимости от выбранной локали. В России принято отделять целую часть от дробной запятой, а в США — точкой. И знак валюты ставится не только правильный, но и в нужном месте относительно суммы.

Парсинг чисел <fmt:parseNumber>

Этот тег поможет распарсить строку с цифровыми значениями, будь то денежные суммы, проценты или просто какие-то числа.

И снова кроме value есть несколько полезных атрибутов:

  • уже знакомый type, который может принимать значения number, currency или percentage
  • ещё один знакомый уже атрибут pattern для задания пользовательских шаблонов
  • parseLocale, который на этот раз поможет указывать нужную локаль отдельно для каждого парсинга
  • integerOnly со значениями true или false — указывает, что парсить нужно только целую часть числа
<c:set var="numberStr" value="1,234.56"/> <br>
<fmt:parseNumber value="${numberStr}" parseLocale="en_US" integerOnly="true" pattern="#,###.##"/><br>

Этот код выведет на экран число 1234, так как целую часть мы попросили откинуть.

А эти два тега покажут важность локалей ещё раз:

<c:set var="numberStr" value="1234,56"/> <br>
en_US: <fmt:parseNumber value="${numberStr}" parseLocale="en_US"/> <br>
ru_RU: <fmt:parseNumber value="${numberStr}" parseLocale="ru_RU"/> <br>

В первом случае американская локаль предполагает, что запятая разделяет группы цифр, и мы после парсинга получаем целое число. Во втором случае запятая отделяет дробную часть от целой, и число получается уже корректным:

en_US: 123456
ru_RU: 1234.56

Одна лишь проблема: если в строке есть пробелы, то парсинг будет продолжаться до первого проблема, какую локаль ни ставь и какой паттерн ни задавай:

<c:set var="numberStr" value="1 234,56"/> <br>
<fmt:parseNumber value="${numberStr}" parseLocale="en_US"/> <br>
<fmt:parseNumber value="${numberStr}" parseLocale="ru_RU"/> <br>

Этот пример выдаст две единицы. К остальной части даже не притронется.

Локализация сообщений с помощью <fmt:bundle>, <fmt:setBundle> и <fmt:message>

Чтобы отображать строки в зависимости от локали пользователя, используем теги <fmt:bundle> и <fmt:message>.

Для начала нужно создать файлы .properties для каждого языка. Например messages_ru_RU.properties и messages_en_US.properties.

greeting=Привет, мир!
farewell=До свидания!
greeting=Hello, world!
farewell=Goodbye!

Я работаю в Intellij Idea, и она автоматически объединит эти файлы в «resource bundle». Если вы захотите переименовать файлы, то достаточно один раз переименовать бандл, и все файлы для разных локализаций переименуются разом.

После этого на jsp странице можно использовать этот бандл. В атрибуте basename прописываем адрес к ресурсу. Мой лежит в папке src/resources и при сборке проекта оказывается в папке WEB-INF/classes. По сути, находится в корне, поэтому достаточно указать имя бандла, и не прописывать полный адрес.

<fmt:setLocale value="ru_RU"/>
<fmt:bundle basename="messages">
    <p><fmt:message key="greeting"/></p>
    <p><fmt:message key="farewell"/></p>
</fmt:bundle>

<fmt:setLocale value="en_US" />
<fmt:bundle basename="messages">
    <p><fmt:message key="greeting"/></p>
    <p><fmt:message key="farewell"/></p>
</fmt:bundle>

Здесь в тегах <fmt:message> указан ключ (key) с названием строки из наших .properties файлов. В выводе получим сначала русские строки, потом уже английские.

Тег<fmt:bundle> позволяет использовать выбранные ресурсы только в теле тега, но если их нужно использовать много раз на всей странице, то выбираем тег <fmt:setBundle>:

<fmt:setBundle basename="messages" var="msg"/>
<p><fmt:message key="greeting" bundle="${msg}"/></p>
<p><fmt:message key="farewell"  bundle="${msg}"/></p>

В атрибуте basename так же указываем адрес ресурса, а в атрибуте var — название переменной, т.е. как мы будем обращаться к этому конкретному бандлу на странице. Поэтому в тегах <fmt:messsage> нужно прописать ещё один атрибут bundle и уже в нём назвать нужный бандл, как мы это сделали ранее в var.

Но самое важное, чего я не заметил ни в одном другом туториале — это изменения в файле web.xml. Без них я получал названия ключей со знаками вопросов вместо самих строк (???greeting??? и ???farewell???) при использовании <fmt:bundle>, а для <fmt:setBundle> исключение с сообщением:

Unable to convert string [msg] to class [javax.servlet.jsp.jstl.fmt.LocalizationContext] for attribute [bundle]

Итак, добавляем в web.xml следующий код для обозначения контекста:

    <context-param>
        <param-name>javax.servlet.jsp.jstl.fmt.localizationContext</param-name>
        <param-value>messages</param-value>
    </context-param>

Всё, теперь можем использовать этот бандл на любой jsp странице в проекте.

Значения параметров для <fmt:message> с <fmt:param>

Когда нужно не просто подгрузить строки на разных языках, а ещё и нужно в них добавить значение какого-то параметра (например, обратиться к юзеру по имени), то нужно использовать <fmt:param>.

Я обновил файлы .properties следующим образом:

greeting=Hello, {0}!
farewell=Goodbye {0}!

{0} обозначает номер параметра, поэтому при задании их значений важно не перепутать порядок.

Теперь, если ничего не поменять, и оставить теги <fmt:message> без параметров, получим безликое обращение:

<fmt:setLocale value="en_US" />
<fmt:bundle basename="messages">
    <p><fmt:message key="greeting"/></p>
    <p><fmt:message key="farewell"/></p>
</fmt:bundle>
Hello, {0}!
Goodbye {0}!

Но если добавить тег <fmt:param> с атрибутом value, то наши обращения засияют новыми красками:

<c:set var="someone" value="world" />
<fmt:setLocale value="en_US"/>
<fmt:bundle basename="messages">
    <fmt:message key="greeting">
        <fmt:param value="${someone}" />
    </fmt:message> <br/>
    <fmt:message key="farewell">
        <fmt:param value="${someone}" />
    </fmt:message> <br/>
</fmt:bundle>

Здесь я сначала объявляю переменную someone со значением world (можно этого не делать, если такая переменная получается с бэка). Потом уже в каждый тег <fmt:message> помещаю нужное число параметров (в моём случае один, с индексом 0). Получаю вежливое обращение к миру:

Hello, world!
Goodbye world!

Установка кодировки с <fmt:requestEnconding>

Этот тег используется для установки кодировки запроса в JSP. Этот тег полезен, когда вам нужно убедиться, что данные, отправленные клиентом на сервер, интерпретируются правильно.

Тег должен быть размещён до того, как произойдет любое чтение параметров запроса или тела запроса. Можно его ставить сразу после декларации страницы:

<fmt:requestEncoding value="UTF-8" />

В атрибуте value указываем название кодировки.

Установка часового пояса с <fmt:setTimeZone> и <fmt:timeZone>

Эти два тега используются для задания нужного часового пояса. <fmt:timeZone> позволяет работать с заданным часовым поясом в теле тега:

<c:set var="now" value="<%=new java.util.Date()%>" />

Дата и время в моём часовом поясе:
<fmt:formatDate value="${now}" type="both" />
<br/>
Дата и время в часовом поясе GMT: 
<fmt:timeZone value="GMT">
        <fmt:formatDate value="${now}" timeZone="${timeZone}" type="both" />
</fmt:timeZone>

В этом примере сначала задаю переменную now, куда помещаю новую дату (по умолчанию значение java.util.Date устанавливается на текущее время). Первой строчкой вывожу дату и время без корректировки часового пояса. Потом в теге <fmt:timeZone> делаю то же самое, но уже для часового пояса GMT (задаётся в атрибуте value). Получаю на экране:

Дата и время в моём часовом поясе: 10 мар. 2025 г., 16:20:13
Дата и время в часовом поясе GMT: 10 мар. 2025 г., 11:20:13

Конечно, дату можно получать и из бэка, но в моём проекте у дат нет времени (то есть они все установлены на полночь), поэтому для этого примера они были бы не такими наглядными.

Тег <fmt:setTimeZone> позволяет использовать один часовой пояс на всей странице после этого тега.

<c:set var="now" value="<%=new java.util.Date()%>" />

Дата и время в моём часовом поясе:
<fmt:formatDate value="${now}" type="both" />
<br/>

<fmt:setTimeZone value="GMT" />
Дата и время в часовом поясе GMT:
<fmt:formatDate value="${now}" timeZone="${timeZone}" type="both" />
<br/>
И ещё раз в поясе GMT:
<fmt:formatDate value="${now}" timeZone="${timeZone}" type="both" />

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

Дата и время в моём часовом поясе: 10 мар. 2025 г., 16:24:51
Дата и время в часовом поясе GMT: 10 мар. 2025 г., 11:24:51
И ещё раз в поясе GMT: 10 мар. 2025 г., 11:24:51

Уффф… какая длинная получилась статья. Вроде тегов в этой библиотеке не очень много, но форматирование и локали для меня всегда были одной из самых сложных тем (потому что это всё очень муторно). С этой статьёй я и сам разобрался в форматировании немного лучше, надеюсь, что и вам она помогла. А впереди ещё теги функций fn.

Оставьте комментарий