Noda Time — создайте ZonedDateTime из DateTime и TimeZoneId

Допустим, у меня есть следующие дата, время и часовой пояс: 2016-10-15, 1:00:00, America/Toronto.

Как создать ZonedDateTime, представляющий точную дату и время в указанной зоне?

В основном мне нужен объект ZonedDateTime, который представляет точную дату и время в точном часовом поясе.

В случае, если время пропущено, я хотел бы добавить тик часа к новому времени. Пример:

Если 00:00 пропущено до 1:00, и я пытаюсь получить время 00:30 в зоне, я хочу, чтобы результат был 1:30, а не только 1:00, который является первым временем интервала.

Если 00:00 пропущено до 1:45, и я пытаюсь получить время 00:20 в зоне, я хочу, чтобы результат был 2:05.

Если время неоднозначно, т.е. е., встречается дважды, я хочу более раннее отображение.


person victor    schedule 21.10.2016    source источник
comment
может быть, это вас заинтересует. зона"> stackoverflow.com/questions/21945436/   -  person s7vr    schedule 22.10.2016
comment
@Veeram большое спасибо за это! Именно то, что мне было нужно.   -  person victor    schedule 22.10.2016
comment
Что вы хотите, чтобы произошло, если эта дата/время не существует или встречается дважды? Возможно, я мог бы вывести это из кода в вашем ответе, но вместо этого требования должны быть в вашем вопросе.   -  person Jon Skeet    schedule 22.10.2016
comment
@JonSkeet Дело не в том, что я не упомянул о них, а в том, что я вообще не осознавал, что эта проблема существует, потому что я не принимал во внимание изменения tz. Только после того, как я понял разницу между тремя методами InZone, стало ясно, что определенное время может не существовать в tz, потому что оно было пропущено или могло встречаться дважды.   -  person victor    schedule 22.10.2016
comment
Верно, но как только вы поймете, что проблема существует, вы должны указать свои обновленные требования в вопросе. В настоящее время вы говорите, что вам нужен ZonedDateTime, который представляет точную дату и время в часовом поясе, но ваш ответ этого не дает. (Если желаемое время — 1:30, но оно пропущено, вы указываете 2:30.)   -  person Jon Skeet    schedule 22.10.2016


Ответы (2)


То, что вы описали, и есть поведение LocalDateTime.InZoneLeniently в Noda Time 2.0. (Благодаря изменению Мэтта Джонсона :) Однако, поскольку это все еще находится в альфа-версии, вот решение для 1.3.2. По сути, вам просто нужен соответствующий ZoneLocalMappingResolver, который можно создать с помощью Resolvers. . Вот полный пример.

using NodaTime.TimeZones;
using NodaTime.Text;

class Program
{
    static void Main(string[] args)
    {
        // Paris went forward from UTC+1 to UTC+2
        // at 2am local time on March 29th 2015, and back
        // from UTC+2 to UTC+1 at 3am local time on October 25th 2015.
        var zone = DateTimeZoneProviders.Tzdb["Europe/Paris"];

        ResolveLocal(new LocalDateTime(2015, 3, 29, 2, 30, 0), zone);
        ResolveLocal(new LocalDateTime(2015, 6, 19, 2, 30, 0), zone);
        ResolveLocal(new LocalDateTime(2015, 10, 25, 2, 30, 0), zone);
    }

    static void ResolveLocal(LocalDateTime input, DateTimeZone zone)
    {
        // This can be cached in a static field; it's thread-safe.
        var resolver = Resolvers.CreateMappingResolver(
            Resolvers.ReturnEarlier, ShiftForward);

        var result = input.InZone(zone, resolver);
        Console.WriteLine("{0} => {1}", input, result);
    }

    static ZonedDateTime ShiftForward(
        LocalDateTime local,
        DateTimeZone zone,
        ZoneInterval intervalBefore,
        ZoneInterval intervalAfter)
    {
        var instant = new OffsetDateTime(local, intervalBefore.WallOffset)
            .WithOffset(intervalAfter.WallOffset)
            .ToInstant();
        return new ZonedDateTime(instant, zone);
    }            
}

Выход:

29/03/2015 02:30:00 => 2015-03-29T03:30:00 Europe/Paris (+02)
19/06/2015 02:30:00 => 2015-06-19T02:30:00 Europe/Paris (+02)
25/10/2015 02:30:00 => 2015-10-25T02:30:00 Europe/Paris (+02)
person Jon Skeet    schedule 24.10.2016
comment
Действительно лучше, спасибо! Только один вопрос: можно ли узнать, было ли пропущено время с помощью этого решения? Я использовал SkipedTimeException для установки флага. Кажется, теперь мне придется сделать что-то вроде (ввод + смещение результата) != результат -> пропущено, верно? - person victor; 25.10.2016
comment
@victor: Ну, вы могли бы использовать if (result.LocalDateTime != input) - в основном это говорит о том, что если мне пришлось что-то настраивать, это, должно быть, было пропущено ... - person Jon Skeet; 25.10.2016
comment
Можно ли с уверенностью сказать, что intervalAfter всегда будет текущим смещением для локального? - person victor; 25.10.2016
comment
@victor: Ну, intervalAfter - это ZoneInterval, а не Offset, но да, если он был сдвинут, то в соответствии с тем, как построен момент, он должен иметь это смещение. - person Jon Skeet; 25.10.2016

