Catch: Most solutions that I've found already take a DateTime object and convert it but I want to CREATE it IN A SPECIFIC timezone knowing the timezone.
So it is not DateTimeKind.Utc and it is not DateTimeKind.Local it would be in DateTime in timezone.
Basis data:
1. string From = "09:00" //local time because summertime/wintertime
2. string Till = "17:00" //local time because summertime/wintertime
3. string TimeZoneResolved = "Europe/Vienna"
Implicitly I have:
TimeZoneInfo timeZoneInfo = TZConvert.GetTimeZoneInfo(TimeZoneResolved);
TimeSpan workHoursStart = TimeSpan.Parse(From);
TimeSpan workHoursEnd = TimeSpan.Parse(Till);
What I want to achieve:
//reconstruct today 9am in that country of timeZoneInfo
var now = DateTime.UtcNow;
var startTime = new DateTime(now.Year, now.Month, now.Day, workHoursStart.Hour, workHoursStart.Minute, 0, 0, timeZoneInfo);
-> invalid because TimeZoneInfo parameter is invalid. Expects DateTimeKind
Because constructor overload might be tricky maybe like that
var now = DateTime.UtcNow;
var officeHoursStart = new DateTime().BasedOnTz(timeZoneInfo, now.Year, now.Month, now.Day, workHoursStart.Hour, workHoursStart.Minute, 0, 0);
var officeHoursStartUtc = officeHoursStart.ToUtc();
To restate the problem - you have as input a time and a time zone, and you need as output the point in time that corresponds to that time on the current day in the given time zone.
Since the output is a point in time, you should prefer using DateTimeOffset
. (If you must use a DateTime
, you can take the .DateTime
property from that.)
Since the input is a time of day, you should prefer using TimeOnly
, available in .NET 6 and higher. (If you're using older .NET, then use a TimeSpan
.)
Here's the general approach:
using System;
using System.Globalization;
// Get the time zone as a TimeZoneInfo object, either directly or via TimeZoneConverter.
TimeZoneInfo timeZone = TimeZoneInfo.FindSystemTimeZoneById("Eurpope/Vienna");
// Parse the time string. Prefer using TimeOnly (.NET 6+).
string timeString = "17:00";
TimeOnly time = TimeOnly.ParseExact(timeString, "HH:mm", CultureInfo.InvariantCulture);
// Or use TimeSpan on older .NET
// TimeSpan timeSpan = TimeSpan.ParseExact(timeString, "hh\\:mm", CultureInfo.InvariantCulture);
// Get the time on "today" with respect to the given time zone
DateTimeOffset timeTodayInTimeZone = time.OnTodayInTimeZone(timeZone);
You'll need some extension methods. Pick one of these, depending on which input you're using.
public static DateTimeOffset OnTodayInTimeZone(this TimeOnly time, TimeZoneInfo tz) =>
DateOnly.FromDateTime(TimeZoneInfo.ConvertTime(DateTime.UtcNow, tz))
.ToDateTime(time)
.ToDateTimeOffset(tz);
public static DateTimeOffset OnTodayInTimeZone(this TimeSpan time, TimeZoneInfo tz) =>
TimeZoneInfo.ConvertTime(DateTime.UtcNow, tz).Date
.Add(time)
.ToDateTimeOffset(tz);
And finally, you need this extension method that has the bulk of the logic.
(I've used this one on several other answers now.)
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));
}