0

I'm attempting to build a logical set of time zone records representing a 5-year period where the stop and start date/time matches to what a person perceives as the time change moment daylight savings is observed. For example, in America/New_York id, I want:

03/13/2016 2:00 am    -   11/06/2016  01:59:59 am  gmtOffset:  -4:00
11/06/2016 2:00 am    -   03/12/2017  01:59:59 am  gmtOffset:  -5:00

I am able to build these records, but I see no other way than to start prior to 1912 when dst went in effect and track the End Instant for all zone intervals. I don't see a way to determine the offset in effect when an Interval record period begins other than the previous Interval record. Is there a way to do this using just a single ZoneInterval record?

Question noted in code:

    private void processTZ()
    {
        IDateTimeZoneProvider provider = DateTimeZoneProviders.Tzdb;

        using (StreamWriter sw = new StreamWriter(@"c:\tzOut.txt"))
        {
            foreach (string id in provider.Ids)
            {
                sw.WriteLine("Id: {0}", id);
                DateTimeZone zone = provider[id];

                // Time period I wish to create logical representation.
                Instant instantValidityBegin = (new LocalDateTime(2014, 1, 1, 0, 0)).InZoneLeniently(zone).ToInstant();
                Instant instantValidityEnd = (new LocalDateTime(2019, 12, 31, 23, 59, 59, 0)).InZoneLeniently(zone).ToInstant();

                // Time period from zone intervals to iterate. The first year dst was observed was 1916.
                Instant instantFetchYearBegin = (new LocalDateTime(1916, 1, 1, 0, 0)).InZoneLeniently(zone).ToInstant();
                Instant instantFetchYearEnd = (new LocalDateTime(2019, 12, 31, 23, 59, 0)).InZoneLeniently(zone).ToInstant();

                // Get the intervals
                IEnumerable<NodaTime.TimeZones.ZoneInterval> intervals = zone.GetZoneIntervals(instantFetchYearBegin, instantFetchYearEnd);

                // Determine number of intervals for this tzId.
                int count = 0;
                foreach (NodaTime.TimeZones.ZoneInterval zi in intervals)
                    count++;

                bool singleEntry = (1 == count);

                // myStart and myEnd are desired output period
                // capture IsoLocalEnd of an interval to be used as the start date of program output (myStart).  Don't display older than 1900.
                DateTime myStart, myEnd, prevEnd = new DateTime(1900, 1, 1);

                foreach (NodaTime.TimeZones.ZoneInterval zi in intervals)
                {
                    if (singleEntry)
                    {
                        // No question here
                    }
                    else
                    {
                        // skip intervals for this tzId that represent a period beginning after time period I want.
                        if (instantValidityEnd < zi.Start)
                            break;

                        // Skip intervals ending prior to time period I want, but capture the IsoLocalEnd to be used for report output.
                        if (zi.End < instantValidityBegin)
                        {
                            prevEnd = (zi.End == Instant.MinValue) ? prevEnd = new DateTime(1900, 1, 1) : prevEnd = zi.IsoLocalEnd.ToDateTimeUnspecified(); ;
                            continue;
                        }

                        //   ***Question***  Can this myStart value be determined using the current interval record instead of storing the previous interval's IsoLocalEnd??
                        myStart = prevEnd;
                        if (zi.End == Instant.MaxValue)
                            prevEnd = myEnd = new DateTime(9999, 12, 31);
                        else
                        {
                            prevEnd = zi.IsoLocalEnd.ToDateTimeUnspecified();
                            myEnd = prevEnd.Subtract(TimeSpan.FromSeconds(1));  // force period back 1 second for logical representation.
                        }


                        sw.WriteLine("Name: " + zi.Name);
                        sw.WriteLine("Multi Entry: {0}", zi.ToString());
                        sw.WriteLine("myStart: {0:G}   myEnd: {1:G}    gmtOffset: {2:G}", myStart, myEnd, zi.WallOffset.ToTimeSpan());
                        sw.WriteLine();
                    }
                }

                sw.WriteLine("------------------------------------------------------------------");
                sw.WriteLine();
            }
        }
    }
