Preserve DateTimeOffset in Date String after UTC to TimeZoneInfo Date Conversion - c#

Given the following UTC date string: "2020-10-10T12:00:00"
When converting to string, why do I loose the datetime offset, despite specifying it in the format?
string parseString = "2020-10-10T12:00:00";
if (DateTime.TryParseExact(parseString, "s", null, DateTimeStyles.None, out DateTime parsedDate))
{
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Pacific/Honolulu");
DateTime atUtc = DateTime.SpecifyKind(parsedDate, DateTimeKind.Utc);
DateTimeOffset dateAtTimeZone = TimeZoneInfo.ConvertTime(atUtc, timeZoneInfo);
Console.WriteLine(dateAtTimeZone.ToString("O"));
Console.WriteLine(dateAtTimeZone.ToString("yyyy-MM-ddTHH:mm:ss.fffffffK"));
}
Why do I get back the following: 2020-10-10T02:00:00.0000000+13:00
Instead of: 2020-10-10T02:00:00.0000000-10:00

There are two TimeZoneInfo.ConvertTime that takes 2 parameters - one that takes a DateTime and returns a DateTime, and one that takes an DateTimeOffset and returns a DateTimeOffset. Right now, you are calling the one that takes a DateTime:
DateTime atUtc = DateTime.SpecifyKind(parsedDate, DateTimeKind.Utc);
DateTimeOffset dateAtTimeZone = TimeZoneInfo.ConvertTime(atUtc, timeZoneInfo); // atUtc is a DateTime here!
The fact that you are assigning the result to DateTimeOffset doesn't matter, because the return value type does not participate in overload resolution.
The overload that takes a DateTime just changes the date and time parts of the DateTime passed in, since timezone/offset information is not a part of DateTimes. When you convert it to DateTimeOffset, your local offset is added to it during the conversion, and that is what gets printed.
What you should be calling is the overload that takes an DateTimeOffset. Because it takes a DateTimeOffset, it is able to change the offset part of the DateTimeOffset as well as the date and time parts, to the desired values. You need to declare atUtc as DateTimeOffset too:
DateTimeOffset atUtc = DateTime.SpecifyKind(parsedDate, DateTimeKind.Utc);
DateTimeOffset dateAtTimeZone = TimeZoneInfo.ConvertTime(atUtc, timeZoneInfo);
Or better, use DateTimeOffset.TryParseExact in the first place!

Related

Create new DateTime in Country via TimeZoneInfo for office hours

Question
Q: How can I create a DateTime for e.g. 09:00 in Europe/Vienna
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.
Problem
P: TimeZoneInfo is not a parameter of DateTime. Why not? Could there be a simple extension?
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));
}

C# DateTimeOffset to DateTime Conversion

Can somebody please explain to me why the following code returns DateTimeKind.Unspecified. I was expecting DateTimeKind.Utc:
DateTimeOffset dateTimeOffset = DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Utc);
DateTimeKind dateTimeKind = dateTimeOffset.DateTime.Kind;
Implicit cast operator from DateTime to DateTimeOffset behaves as DateTimeOffset constructor. You can have a look at sources for details
// Constructs a DateTimeOffset from a DateTime. For Local and Unspecified kinds,
// extracts the local offset. For UTC, creates a UTC instance with a zero offset.
public DateTimeOffset(DateTime dateTime) {
TimeSpan offset;
if (dateTime.Kind != DateTimeKind.Utc) {
// Local and Unspecified are both treated as Local
offset = TimeZoneInfo.GetLocalUtcOffset(dateTime, TimeZoneInfoOptions.NoThrowOnInvalidTime);
}
else {
offset = new TimeSpan(0);
}
m_offsetMinutes = ValidateOffset(offset);
m_dateTime = ValidateDate(dateTime, offset);
}
And ValidateDate() returns Unspecified kind
...
// make sure the Kind is set to Unspecified
//
return new DateTime(utcTicks, DateTimeKind.Unspecified);
The important point here is also described in remarks section of DateTimeOffset constructor
This constructor's behavior depends on the value of the DateTime.Kind
property of the dateTime parameter:
If the value of DateTime.Kind is DateTimeKind.Utc, the DateTime property
of the new instance is set equal to dateTime, and the Offset property is set
equal to Zero.
If the value of DateTime.Kind is DateTimeKind.Local or
DateTimeKind.Unspecified, the DateTime property of the new instance is
set equal to dateTime, and the Offset property is set equal to the
offset of the local system's current time zone.
You are checking the input datetime Kind and extract Offset value based on it. The DateTime property of resulting DateTimeOffset is Unspecified, because Offset property already represented a date offset. DateTime property of DateTimeOffset is also saying that
The value of the DateTime.Kind property of the returned DateTime
object is DateTimeKind.Unspecified.

