Getting Daylight Savings Time Start and End in NodaTime - c#

How can I get the starting and ending dates for Daylight Savings Time using Noda Time?
The function below accomplishes this task but it is horribly unwieldy and is begging for a simpler solution.
/// <summary>
/// Gets the start and end of daylight savings time in a given time zone
/// </summary>
/// <param name="tz">The time zone in question</param>
/// <returns>A tuple indicating the start and end of DST</returns>
/// <remarks>Assumes this zone has daylight savings time</remarks>
private Tuple<LocalDateTime, LocalDateTime> GetZoneStartAndEnd(DateTimeZone tz)
{
int thisYear = TimeUtils.SystemLocalDateTime.Year; // Get the year of the current LocalDateTime
// Get January 1, midnight, of this year and next year.
var yearStart = new LocalDateTime(thisYear, 1, 1, 0, 0).InZoneLeniently(tz).ToInstant();
var yearEnd = new LocalDateTime(thisYear + 1, 1, 1, 0, 0).InZoneLeniently(tz).ToInstant();
// Get the intervals that we experience in this year
var intervals = tz.GetZoneIntervals(yearStart, yearEnd).ToArray();
// Assuming we are in a US-like daylight savings scheme,
// we should see three intervals:
// 1. The interval that January 1st sits in
// 2. At some point, daylight savings will start.
// 3. At some point, daylight savings will stop.
if (intervals.Length == 1)
throw new Exception("This time zone does not use daylight savings time");
if (intervals.Length != 3)
throw new Exception("The daylight savings scheme in this time zone is unexpected.");
return new Tuple<LocalDateTime,LocalDateTime>(intervals[1].IsoLocalStart, intervals[1].IsoLocalEnd);
}

There's not a single built-in function that I am aware of, but the data is all there, so you can certainly create your own.
You're on the right track with what you've shown, but there are a few things to consider:
Normally people are interested in the end points of the intervals. By returning the start and stop of only the middle interval, you are likely getting values different than you expect. For example, if you use one of the US time zones, such as "America/Los_Angeles", your function returns the transitions as 3/9/2014 3:00:00 AM and 11/2/2014 2:00:00 AM, where you are probably expecting 2:00 AM for both.
Time zones south of the equator that use DST will start it towards the end of the year, and end it towards the beginning of the next year. So sometimes the items in the tuple might be reversed from what you expect them to be.
There are quite a lot of time zones that don't use daylight saving time, so throwing an exception isn't the best idea.
There are at least two time zones that presently have four transitions in a single year ("Africa/Casablanca" and "Africa/Cairo") - having a "break" in their DST periods for Ramadan. And occasionally, there are non-DST-related transitions, such as when Samoa changed its standard offset in 2011, which gave it three transitions in a single year.
Taking all of this into account, it would seem better to return a list of single transition points, rather than a tuple of pairs of transitions.
Also, this is minor, but it would be better form to not bind the method to the system clock at all. The year can easily be passed by parameter. Then you can use this method for non-current years if necessary.
public IEnumerable<LocalDateTime> GetDaylightSavingTransitions(DateTimeZone timeZone, int year)
{
var yearStart = new LocalDateTime(year, 1, 1, 0, 0).InZoneLeniently(timeZone).ToInstant();
var yearEnd = new LocalDateTime(year + 1, 1, 1, 0, 0).InZoneLeniently(timeZone).ToInstant();
var intervals = timeZone.GetZoneIntervals(yearStart, yearEnd);
return intervals.Select(x => x.IsoLocalEnd).Where(x => x.Year == year);
}
Also note at the end, it's important to filter just the values that are in the current year because the intervals may very well extend into the following year, or go on indefinitely.

This snippet code also help you to check a time is in daylightsavingstime or not
public static bool IsDaylightSavingsTime(this DateTimeOffset dateTimeOffset)
{
var timezone = "Europe/London"; //https://nodatime.org/TimeZones
ZonedDateTime timeInZone = dateTimeOffset.DateTime.InZone(timezone);
var instant = timeInZone.ToInstant();
var zoneInterval = timeInZone.Zone.GetZoneInterval(instant);
return zoneInterval.Savings != Offset.Zero;
}
how to use it
var testDate = DateTimeOffset.Now;
var isDst = testDate.IsDaylightSavingsTime();
Depend on your situation, you can modify it a bit