J Reynolds
  • 33
  • 1
  • 5
  • To be more precise, the start time of an interval is "off" by the dst amount of the previous record; however, the previous end record stores the time needed for this type of logical representation. – J Reynolds May 20 '16 at 13:00
  • 1
    [The docs for `ZoneInterval`](http://nodatime.org/1.3.x/api/?topic=html/Properties_T_NodaTime_TimeZones_ZoneInterval.htm) has a comment about `IsoLocalEnd` not including the saving. However, it's unclear if you're using this, or if you're using the `Start` and `End` instants and converting them. Could you please edit your question to show the code you currently have? We can probably help you easier if you do. See also [How to create a Minimal, Complete, and Verifiable example](http://stackoverflow.com/help/mcve) and [How do I ask a good question?](http://stackoverflow.com/help/how-to-ask) – Matt Johnson-Pint May 21 '16 at 04:25
  • "I don't see a way to determine the offset in effect on a ZoneInterval record" - That's what `WallOffset` is for. Beyond that, I'm having trouble understanding your question I'm afraid. – Jon Skeet May 21 '16 at 12:35
  • @MattJohnson: That "This does not include any daylight saving" comment is a really weird one. Will look into that... – Jon Skeet May 21 '16 at 12:36
  • My apologies Matt and Jon for asking such a confusing question without originally including code example. Thanks for your help. – J Reynolds May 21 '16 at 16:59
  • 1
    I still don't really understand what your code is trying to do - or why you're asking for one set of zone intervals but then filtering to a different range. I've answered the part about determining the offset in a `ZoneInterval` - and I guess you probably want `zi.IsoLocalStart` for `myStart`. Finally, it's not clear why you're using `DateTime` at all - there's less room for error if you just stick to the Noda Time types. – Jon Skeet May 22 '16 at 20:01

1 Answers1

1

On any given ZoneInterval record, the IsoLocalStart and IsoLocalEnd are relative to that particular interval, using the same WallOffset value for both.

If you want to get the actual local time in effect you can take the Start and End instants and convert them to the specific time zone.

zi.Start.InZone(zone)
zi.End.InZone(zone)

You'll find that the LocalDateTime of the starting value matches IsoLocalStart, but the ending value does not. By the time that instant is observed, it is now in covered by the offset of the next interval. Don't forget that these are half-open intervals. [inclusive-start, exclusive-end).

Let's go back to the example in your original request:

03/13/2016 2:00 am    -   11/06/2016  01:59:59 am  gmtOffset:  -4:00
11/06/2016 2:00 am    -   03/12/2017  01:59:59 am  gmtOffset:  -5:00

It doesn't actually work like that. In March, the local time moves from 1:59:59 am to 3:00:00 am, skipping over an hour. So the first row should start at 3:00 am rather than 2:00 am. In November, the local time moves from 01:59:59 am to 1:00:00 am repeating an hour. So the second row should start at 1:00 am rather than 2:00 am. Your chart seems to be attempting to make local time continuous - which it is not.

Additionally, you'll find that by convention many people think about the time of the transition as being 2:00, rather that 1:59:59. Think of it as approaching 2:00. This is why IsoLocalEnd is presented with the offset of the current interval, and you could use that field for such a purpose.

Also, you may want to check out how timeanddate.com shows it. They've decided to only show the ending values, which may make things a bit clearer.

Matt Johnson-Pint
  • 230,703
  • 74
  • 448
  • 575
  • Yes, continuous local time is needed. My company accepts world wide travel schedules and builds customized trips handling connection points for periods greater than a year. Output is in terabytes and exponential to the number of input records. A single input record may represent several years of a flight/train/ship schedule offering departing/arriving at a "constant time". For accurate output, the pgm must analyze the constant time and interpolate (grrrrrrr). Thanks for you thorough answer! – J Reynolds May 24 '16 at 14:05
  • Continuous local time is not reality. See the charts in the [dst tag wiki](http://stackoverflow.com/tags/dst/info). This is why Noda Time has `InZoneLeniently` and related methods. However, you'll probably find in practice that travel departure times are usually not scheduled near DST transitions. Arrival times might be though. Use departure time + duration to validate the arrival time. – Matt Johnson-Pint May 24 '16 at 16:47