2

Let's say I have the following date, time, and time zone: 2016-10-15, 1:00:00, America/Toronto.

How do I create a ZonedDateTime that represents that exact date and time in the specified zone?

Basically what I need a ZonedDateTime object that represents the exact date and time in the exact time zone.

In case the time is skipped, I would like to add the tick of hour to the new time. Example:

If 00:00 is skipped to 1:00, and I attempt to get the time 00:30 in the zone, I want the result to be 1:30, not only 1:00, which is the first time of the interval.

If 00:00 is skipped to 1:45, and I attempt to get the time 00:20 in the zone, I want the result ot be 2:05.

If a time is ambiguous, i. e., occurs twice, I want the earlir mapping.

victor
  • 1,532
  • 1
  • 13
  • 32
  • may be this is of interest to you.. http://stackoverflow.com/questions/21945436/convert-same-time-to-different-time-zone – s7vr Oct 21 '16 at 22:24
  • @Veeram thank you very much for that! Exactly what I needed. – victor Oct 22 '16 at 00:39
  • What do you want to happen if that date/time doesn't exist, or occurs twice? I could possibly infer it from the code in your answer, but the requirements should be in your *question* instead. – Jon Skeet Oct 22 '16 at 07:23
  • @JonSkeet It's not that I didn't mention them, it's that I hand't really realized this problem existed at all because I wasn't taking tz changes into account. Only after I understood the difference between the 3 InZone methods it did come to light that a particular time might not exist in a tz because it was skipped, or might occur twice. – victor Oct 22 '16 at 18:30
  • Right, but once you realise that the problem exists, you should put your updated requirements in the question. Currently you're saying you want a `ZonedDateTime` that represents the exact date and time in the time zone - but your *answer* doesn't give that. (If the "desired" time is 1:30am but that's skipped, you're giving 2:30am.) – Jon Skeet Oct 22 '16 at 19:20

2 Answers2

6

What you've described is precisely the behaviour of LocalDateTime.InZoneLeniently in Noda Time 2.0. (Thanks to Matt Johnson's change :) However, as that's still in alpha, here's a solution for 1.3.2. Basically, you just want an appropriate ZoneLocalMappingResolver, which you can build using Resolvers. Here's a complete example.

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);
    }            
}

Output:

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)
Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • Much better indeed, thank you! Just one question: is it possible to know if a time was skipped using this solution? I was using the SkippedTimeException to set a flag. It seems now I would have to do something like (input + result offset) != result -> skipped, right? – victor Oct 25 '16 at 14:13
  • @victor: Well you could use `if (result.LocalDateTime != input)` - basically that says "If I've had to adjust anything, it must have been skipped..." – Jon Skeet Oct 25 '16 at 14:14
  • Is it safe to say that intervalAfter will always be the current offset for the "local"? – victor Oct 25 '16 at 14:54
  • @victor: Well `intervalAfter` is a `ZoneInterval`, not an `Offset` - but yes, if it's been shifted, then as per how the instant is constructed, it should have that offset. – Jon Skeet Oct 25 '16 at 14:55
1

Edit
There were problems with the previous solution, suchs as invalid datetimes during DST, etc.

Here's the new solution that accounts for everything, with explanation. Thanks to @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;
}
victor
  • 1,532
  • 1
  • 13
  • 32
  • Don't use `InZoneStrictly` when you don't *want* strict semantics. Your second piece of code is still broken - there's no guarantee it will end up with the local date/time you want. Basically, if neither `InZoneLeniently` nor `InZoneStrictly` does what you want, you should call `InZone` and pass in an appropriate `ZoneLocalMappingResolver`. – Jon Skeet Oct 22 '16 at 07:23
  • I know it is broken, I stated that in at the beginning of the answer. I just didn't remove the code to keep a history. The thing is I want strict semantics, if it's possible. I wanted to be able to capture the exceptions and deal with them explicitly. – victor Oct 22 '16 at 18:35
  • You can see that I keep the time of day in case the hour is skipped, while InZoneLeniently would ignore it and return the first hour of the interval. You think that's a bad approach? Handling the exception was the only way I could make it work this way – victor Oct 22 '16 at 18:49
  • That's certainly not the only way you can make it work - you can use a `ZoneLocalMappingResolver`... and that's the whole point of that. And you're not really keeping the time of day - you're adding the time of day as a duration to the start of the day, which isn't the same thing at all. (Basically, you'll end up advancing by the amount of time which is skipped, so 1:30am will become 2:30am. Is that what you want to do? Again, your requirements should be in the question...) – Jon Skeet Oct 22 '16 at 19:20
  • Another alternative is to use `DateTimeZone.MapLocal` btw - have a look at the [API documentation](http://nodatime.org/1.3.x/api/html/M_NodaTime_DateTimeZone_MapLocal.htm) for that. – Jon Skeet Oct 22 '16 at 19:36
  • I will definitely take a look at ti. To answer your previous question: yes, that's what I want. If 00:00 is skipped to 1:00, and I try to enter 00:30, I expect the result to be 1:30, not 1:00. Same if time was skipped to 1:15: I would expect 1:45, because when the time comes, my clock in real life will be ajusted, so it will be consistent. Of course this needs fine tunning and a few warning, but that's the general idea. I will reformulate my question to match my answer – victor Oct 24 '16 at 17:05
  • Right - I'll add an answer myself later on today :) – Jon Skeet Oct 24 '16 at 17:22