Related

NodaTime - Calculate duration between two LocalTime variables that span midnight

I have a requirement to calculate the duration between two times that may span midnight.
The use case is to allow the user to set up shift plans e.g. “09:00 to 17:00” or “22:00 to 06:00” and calculate the contracted time
For “09:00 to 17:00” I can use:
LocalTime inAt = new LocalTime(9, 0);
LocalTime outAt = new LocalTime(17, 0);
var period = Period.Between(inAt, outAt);
Which results in 8 hours, the answer I am looking for.
For “22:00 to 06:00” the period returns 16 hours (regardless of the order of the parameters).
LocalTime inAt = new LocalTime(22, 0);
LocalTime outAt = new LocalTime(6, 0);
var period = Period.Between(inAt, outAt);
var period2 = Period.Between(outAt, inAt);
I am guessing that this is related to daylight saving time, unless you know the dates you cannot be sure that the answer will always the 8 hours. If the clocks go forward it would be 7, backwards would be 9.
How can I ensure that no matter what LocalTime values are used the period would disregard any daylight savings? Should I use LocalDateTime with an arbitrary date such as 2021-01-01?
Also, am I correct in using Period or should I be using Duration?
Update
This seems to work however I am still wondering if there is an eaiser way of doing it?
LocalTime inAt = new LocalTime(22, 0);
LocalTime outAt = new LocalTime(6, 0);
var period = Period.Between(inAt, outAt, PeriodUnits.Ticks);
LocalTime? midnightAdjustedTime = null;
if (period.Ticks < 0)
{
midnightAdjustedTime = LocalTime.Midnight + period;
}
var duration = Duration.FromTicks(midnightAdjustedTime?.TickOfDay ?? period.Ticks);
This has nothing to do with daylight savings - it can't do, given that everything is in terms of LocalTime. It's about negative periods.
For “22:00 to 06:00” the period returns 16 hours (regardless of the order of the parameters)
No, it doesn't. One returns 16 hours, the other returns -16 hours:
using NodaTime;
var start = new LocalTime(22, 0);
var end = new LocalTime(6, 0);
var startToEnd = Period.Between(start, end);
var endToStart = Period.Between(end, start);
Console.WriteLine(startToEnd);
Console.WriteLine(endToStart);
Output:
PT-16H
PT16H
It's not clear to me how you came to the conclusion that both returned the same value, but the fact that they don't return the same value is crucial to fixing the problem. The simple approach is just to add 1 day (24 hours) if the period is negative. The code you've got is almost right, but it can be done much simpler - and without using the Ticks unit at all:
// Note: Period.ToDuration() isn't "generally safe" due to variable lengths
// of months etc, but is okay here.
var duration = startToEnd.ToDuration();
if (duration < Duration.Zero)
{
duration += Duration.FromDays(1);
}
Console.WriteLine(duration);

Schedule a hangfire job at specific time of the day based on time zone

