it-swarm.com.ru

Java 8 Time API - ZonedDateTime - указывать ZoneId по умолчанию при разборе

Я пытаюсь написать универсальный метод для возврата ZonedDateTime с указанной датой в виде String и ее форматом. 

Как заставить ZonedDateTime использовать значение по умолчанию ZoneId, если оно не указано в дате String

Это можно сделать с помощью Java.util.Calendar, но я хочу использовать Java 8 time API.

Этот вопрос здесь использует фиксированный часовой пояс. Я указываю формат в качестве аргумента. Дата и ее формат являются аргументами String. Более общий. 

Код и вывод ниже: 

public class DateUtil {
    /** Convert a given String to ZonedDateTime. Use default Zone in string does not have zone.  */
    public ZonedDateTime parseToZonedDateTime(String date, String dateFormat) {
        //use Java.time from Java 8
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat);
        ZonedDateTime zonedDateTime = ZonedDateTime.parse(date, formatter);
        return zonedDateTime;
    }

    public static void main(String args[]) {
        DateUtil dateUtil = new DateUtil();
        System.out.println(dateUtil.parseToZonedDateTime("2017-09-14 15:00:00+0530", "yyyy-MM-dd HH:mm:ssZ"));
        System.out.println(dateUtil.parseToZonedDateTime("2017-09-14 15:00:00", "yyyy-MM-dd HH:mm:ss"));
    }
}

Результат 

2017-09-14T15:00+05:30
Exception in thread "main" Java.time.format.DateTimeParseException: Text '2017-09-14 15:00:00' could not be parsed: Unable to obtain ZonedDateTime from TemporalAccessor: {},ISO resolved to 2017-09-14T15:00 of type Java.time.format.Parsed
    at Java.time.format.DateTimeFormatter.createError(DateTimeFormatter.Java:1920)
    at Java.time.format.DateTimeFormatter.parse(DateTimeFormatter.Java:1855)
    at Java.time.ZonedDateTime.parse(ZonedDateTime.Java:597)
    at com.nam.sfmerchstorefhs.util.DateUtil.parseToZonedDateTime(DateUtil.Java:81)
    at com.nam.sfmerchstorefhs.util.DateUtil.main(DateUtil.Java:97)
Caused by: Java.time.DateTimeException: Unable to obtain ZonedDateTime from TemporalAccessor: {},ISO resolved to 2017-09-14T15:00 of type Java.time.format.Parsed
    at Java.time.ZonedDateTime.from(ZonedDateTime.Java:565)
    at Java.time.format.Parsed.query(Parsed.Java:226)
    at Java.time.format.DateTimeFormatter.parse(DateTimeFormatter.Java:1851)
    ... 3 more
Caused by: Java.time.DateTimeException: Unable to obtain ZoneId from TemporalAccessor: {},ISO resolved to 2017-09-14T15:00 of type Java.time.format.Parsed
    at Java.time.ZoneId.from(ZoneId.Java:466)
    at Java.time.ZonedDateTime.from(ZonedDateTime.Java:553)
    ... 5 more
8
RuntimeException

Для ZonedDateTime требуется часовой пояс или смещение, а на втором входе его нет. (Содержит только дату и время).

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

Одна альтернатива - сначала попытаться создать ZonedDateTime, а если это невозможно, то создать LocalDateTime и преобразовать его в часовой пояс:

public ZonedDateTime parseToZonedDateTime(String date, String dateFormat) {
    // use Java.time from Java 8
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat);
    ZonedDateTime zonedDateTime = null;
    try {
        zonedDateTime = ZonedDateTime.parse(date, formatter);
    } catch (DateTimeException e) {
        // couldn't parse to a ZoneDateTime, try LocalDateTime
        LocalDateTime dt = LocalDateTime.parse(date, formatter);

        // convert to a timezone
        zonedDateTime = dt.atZone(ZoneId.systemDefault());
    }
    return zonedDateTime;
}

В приведенном выше коде я использую ZoneId.systemDefault(), которая получает часовой пояс по умолчанию JVM, но этот может быть изменен без уведомления, даже во время выполнения , поэтому лучше всегда указывать, какой именно вы используете.

API использует имена часовых поясов IANA _ (всегда в формате Region/City, например America/Sao_Paulo или Europe/Berlin) . Избегайте использования трехбуквенных сокращений (например, CST или PST), поскольку они являются неоднозначными и не стандарт .