Изменить
В предыдущем решении были проблемы, например неверные даты и время во время перехода на летнее время и т. д.

Вот новое решение, которое объясняет все, с объяснением. Благодаря @Veeram.

// Transform the "time" in a localized time.                
var tzLocalTime = LocalDateTime.FromDateTime(time);

try
{
    // To get the exact same time in the specified zone.
    zoned = tzLocalTime.InZoneStrictly(zone);
}
catch(SkippedTimeException)
{
    // This happens if the time is skipped
    // because of daylight saving time.
    //
    // Example:
    // If DST starts at Oct 16 00:00:00,
    // then the clock is advanced by 1 hour
    // which means Oct 16 00:00:00 is *skipped*
    // to Oct 16 01:00:00.
    // In this case, it is not possible to convert
    // to exact same date, and SkippedTImeException
    // is thrown.

    // InZoneLeniently will convert the time
    // to the start of the zone interval after
    // the skipped date.
    // For the example above, this would return Oct 16 01:00:00.

     // If someone schedules an appointment at a time that
     // will not occur, than it is ok to adjust it to what
     // will really happen in the real world.

     var originalTime = ste.LocalDateTime;

     // Correct for the minutes, seconds, and milliseconds.
     // This is needed because if someone schedueld an appointment
     // as 00:30:00 when 00:00:00 is skipped, we expect the minute information
     // to be as expected: 01:30:00, instead of 01:00:00.
     var minuteSecondMillisecond = Duration.FromMinutes(originalTime.Minute) + Duration.FromSeconds(originalTime.Second) + Duration.FromMilliseconds(originalTime.Millisecond);

     zoned = zLocalTime.InZoneLeniently(zone).Plus(minuteSecondMillisecond);
}
catch(AmbiguousTimeException ate)
{
    // This happens when the time is ambiguous.
    // During daylight saving time, for example,
    // an hour might happen twice.
    //
    // Example:
    // If DST ends on Feb 19 00:00:00, then
    // Feb 18 23:00:00 will happen twice:
    // once during DST, and once when DST ends
    // and the clock is set back.
    // In such case, we assume the earlier mapping.
    // We could work with the second time that time
    // occur with ate.LaterMapping.

    zoned = ate.EarlierMapping;
}
person victor    schedule 21.10.2016
comment
Не используйте InZoneStrictly, если вам не нужна строгая семантика. Ваш второй фрагмент кода все еще не работает - нет гарантии, что он окажется с нужной вам локальной датой/временем. По сути, если ни InZoneLeniently, ни InZoneStrictly не делают того, что вам нужно, вы должны вызвать InZone и передать соответствующий ZoneLocalMappingResolver. - person Jon Skeet; 22.10.2016
comment
Я знаю, что он сломан, я заявил об этом в начале ответа. Я просто не удалил код, чтобы сохранить историю. Дело в том, что я хочу строгой семантики, если это возможно. Я хотел иметь возможность фиксировать исключения и явно обрабатывать их. - person victor; 22.10.2016
comment
Как видите, я сохраняю время суток на случай пропуска часа, в то время как InZoneLeniently проигнорирует его и вернет первый час интервала. Думаете, это плохой подход? Обработка исключения была единственным способом заставить его работать таким образом. - person victor; 22.10.2016
comment
Это, конечно, не единственный способ заставить его работать - вы можете использовать ZoneLocalMappingResolver... и в этом весь смысл. И вы на самом деле не сохраняете время дня — вы добавляете время дня как продолжительность к началу дня, что совсем не одно и то же. (По сути, вы в конечном итоге продвинетесь на количество пропущенного времени, поэтому 1:30 утра станет 2:30 ночи. Это то, что вы хотите сделать? Опять же, ваши требования должны быть в вопросе...) - person Jon Skeet; 22.10.2016
comment
Другой альтернативой является использование DateTimeZone.MapLocal btw. Посмотрите документацию по API для этого. - person Jon Skeet; 22.10.2016
comment
Тэ обязательно посмотрю. Чтобы ответить на ваш предыдущий вопрос: да, это то, что я хочу. Если 00:00 пропущено до 1:00, и я пытаюсь ввести 00:30, я ожидаю, что результат будет 1:30, а не 1:00. То же самое, если бы время было пропущено до 1:15: я бы ожидал 1:45, потому что, когда придет время, мои часы в реальной жизни будут скорректированы, поэтому они будут последовательными. Конечно, это требует тонкой настройки и нескольких предупреждений, но это общая идея. Я переформулирую свой вопрос, чтобы он соответствовал моему ответу - person victor; 24.10.2016
comment
Правильно - я сам добавлю ответ позже сегодня :) - person Jon Skeet; 24.10.2016