Форматировать дату и время с часовым поясом

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

String inputDatetime = "Mon Dec 21 21:18:37 GMT 2020";
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withLocale(Locale.getDefault()).withZone(ZoneOffset.UTC);
TemporalAccessor date = fmt.parse(inputDatetime);

Но я получаю следующую ошибку:

java.time.format.DateTimeParseException: Text 'Mon Dec 21 21:18:37 GMT 2020' could not be parsed at index 0

В чем проблема с этим кодом?


person llaabbss    schedule 21.12.2020    source источник
comment
Я понял проблему. Если я попытаюсь отформатировать строку даты и времени любого шаблона, кроме yyyy-MM-dd HH: mm: ss, в случае неудачи. Поэтому я не могу сделать это со строкой Mon Dec 21 21:18:37 GMT 2020, поскольку эта строка несовместима с шаблоном yyyy-MM-dd HH: mm: ss. Поэтому, если я получаю дату и время из местоположения, мне нужно применить специальный шаблон к этой строке даты и времени   -  person llaabbss    schedule 23.12.2020
comment
@ OleV.V. Не дубликат. Связанный вопрос спрашивает о дате и времени без смещения или зоны. В этом вопросе задается вопрос о строковом вводе, включающем GMT в качестве указанного смещения.   -  person Basil Bourque    schedule 24.12.2020
comment
@llaabbss - Если один из ответов помог решить вашу проблему, вы можете помочь сообществу, отметив его как принятый. Принятый ответ помогает будущим посетителям уверенно использовать решение. Чтобы пометить ответ как принятый, вам нужно нажать большую галочку (✓) слева от отвечать.   -  person Arvind Kumar Avinash    schedule 24.12.2020


Ответы (2)


Предполагая, что ваша база данных имеет тип данных timestamp with time zone, это то, что вы должны использовать для хранения даты и времени из вашей строки. Ваша входная строка однозначно определяет момент времени, как и timestamp with time zone.

Затем вы не должны хранить дату и время в виде строки в определенном формате, который нравится вашей базе данных. Храните правильные объекты даты и времени. Начиная с JDBC 4.2 это означает объекты типов из java.time, современного API даты и времени Java, который вы уже используете. Тогда вам не нужно заботиться о формате. Обо всем позаботились за вас. Если тип данных вашей базы данных timestamp with time zone, сохраните OffsetDateTime в этом столбце. Вместо этого используется timestamp без часового пояса или datetime, вместо этого сохраняется LocalDateTime. Документация вашего драйвера JDBC должна предоставить вам более подробную информацию.

В чем проблема с этим кодом?

Я вижу несколько проблем с вашим кодом.

  • As you said yourself in the comment, you are trying to parse a string using a formatter with pattern yyyy-MM-dd HH:mm:ss, but your string clearly is not in yyyy-MM-dd HH:mm:ss format. So this is bound to fail. More specifically the format string begins with yyyy for year of era, for example 2020. So the formatter expects to find a four digit number for year at the beginning of your string. Instead it finds Mon and throws the exception. The exception message informs us that the string could not be parsed at index 0. Index 0 is the beginning of the string, where the Mon is. I am not sure, but it seems that you have been confusing input and output format. Converting a date-time from a string in one format to a string in a different format involves two operations:
    1. First you parse your string into a date-time object using a formatter that describes the format of your original string.
    2. Во-вторых, вы форматируете дату и время в строку с помощью средства форматирования, которое описывает формат результирующей строки.
  • Поскольку исходная строка написана на английском языке, при ее синтаксическом анализе необходимо использовать англоязычный языковой стандарт. Использование Locale.getDefault() будет работать на англоязычных устройствах, а затем внезапно выйдет из строя, когда однажды вы запустите его на устройстве с другими языковыми настройками. Так что это плохая идея.
  • TemporalAccessor - это низкоуровневый интерфейс, который нам редко следует использовать. Вместо этого проанализируйте вашу строку в ZonedDateTime, поскольку она содержит дату, время и часовой пояс (в строке GMT считается часовым поясом).

Если бы вы отформатировали дату и время в формате вашего DateTimeFormatter - что, как я уже сказал, я не думаю, что вам нужно, - сработало бы следующее:

    DateTimeFormatter inputParser = DateTimeFormatter
            .ofPattern("EEE MMM dd HH:mm:ss zzz yyyy", Locale.ROOT);
    DateTimeFormatter databaseFormatter
            = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss");
    
    String inputDatetime = "Mon Dec 21 21:18:37 GMT 2020";
    OffsetDateTime dateTimeToStore = ZonedDateTime.parse(inputDatetime, inputParser)
            .toOffsetDateTime()
            .withOffsetSameInstant(ZoneOffset.UTC);
    String formattedString = dateTimeToStore.format(databaseFormatter);
    
    System.out.println(formattedString);