Why UtcDateTime function is not adding the offset to the UTC date?

For instantaneous DateTime tracking, I am using a DateTimeOffset datatype. The following function adds the user corresponding TimeZone ID offset to the UTC DateTime property of DateTimeOffset
According to the documentation, UtcDateTime will perform both a time zone conversion and a type conversion on a DateTimeOffset. The following code does not though. Why is the conversion not taking place?
Function to add TimeSpan offset,
public static DateTimeOffset GetUtcDateTime (DateTime sourceDateTime, string timeZoneId) {
TimeZoneInfo timeZone = TimeZoneInfo.FindSystemTimeZoneById (timeZoneId);
TimeSpan offset = timeZone.GetUtcOffset (sourceDateTime);
DateTimeOffset utcTime = new DateTimeOffset (sourceDateTime, offset);
return utcTime;
}
and here where I am trying to convert,
DateTimeOffset utcDate = (DateTime.UtcNow);
DateTime fromUtc = utcDate.DateTime;
DateTimeOffset UtcDate = StaticHandlers.GetUtcDateTime (fromUtc, "America/Los_Angeles");
Console.WriteLine ("UTC now is {0} and UTC Date LA is {1} and UtcDateTime LA is {2}", utcDate, UtcDate, utcDate.UtcDateTime);
the output is,
UTC now is 5/8/18 6:43:37 AM +00:00 and and UTC Date LA is 5/8/18
6:43:37 AM -07:00 UtcDateTime LA is 5/8/18 6:43:37 AM
update,
I want to preserve both UTC and the user offset for tracking purposes. DST matters in this context. The example below shows what I am talking about.
DateTime currentDateTime = DateTime.Now;
DateTime beforeDST_LA = new DateTime (2018, 3, 11, 0, 0, 0);
DateTime afterDST_LA = new DateTime (2018, 3, 12, 0, 0, 0);
TimeSpan offsetCurrent = tzi.GetUtcOffset (currentDateTime);
TimeSpan offsetBeforeDST = tzi.GetUtcOffset (beforeDST_LA);
TimeSpan offsetAfterDST = tzi.GetUtcOffset (afterDST_LA);
Console.WriteLine ("Current offset is {0} before DST is {1} and After DST is {2}", offsetCurrent, offsetBeforeDST, offsetAfterDST);
Current offset is -07:00:00 before DST is -08:00:00 and After DST is
-07:00:00
First, I would not call your function GetUtcDateTime, because that's not what it does. It is trying to get a DateTimeOffset for a specific time zone for a specific time, so call it something like GetDateTimeOffset.
The main concept you're missing in your code is that DateTime has .Kind property, which sets a DateTimeKind value. The kind is taken into consideration by several places in your code:
GetUtcOffset will convert Utc or Local kinds to the zone provided before determining the offset.
new DateTimeOffset (the constructor) will error if the kind and the offset conflict, if you provide an offset.
When you assign a DateTime to a DateTimeOffset, the implicit conversion is evaluating the kind.
When you call .DateTime from the DateTimeOffset, the kind will always be Unspecified - regardless of the offset.
If you take all of this into account, you'll realize you need to check the kind yourself before calling GetUtcOffset. If it's not Unspecified then you'll need to convert it to the specified time zone before getting the offset.
public static DateTimeOffset GetDateTimeOffset(DateTime sourceDateTime, string timeZoneId)
{
TimeZoneInfo timeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
// here, is where you need to convert
if (sourceDateTime.Kind != DateTimeKind.Unspecified)
sourceDateTime = TimeZoneInfo.ConvertTime(sourceDateTime, timeZone);
TimeSpan offset = timeZone.GetUtcOffset(sourceDateTime);
return new DateTimeOffset(sourceDateTime, offset);
}
Now that this is handled, turn to the next set of problems, which is where you call it.
DateTimeOffset utcDate = (DateTime.UtcNow);
DateTime fromUtc = utcDate.DateTime;
In line 1, the implicit cast from DateTime to DateTimeOffset sets the offset to 00:00 - because DateTime.UtcNow has .Kind == DateTimeKind.Utc.
In line 2, the call to the .DateTime property sets fromUtc.Kind == DateTimeKind.Unspecified. Essentially, you've stripped away the kind.
So instead of this, just pass DateTime.UtcNow directly into the function. The kind will persist, and it will all work - now that the Kind is recognized and the conversion is happening inside the function.
All that said, if your original values are all DateTimeOffset (example, DateTimeOffset.UtcNow) then you don't need that function at all. Just call TimeZoneInfo.ConvertTime with the DateTimeOffset directly.