In hangfire i can schedule a job to run at a specific time by calling a method with delay
BackgroundJob.Schedule(
() => Console.WriteLine("Hello, world"),
TimeSpan.FromDays(1));
I have a table with following information
User Time TimeZone
--------------------------------------------------------
User1 08:00:00 Central Standard Time
User1 13:00:00 Central Standard Time
User2 10:00:00 Eastern Standard Time
User2 17:00:00 Eastern Standard Time
User3 13:00:00 UTC
Given this information, For every user i want to send notice every day at configured time based on their time zone
ScheduleNotices method will run everyday at 12 AM UTC. This method will schedule jobs that needs to run that day.
public async Task ScheduleNotices()
{
var schedules = await _dbContext.GetSchedules().ToListAsync();
foreach(var schedule in schedules)
{
// Given schedule information how do i build enqueueAt that is timezone specific
var enqueuAt = ??;
BackgroundJob.Schedule<INotificationService>(x => x.Notify(schedule.User), enqueuAt );
}
}
Update 1
The Schedules table information keep changing. User has option to add/delete time. I can create a recurring job that runs every minuet (minute is minimum unit hangfire supports) and then this recurring job can query Schedules table and send notices based the time schedule.
However that too much database interaction.So instead i will have only one recurring job ScheduleNotices that will run at 12 AM (once in a day) and will schedule jobs for next 24 hours. In this case any changes they make will be effective from next day.
Your answer was pretty close. There were a few problems:
You were assuming that today in a given time zone was the same date as today in UTC. Depending on time zone, these could be different days. For example, 1 AM UTC on 2019-10-18, is 8:00 PM in US Central Time on 2019-10-17.
If you design around "has it happened yet today", you'll potentially skip over legitimate occurrences. Instead, it's much easier to just think about "what is the next future occurrence".
You weren't doing anything to handle invalid or ambiguous local times, such as occur with the start or end of DST and with changes in standard time. This is important for recurring events.
So on to the code:
// Get the current UTC time just once at the start
var utcNow = DateTimeOffset.UtcNow;
foreach (var schedule in schedules)
{
// schedule notification only if not already scheduled in the future
if (schedule.LastScheduledDateTime == null || schedule.LastScheduledDateTime.Value < utcNow)
{
// Get the time zone for this schedule
var tz = TimeZoneInfo.FindSystemTimeZoneById(schedule.User.TimeZone);
// Decide the next time to run within the given zone's local time
var nextDateTime = nowInZone.TimeOfDay <= schedule.PreferredTime
? nowInZone.Date.Add(schedule.PreferredTime)
: nowInZone.Date.AddDays(1).Add(schedule.PreferredTime);
// Get the point in time for the next scheduled future occurrence
var nextOccurrence = nextDateTime.ToDateTimeOffset(tz);
// Do the scheduling
BackgroundJob.Schedule<INotificationService>(x => x.Notify(schedule.CompanyUserID), nextOccurrence);
// Update the schedule
schedule.LastScheduledDateTime = nextOccurrence;
}
}
I think you'll find that your code and data are much clearer if you make your LastScheduledDateTime a DateTimeOffset? instead of a DateTime?. The above code assumes that. If you don't want to, then you can change that last line to:
schedule.LastScheduledDateTime = nextOccurrence.UtcDateTime;
Also note the use of ToDateTimeOffset, which is an extension method. Place it in a static class somewhere. Its purpose is to create a DateTimeOffset from a DateTime taking a specific time zone into account. It applies typical scheduling concerns when dealing with ambiguous and invalid local times. (I last posted about it in this other Stack Overflow answer if you want to read more.) Here is the implementation:
public static DateTimeOffset ToDateTimeOffset(this DateTime dt, TimeZoneInfo tz)
{
if (dt.Kind != DateTimeKind.Unspecified)
{
// Handle UTC or Local kinds (regular and hidden 4th kind)
DateTimeOffset dto = new DateTimeOffset(dt.ToUniversalTime(), TimeSpan.Zero);
return TimeZoneInfo.ConvertTime(dto, tz);
}
if (tz.IsAmbiguousTime(dt))
{
// Prefer the daylight offset, because it comes first sequentially (1:30 ET becomes 1:30 EDT)
TimeSpan[] offsets = tz.GetAmbiguousTimeOffsets(dt);
TimeSpan offset = offsets[0] > offsets[1] ? offsets[0] : offsets[1];
return new DateTimeOffset(dt, offset);
}
if (tz.IsInvalidTime(dt))
{
// Advance by the gap, and return with the daylight offset (2:30 ET becomes 3:30 EDT)
TimeSpan[] offsets = { tz.GetUtcOffset(dt.AddDays(-1)), tz.GetUtcOffset(dt.AddDays(1)) };
TimeSpan gap = offsets[1] - offsets[0];
return new DateTimeOffset(dt.Add(gap), offsets[1]);
}
// Simple case
return new DateTimeOffset(dt, tz.GetUtcOffset(dt));
}
(In your case, the kind is always unspecified, so you could remove that first check if you want to, but I prefer to keep it fully functional in case of other usage.)
Incidentally, you don't need the if (!schedules.HasAny()) { return; } check. Entity Framework already tests for changes during SaveChangesAsync, and does nothing if there aren't any.
I think i got it. I added one more column in my Schedules table as LastScheduledDateTime and then my code looks like
ScheduleNotices is Recurring job that will run Daily at 12.00 AM. This job will schedules others jobs that needs to run that day
public async Task ScheduleNotices()
{
var schedules = await _dbContext.Schedules
.Include(x => x.User)
.ToListAsync().ConfigureAwait(false);
if (!schedules.HasAny())
{
return;
}
foreach (var schedule in schedules)
{
var today = DateTime.UtcNow.Date;
// schedule notification only if not already scheduled for today
if (schedule.LastScheduledDateTime == null || schedule.LastScheduledDateTime.Value.Date < today)
{
//construct scheduled datetime for today
var scheduleDate = new DateTime(today.Year, today.Month, today.Day, schedule.PreferredTime.Hours, schedule.PreferredTime.Minutes, schedule.PreferredTime.Seconds, DateTimeKind.Unspecified);
// convert scheduled datetime to UTC
schedule.LastScheduledDateTime = TimeZoneInfo.ConvertTimeToUtc(scheduleDate, TimeZoneInfo.FindSystemTimeZoneById(schedule.User.TimeZone));
//*** i think we dont have to convert to DateTimeOffSet since LastScheduledDateTime is already in UTC
var dateTimeOffSet = new DateTimeOffset(schedule.LastScheduledDateTime.Value);
BackgroundJob.Schedule<INotificationService>(x => x.Notify(schedule.CompanyUserID), dateTimeOffSet);
}
}
await _dbContext.SaveChangesAsync();
}