Выход:

2020-12-21 21:18:37

person Ole V.V.    schedule 22.12.2020

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

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

Это требование состоит из двух частей: A. Анализировать и преобразовывать дату и время в указанном пользователем языковом стандарте и часовом поясе в эквивалентную дату и время в UTC (не только рекомендуется, но и требуется некоторыми базами данных. например, PostgreSQL) Б. Сохраните его в базе данных.

Шаги для выполнения первой части требований:

  1. Поскольку полученная дата и время находится в указанном пользователем часовом поясе, игнорируйте часовой пояс, содержащийся в строке даты и времени, и анализируйте его до LocalDateTime.
  2. Преобразуйте LocalDateTime в ZonedDateTime в указанном пользователем часовом поясе.
  3. Преобразуйте это ZonedDateTime в ZonedDateTime в UTC.
  4. Наконец, преобразуйте ZonedDateTime в OffsetDateTime.

Получив OffsetDateTime, вы можете сохранить его в базе данных следующим образом:

PreparedStatement st = conn.prepareStatement("INSERT INTO mytable (columnfoo) VALUES (?)");
st.setObject(1, odt);// odt is the instance of OffsetDateTime
st.executeUpdate();
st.close();

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

import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Arrays;
import java.util.Locale;
import java.util.Objects;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        // Test
        Scanner scanner = new Scanner(System.in);

        while (true) {
            System.out.print("Enter the date-time string (press Enter without entering anything to quit): ");
            String strDateTime = scanner.nextLine();
            if (strDateTime.isBlank()) {
                break;
            }

            boolean valid;

            // Create Locale
            Locale locale = null;
            do {
                valid = true;
                System.out.print("Enter language code e.g. en, fr, in: ");
                String languageTag = scanner.nextLine();
                if (!isValidForLocale(languageTag)) {
                    System.out.println("Invalid code. Please try again.");
                    valid = false;
                } else {
                    locale = Locale.forLanguageTag(languageTag);
                }
            } while (!valid);

            // Create ZoneId
            ZoneId zoneId = null;
            do {
                valid = true;
                System.out.print("Enter timezone in the format Continent/City e.g. Asia/Calcutta: ");
                String timezone = scanner.nextLine();
                try {
                    zoneId = ZoneId.of(timezone);
                } catch (Exception e) {
                    System.out.println("Invalid timezone. Please try again.");
                    valid = false;
                }
            } while (!valid);

            try {
                System.out.println(getDateTimeInUTC(strDateTime, locale, zoneId));
            } catch (DateTimeParseException e) {
                System.out.println("The date-time string has the following problem:\n" + e.getMessage());
                System.out.println("Please try again.");
            }
        }
    }

    static OffsetDateTime getDateTimeInUTC(String strDateTime, Locale locale, ZoneId zoneId)
            throws DateTimeParseException {
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("[uuuu-M-d H:m:s][EEE MMM d H:m:s zzz uuuu]", locale);

        // Ignore the timezone contained in strDateTime and parse strDateTime to
        // LocalDateTime. Then, convert the LocalDateTime to ZonedDateTime at zoneId.
        // Then, convert this ZonedDateTime to ZonedDateTime at UTC. Finally, convert
        // the ZonedDateTime to OffsetDateTime and return the same.
        ZonedDateTime zdt = LocalDateTime.parse(strDateTime, dtf).atZone(zoneId).withZoneSameInstant(ZoneOffset.UTC);
        return zdt.toOffsetDateTime();
    }

    static boolean isValidForLocale(String languageTag) {
        return Arrays.stream(Locale.getISOLanguages()).anyMatch(l -> Objects.equals(l, languageTag));
    }
}

Пробный запуск:

Enter the date-time string (press Enter without entering anything to quit): Mon Dec 21 21:18:37 GMT 2020
Enter language code e.g. en, fr, in: en
Enter timezone in the format Continent/City e.g. Asia/Calcutta: Asia/Calcutta
2020-12-21T15:48:37Z
Enter the date-time string (press Enter without entering anything to quit): 2020-1-23 5:15:8
Enter language code e.g. en, fr, in: en
Enter timezone in the format Continent/City e.g. Asia/Calcutta: Asia/Calcutta
2020-01-22T23:45:08Z
Enter the date-time string (press Enter without entering anything to quit): 
person Arvind Kumar Avinash    schedule 23.12.2020