Converting UTC DateTime to EST DateTime with correct offset

My app gets a DateTime object in UTC and needs to output as a string it in EST format. I've tried the following code, but when I get the output the offset still shows as +00:00 instead of -05:00
static void Main(string[] args)
{
var currentDate = DateTime.Now.ToUniversalTime();
var convertedTime = ConvertUtcToEasternStandard(currentDate);
Console.WriteLine(currentDate.ToString("yyyy-MM-ddTHH:mm:sszzz"));
Console.WriteLine(convertedTime.ToString("yyyy-MM-ddTHH:mm:sszzz"));
}
private static DateTime ConvertUtcToEasternStandard(DateTime dateTime)
{
var easternZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
return TimeZoneInfo.ConvertTimeFromUtc(dateTime, easternZone);
}
This outputs:
2016-11-18T06:56:14+00:00
2016-11-18T01:56:14+00:00
So the time is being shifted correctly, but the offset stays at +00:00 when I want it to be -05:00. Any idea how I can get a DateTime object with the correct offset when output with the above format string?
I read this in the API not long ago, basically, with DateTime values, it's very much impossible to get the zzz format offset to be something useful.
With DateTime values, the "zzz" custom format specifier represents the signed offset of the local operating system's time zone from UTC, measured in hours and minutes. It does not reflect the value of an instance's DateTime.Kind property. For this reason, the "zzz" format specifier is not recommended for use with DateTime values.
With DateTimeOffset values, this format specifier represents the DateTimeOffset value's offset from UTC in hours and minutes.
The offset is always displayed with a leading sign. A plus sign (+) indicates hours ahead of UTC, and a minus sign (-) indicates hours behind UTC. A single-digit offset is formatted with a leading zero.
For example, I'm in Eastern Standard Time, and this is my result:
2016-11-18T07:9:38-05:00
2016-11-18T02:9:38-05:00
Obviously UTC time shouldn't have an offset of -05:00.
Modify your code just a bit and we have a solution:
void Main()
{
var currentDate = DateTimeOffset.Now.ToUniversalTime();
var convertedTime = ConvertUtcToEasternStandard(currentDate);
var format = "yyyy-MM-ddTHH:m:sszzz";
Console.WriteLine(currentDate.ToString(format));
Console.WriteLine(convertedTime.ToString(format));
}
// Define other methods and classes here
private static DateTimeOffset ConvertUtcToEasternStandard(DateTimeOffset dateTime)
{
var easternZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
return TimeZoneInfo.ConvertTime(dateTime, easternZone);
}
Result:
2016-11-18T07:17:46+00:00
2016-11-18T02:17:46-05:00
That's more like it.
Note: replaced previous code, ignorance got the best of me and I failed to realize that it wasn't working right. TimeZoneInfo.ConvertTime takes a DateTimeOffset which does what we want.
And if we add another case for Pacific Standard Time:
private static DateTimeOffset ConvertUtcToPacificStandard(DateTimeOffset dateTime)
{
var pacificZone = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time");
return TimeZoneInfo.ConvertTime(dateTime, pacificZone);
}
We get the correct result:
2016-11-17T23:21:21-08:00

Format DateTime without string conversion

I'm revising some legacy code and there is this...
DateTime dateTime = DateTime.Now;
DateTime from = DateTime.Parse(dateTime.ToString("dd/MM/yyyy 00:00:00"));
DateTime to = DateTime.Parse(dateTime.AddDays(8).ToString("dd/MM/yyyy 23:59:59"));
The from and to variables are then used in Linq / Lambda comparisons, so must be a DateTime.
I can't seem to find a way to format a DateTime variable without converting it to a string, and then back to a DateTime, this seems daft to say the least.
Surely there must be a way to format a DateTime without converting it to a string and then back to a DateTime?
There is no need to convert your DateTime to string and then parse it back to DateTime, instead use DateTime.Date like:
DateTime from = dateTime.Date;
DateTime to = dateTime.Date.AddDays(9).AddTicks(-1); //or .AddSeconds(-1) if you want
// accuracy to a second.
A DateTime doesn't have any implicit format. String representations of it have. And Today property sets time part to midnight.
DateTime from = DateTime.Today;
DateTime to = DateTime.Today.AddDays(9).AddSeconds(-1);

Categories

Resources