Вы можете получить список доступных часовых поясов (и выбрать тот, который лучше всего подходит вашей системе), вызвав ZoneId.getAvailableZoneIds().

Если вы хотите использовать определенный часовой пояс, просто используйте ZoneId.of("America/New_York") (или любое другое действительное имя, возвращаемое ZoneId.getAvailableZoneIds(), просто пример из Нью-Йорка) вместо ZoneId.systemDefault().


Другой альтернативой является использование parseBest() method , который пытается создать подходящий объект даты (используя список TemporalQuery) до тех пор, пока не создаст нужный тип:

public ZonedDateTime parseToZonedDateTime(String date, String dateFormat) {
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat);

    // try to create a ZonedDateTime, if it fails, try LocalDateTime
    TemporalAccessor parsed = formatter.parseBest(date, ZonedDateTime::from, LocalDateTime::from);

    // if it's a ZonedDateTime, return it
    if (parsed instanceof ZonedDateTime) {
        return (ZonedDateTime) parsed;
    }
    if (parsed instanceof LocalDateTime) {
        // convert LocalDateTime to JVM default timezone
        LocalDateTime dt = (LocalDateTime) parsed;
        return dt.atZone(ZoneId.systemDefault());
    }

    // if it can't be parsed, return null or throw exception?
    return null;
}

В этом случае я просто использовал ZonedDateTime::from и LocalDateTime::from, поэтому средство форматирования сначала попытается создать ZonedDateTime, а если это невозможно, то попытается создать LocalDateTime.

Затем я проверяю, какой тип был возвращен, и выполняю соответствующие действия .. Вы можете добавить столько типов, сколько хотите (все основные типы, такие как LocalDate, LocalTime, OffsetDateTime и т.д., Имеют метод from, который работает с parseBest - Вы также можете создать свой собственный TemporalQuery , если хотите, но я думаю, что встроенных методов для этого достаточно).


Летнее время

При преобразовании LocalDateTime в ZonedDateTime с использованием метода atZone() есть несколько хитрых случаев, связанных с Летнее время (летнее время).

В качестве примера я собираюсь использовать часовой пояс, в котором я живу (America/Sao_Paulo), но это может произойти в любом часовом поясе с DST.

В Сан-Паулу летнее время началось 16 октябряго 2016: в полночь часы сместились на 1 час вперед с полуночи до 1 часа ночи (и смещение меняется с -03:00 на -02:00). Таким образом, все локальные времена между 00:00 и 00:59 не существовали в этом часовом поясе (вы также можете думать, что часы изменились с 23: 59: 59.999999999 непосредственно на 01:00). Если я создаю локальную дату в этом интервале, она корректируется до следующего действительного момента:

ZoneId zone = ZoneId.of("America/Sao_Paulo");

// October 16th 2016 at midnight, DST started in Sao Paulo
LocalDateTime d = LocalDateTime.of(2016, 10, 16, 0, 0, 0, 0);
ZonedDateTime z = d.atZone(zone);
System.out.println(z);// adjusted to 2017-10-15T01:00-02:00[America/Sao_Paulo]

Когда заканчивается летнее время: 19 февраляго 2017 в полночь, часы смещены назад на 1 час, с полуночи до 23 PM из 18го (и смещение изменяется с -02:00 на -03:00). Таким образом, все локальные времена с 23:00 до 23:59 существовали дважды (в обоих смещениях: -03:00 и -02:00), и вы должны решить, какой из них вы хотите . По умолчанию, он использует смещение до окончания DST , но вы можете использовать метод withLaterOffsetAtOverlap(), чтобы получить смещение после окончания летнего времени:

// February 19th 2017 at midnight, DST ends in Sao Paulo
// local times from 23:00 to 23:59 at 18th exist twice
LocalDateTime d = LocalDateTime.of(2017, 2, 18, 23, 0, 0, 0);
// by default, it gets the offset before DST ends
ZonedDateTime beforeDST = d.atZone(zone);
System.out.println(beforeDST); // before DST end: 2018-02-17T23:00-02:00[America/Sao_Paulo]

// get the offset after DST ends
ZonedDateTime afterDST = beforeDST.withLaterOffsetAtOverlap();
System.out.println(afterDST); // after DST end: 2018-02-17T23:00-03:00[America/Sao_Paulo]