C# - Handling ranges of prevailing times on DST transition days - The supplied DateTime represents an invalid time

A couple of premises:
By "prevailing time" I mean how it is handled locally (my industry uses this terminology). For example, Eastern Prevailing Time has a UTC offset of -05:00 except during DST when it is -04:00
I find it much cleaner to handle range data by treating the end value as exclusive, rather than the hackish inclusive approach (where you have to subtract an epsilon from the first value beyond the end of your range).
For example, the range of values from 0 (inclusive) to 1 (exclusive), as per interval notation, is [0, 1), which is much more readable than [0, 0.99999999999...] (and is less prone to rounding issues and thus off-by-one errors, because the epsilon value depends on the data type being used).
With these two ideas in mind, how can I represent the final hour time range on the spring DST transition day, when the ending timestamp is invalid (i.e. there is no 2am, it instantly becomes 3am)?
[2019-03-10 01:00, 2019-03-10 02:00) in your time zone of choice that supports DST.
Putting the end time as 03:00 is quite misleading, as it looks like a 2-hour wide time range.
When I run it through this C# sample code, it blows up:
DateTime hourEnd_tz = new DateTime(2019, 3, 10, 0, 0, 0, DateTimeKind.Unspecified);//midnight on the spring DST transition day
hourEnd_tz = hourEnd_tz.AddHours(2);//other code variably computes this offset from business logic
TimeZoneInfo EPT = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");//includes DST rules
DateTime hourEnd_utc = TimeZoneInfo.ConvertTime(//interpret the value from the user's time zone
hourEnd_tz,
EPT,
TimeZoneInfo.Utc);
System.ArgumentException: 'The supplied DateTime represents an invalid time. For example, when the clock is adjusted forward, any time in the period that is skipped is invalid.
Parameter name: dateTime'
How might I handle this case (elsewhere I am already handling the autumn ambiguous times), without having to extensively refactor my time range class library?
Premise 1 is reasonable, though often the word "prevailing" is dropped and it's just called "Eastern Time" - either are fine.
Premise 2 is a best practice. Half-open ranges offer many benefits, such as not having to deal with date math involving an epsilon, or having to determine what precision the epsilon should have.
However, the range you're attempting to describe cannot be done with a date and time alone. It needs to also involve the offset from UTC. For US Eastern Time (using ISO 8601 format), it looks like this:
[2019-03-10T01:00:00-05:00, 2019-03-10T03:00:00-04:00) (spring-forward)
[2019-11-03T02:00:00-04:00, 2019-11-03T02:00:00-05:00) (fall-back)
You said:
Putting the end time as 03:00 is quite misleading, as it looks like a 2-hour wide time range.
Ah, but putting the spring end time as 02:00 would also be misleading, as that local time is not observed on that day. Only by combining the actual local date and time with the offset at that time can one be accurate.
You can use the DateTimeOffset structure in .NET to model these (or the OffsetDateTime structure in Noda Time).
How might I handle this case ... without having to extensively refactor my time range class library?
First, you'll need an extension method that lets you convert from DateTime to a DateTimeOffset for a specific time zone. You'll need this for two reasons:
The new DateTimeOffset(DateTime) constructor assumes that a DateTime with Kind of DateTimeKind.Unspecified should be treated as local time. There's no opportunity to specify a time zone.
The new DateTimeOffset(dt, TimeZoneInfo.GetUtcOffset(dt)) approach isn't good enough, because GetUtcOffset presumes you want the standard time offset in the case of ambiguity or invalidity. That is usually not the case, and thus you have to code the following yourself:
public static DateTimeOffset ToDateTimeOffset(this DateTime dt, TimeZoneInfo tz)
{
if (dt.Kind != DateTimeKind.Unspecified)
{
// Handle UTC or Local kinds (regular and hidden 4th kind)
DateTimeOffset dto = new DateTimeOffset(dt.ToUniversalTime(), TimeSpan.Zero);
return TimeZoneInfo.ConvertTime(dto, tz);
}
if (tz.IsAmbiguousTime(dt))
{
// Prefer the daylight offset, because it comes first sequentially (1:30 ET becomes 1:30 EDT)
TimeSpan[] offsets = tz.GetAmbiguousTimeOffsets(dt);
TimeSpan offset = offsets[0] > offsets[1] ? offsets[0] : offsets[1];
return new DateTimeOffset(dt, offset);
}
if (tz.IsInvalidTime(dt))
{
// Advance by the gap, and return with the daylight offset (2:30 ET becomes 3:30 EDT)
TimeSpan[] offsets = { tz.GetUtcOffset(dt.AddDays(-1)), tz.GetUtcOffset(dt.AddDays(1)) };
TimeSpan gap = offsets[1] - offsets[0];
return new DateTimeOffset(dt.Add(gap), offsets[1]);
}
// Simple case
return new DateTimeOffset(dt, tz.GetUtcOffset(dt));
}
Now that you have that defined (and put it in a static class somewhere in your project), you can call it where needed in your application.
For example:
TimeZoneInfo tz = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
DateTime dt = new DateTime(2019, 3, 10, 2, 0, 0, DateTimeKind.Unspecified);
DateTimeOffset dto = dt.ToDateTimeOffset(tz); // 2019-03-10T03:00:00-04:00
or
TimeZoneInfo tz = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
DateTime dt = new DateTime(2019, 11, 3, 1, 0, 0, DateTimeKind.Unspecified);
DateTimeOffset dto = dt.ToDateTimeOffset(tz); // 2019-11-03T01:00:00-04:00
or
TimeZoneInfo tz = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
DateTime dt = new DateTime(2019, 3, 10, 0, 0, 0, DateTimeKind.Unspecified);
DateTimeOffset midnight = dt.ToDateTimeOffset(tz); // 2019-03-10T00:00:00-05:00
DateTimeOffset oneOClock = midnight.AddHours(1); // 2019-03-10T01:00:00-05:00
DateTimeOffset twoOClock = oneOClock.AddHours(1); // 2019-03-10T02:00:00-05:00
DateTimeOffset threeOClock = TimeZoneInfo.ConvertTime(twoOClock, tz); // 2019-03-10T03:00:00-04:00
TimeSpan diff = threeOClock - oneOClock; // 1 hour
Note that subtracting two DateTimeOffset values correctly considers their offsets (whereas subtracting two DateTime values completely ignores their Kind).