Обратите внимание, что даты до и после окончания летнего времени имеют разные смещения (-02:00 и -03:00). Если вы работаете с часовым поясом, в котором есть летнее время, имейте в виду, что такие случаи могут возникнуть.

9
user7605325

В библиотеке Java.time почти нет настроек по умолчанию, что, в основном, хорошо - то, что вы видите, это то, что вы получаете, точка.

Я бы посоветовал, что если ваша строка даты не содержит Zone - это LocalDateTime и не может быть ZonedDateTime, что означает исключение, которое вы получаете (даже если словосочетание пострадало из-за слишком гибкой структуры кода).

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

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

TemporalAccessor parsed = f.parse(string);
if (parsed.query(TemporalQueries.zone()) == null) {
  parsed = f.withZone(ZoneId.systemDefault()).parse(string);
}
return ZonedDateTime.from(parsed);

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

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

class TemporalWithZone implements TemporalAccessor {
  private final ZoneId zone;
  private final TemporalAccessor delegate;
  public TemporalWithZone(TemporalAccessor delegate, ZoneId zone) {
    this.delegate = requireNonNull(delegate);
    this.zone = requireNonNull(zone);
  }

  <delegate methods: isSupported(TemporalField), range(TemporalField), getLong(TemporalField)>

  public <R> R query(TemporalQuery<R> query) {
    if (query == TemporalQueries.zone() || query == TemporalQueries.zoneId()) {
      return (R) zone;
    }
    return delegate.query(query);
  }
}
3
M. Prokhorov

В соответствии с реализацией Java 8 ZonedDateTime, Вы не можете анализировать дату без зоны в ZonedDateTime. 

Чтобы обслужить данную проблему, вы должны поставить try catch на случай, если любое исключение будет считать часовой пояс по умолчанию. 

Пожалуйста, найдите пересмотренную программу, как показано ниже:

public class DateUtil {
     /** Convert a given String to ZonedDateTime. Use default Zone in string does not have zone.  */
    public ZonedDateTime parseToZonedDateTime(String date, String dateFormat) {
        //use Java.time from Java 8
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat);
        ZonedDateTime zonedDateTime = null;
        try {
            zonedDateTime = ZonedDateTime.parse(date, formatter);
        } catch (DateTimeException e) {
            // If date doesn't contains Zone then parse with LocalDateTime 
            LocalDateTime localDateTime = LocalDateTime.parse(date, formatter);
            zonedDateTime = localDateTime.atZone(ZoneId.systemDefault());
        }
        return zonedDateTime;
    }

    public static void main(String args[]) {
        DateUtil dateUtil = new DateUtil();
        System.out.println(dateUtil.parseToZonedDateTime("2017-09-14 15:00:00+0530", "yyyy-MM-dd HH:mm:ssZ"));
        System.out.println(dateUtil.parseToZonedDateTime("2017-09-14 15:00:00", "yyyy-MM-dd HH:mm:ss"));
    }
}

Вы можете обратиться к http://www.codenuclear.com/Java-8-date-time-intro для получения более подробной информации о будущих функциях Java

3
viviek

Вы можете просто добавить значение по умолчанию в DateTimeFormatterBuilder, если нет OFFSET_SECOND:


Правка: Чтобы получить системную ZoneOffset по умолчанию, вы должны применить ZoneRules к текущей Instant. Результат выглядит так:

class DateUtil {
  public ZonedDateTime parseToZonedDateTime(String date, String dateFormat) {
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat);
    LocalDateTime localDateTime = LocalDateTime.parse(date, formatter);
    ZoneOffset defaultOffset =  ZoneId.systemDefault().getRules().getOffset(localDateTime);
    DateTimeFormatter dateTimeFormatter = new DateTimeFormatterBuilder()
            .append(formatter)
            .parseDefaulting(ChronoField.OFFSET_SECONDS, defaultOffset.getTotalSeconds())
            .toFormatter();
    return ZonedDateTime.parse(date, dateTimeFormatter);
  }
}

Результат:

2017-09-14T15:00+05:30
2017-09-14T15:00+02:00
2
Flown

ZoneId можно указать с помощью метода withZone в DateTimeFormatter:

DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat).withZone("+0530");
1
RSloeserwij