Comparing two different timezone timespans using NodaTime

I have a requirement which I'm getting a little confused about. I started using NodaTime which I think is the best way to go.
I have two users, User1 and User2 both in two different timezones. They are available to meet between 2pm and 5pm for example, in their local timezones. If User2 has an offset of +2 hours from User1, then the overlap is just 1 hour. What I want to get the number of hours overlap (the actual time for User1 and User2 would be a bonus.)
All I have got so far is:
var user1TimeZone = DateTimeZoneProviders.Tzdb.GetZoneOrNull(user1timezone);
var user2TimeZone = DateTimeZoneProviders.Tzdb.GetZoneOrNull(user2timeZone);
Any thoughts on how I should even start tackling this problem?
Thanks,
Firstly, be aware that it could change each day: don't treat a time zone as a fixed offset.
Secondly, be aware that the local time specified (for each of start/end) may not even happen, or may happen twice. Work out how you want to handle ambiguous and skipped times.
For any particular day, I would just convert they users' start/end times to Instant (via ZonedDateTime) and then you can find the overlap. This does assume that any overlap happens on the same day, however... that isn't the case in reality. I'm having a meeting soon where one of the attendees is in New Zealand - it's March 14th here, but March 15th there. Accounting for that is rather trickier...
Here's code for the relatively simple case though:
using NodaTime;
using System;
class Test
{
static void Main()
{
// My availability: 4pm-7pm in London
var jon = new Availability(
DateTimeZoneProviders.Tzdb["Europe/London"],
new LocalTime(16, 0, 0),
new LocalTime(19, 0, 0));
// My friend Richard's availability: 12pm-4pm in New York
var richard = new Availability(
DateTimeZoneProviders.Tzdb["America/New_York"],
new LocalTime(12, 0, 0),
new LocalTime(16, 0, 0));
// Let's look through all of March 2017...
var startDate = new LocalDate(2017, 3, 1);
var endDate = new LocalDate(2017, 4, 1);
for (LocalDate date = startDate; date < endDate; date = date.PlusDays(1))
{
var overlap = GetAvailableOverlap(date, jon, richard);
Console.WriteLine($"{date:yyyy-MM-dd}: {overlap:HH:mm}");
}
}
static Duration GetAvailableOverlap(
LocalDate date,
Availability avail1,
Availability avail2)
{
// TODO: Check that the rules of InZoneLeniently are what you want.
// Be careful, as you could end up with an end before a start...
var start1 = (date + avail1.Start).InZoneLeniently(avail1.Zone);
var end1 = (date + avail1.End).InZoneLeniently(avail1.Zone);
var start2 = (date + avail2.Start).InZoneLeniently(avail2.Zone);
var end2 = (date + avail2.End).InZoneLeniently(avail2.Zone);
var latestStart = Instant.Max(start1.ToInstant(), start2.ToInstant());
var earliestEnd = Instant.Min(end1.ToInstant(), end2.ToInstant());
// Never return a negative duration... return zero of there's no overlap.
// Noda Time should have Duration.Max really...
var overlap = earliestEnd - latestStart;
return overlap < Duration.Zero ? Duration.Zero : overlap;
}
}
public sealed class Availability
{
public DateTimeZone Zone { get; }
public LocalTime Start { get; }
public LocalTime End { get; }
public Availability(DateTimeZone zone, LocalTime start, LocalTime end)
{
Zone = zone;
Start = start;
End = end;
}
}
If you have a server where you do that, you have to send UTC and then compare it. When you get the time on the client side you have to convert it into local. It means, that when first user wants to arrange a meeting, he sends his time into UTC to server, then when second user gets this time, he will convert it into his local time.
// First user sends UTC.
DateTime firstUserTime = DateTime.UtcNow;
// Second user gets time in his time zone.
DateTime secondUserTime = firstUserTime.ToLocalTime();

Noda Time - Create a ZonedDateTime from DateTime and TimeZoneId

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

Categories

Resources