I am attempting to get a correctly formatted ISO 8601 date, and I keep hitting issues. My initial code seemed to be working, but I found that DST dates were not returning as expected. I made a .NET fiddle to ask about this issue here on stackoverflow, but it seems the way the "system" timezone works is going to cause me further problems when I deploy my code.
Here is a dotnet fiddle that displays something completely wrong:
using System;
public class Program
{
public static void Main()
{
var val3 = TimeZoneInfo.ConvertTimeFromUtc(new DateTime(2021, 10, 13, 18, 0, 0), TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time"));
Console.WriteLine(val3.ToString("yyyy-MM-ddTHH:mm:sszzz"));
}
}
If I run this, I get the following:
2021-10-13T13:00:00+00:00
So the time is correct for CST, but the offset looks like it is reflecting the "system" timezone on the server. Taken altogether, the date is completely different than the input date.
If I run this code on my development system where the local timezone is CST, I get yet another different answer:
2021-10-13T08:00:00-06:00
Given that the date in question was in DST, I expect both of the above to return the following:
2021-10-13T13:00:00-05:00
What I am doing wrong?
Let me see if my understanding is correct (I'm still not entirely sure when the SOAP comes into play in the question).
You have a UTC DateTime:
var dt = new DateTime(2021, 10, 13, 18, 0, 0, DateTimeKind.Utc);
And you'd like to format that in CST -- which, on October 13, is actually CDT (-05:00)?
If that is correct, then you want to utilise DateTimeOffset, which has a better understanding of.. well, time offsets.
// Your original value. This will have an offset of "00:00"
// as the DateTime above was created as `DateTimeKind.Utc`
var timeAtUtc = new DateTimeOffset(dt);
// Alternatively:
// var timeAtUtc = new DateTimeOffset(2021, 10, 13, 18, 0, 0, TimeSpan.Zero);
// Find out the correct offset on that day (-5:00)
var cstOffsetAtTheTime = TimeZoneInfo
.FindSystemTimeZoneById("Central Standard Time")
.GetUtcOffset(timeAtUtc);
// And now apply the offset to your original value
var timeAtCst = timeAtUtc.ToOffset(cstOffsetAtTheTime);
// You will now get "2021-10-13T13:00:00-05:00"
// no matter where you run this from.
Console.WriteLine(timeAtCst.ToString("yyyy-MM-ddTHH:mm:sszzz"));
// Edit: If you pass it a date in Feb, you'll see that it will correctly be at -06:00.
Edit 2022-03-07 based on comment below. If you don't need to care about the offset value, you can simply do:
var timeAtUtc = ...;
var timeAtCst = TimeZoneInfo.ConvertTimeBySystemTimeZoneId(
timeAtUtc,
"Central Standard Time");
NPras's answer is good, but just to answer the question about what was wrong with the original code:
A DateTime object does not store time zone offset information. It only stores a Kind property, which can be one of three DateTimeKind values, either Utc, Local, or Unspecified.
Since your input was a DateTime, the output also has to be a DateTime, which has no time zone offset. Thus the offset related to your target time zone is discarded. Using a DateTimeOffset allows the target offset to persist.
In the final console output, you used zzz to show the offset. The .NET documentation explains why this used the server's time zone:
With DateTime values, the "z" custom format specifier represents the signed offset of the local operating system's time zone from Coordinated Universal Time (UTC), measured in hours. It doesn't reflect the value of an instance's DateTime.Kind property. For this reason, the "z" 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.
As an aside, if you actually need to show an offset for a DateTime whose Kind is either Utc or Local, you should use the K specifier (the K is for Kind) instead of zzz. With the K specifier, Utc kinds will display a "Z", Local values will display the local offset such as "-05:00", and Unspecified values will display neither (an empty string).
Related
I am working with DateTime values in a SQL Server database I don't maintain, and I want to work with them in my code as UTC.
To assist in understanding the problem, the values I'm working with represent the time that actions took place in our CRM system.
When I retrieve the values from SQL Server, they have no time zone indication on them but I know that they always represent Europe/London - either UTC in the winter, or UTC+1 in the summer.
I understand that I can use DateTimeKind.Local to indicate that a DateTime value is expressed in local time, but I don't understand how I specify which time zone the DateTime applies to. For example, if I'm working with the DateTime 2021-01-01 12:34:56, I need to ensure that regardless of where or when my code is running, this date is correctly interpreted as 2021-01-01 12:34:56 +00:00. Equally, I need to ensure that regardless of where or when my code is running, I need to interpret 2021-05-01 12:34:56 as 2021-05-01 12:34:56 +01:00.
How can I indicate that my DateTime values always apply to Europe/London at the time they represent?
Check nodaTime is a powerful datetime library for C#.
The example in there homepage seems to be related to your case.
/ Instant represents time from epoch
Instant now = SystemClock.Instance.GetCurrentInstant();
// Convert an instant to a ZonedDateTime
ZonedDateTime nowInIsoUtc = now.InUtc();
// Create a duration
Duration duration = Duration.FromMinutes(3);
// Add it to our ZonedDateTime
ZonedDateTime thenInIsoUtc = nowInIsoUtc + duration;
// Time zone support (multiple providers)
var london = DateTimeZoneProviders.Tzdb["Europe/London"];
// Time zone conversions
var localDate = new LocalDateTime(2012, 3, 27, 0, 45, 00);
var before = london.AtStrictly(localDate);
I am pushing date/time values into the HubSpot CRM system via their API. For date/time values, the HS API requires UNIX format, which is milliseconds from Epoch (1/1/1970 12:00 AM). [HubSpot docs: https://developers.hubspot.com/docs/faq/how-should-timestamps-be-formatted-for-hubspots-apis]
But my dates are representing incorrectly. I am pulling dates from a SQL database that is in EST, and performing the following conversion:
string dbValue = "2019-02-03 00:00:00";
DateTime dt = Convert.ToDateTime(dbValue);
DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, System.DateTimeKind.Utc);
long apiValue = Convert.ToInt64(dt.Subtract(epoch).TotalMilliseconds);
However, in HubSpot, the date field shows 2/2/2019. The time zone in HubSpot is set to UTC -4 Eastern Time.
It seems like there is some conversion issue, but I do not know what to do to correct it. I've tried using DateTime.SpecifiyKind to explicitly set dt to local before converting to long:
dt = DateTime.SpecifyKind(dt, System.DateTimeKind.Local);
But that hasn't worked either. I tried doing a basic test:
var dt1 = new DateTime(2019, 4, 1, 12, 0, 0, DateTimeKind.Local);
var dt2 = new DateTime(2019, 4, 1, 12, 0, 0, DateTimeKind.Utc);
Console.WriteLine(dt1.Subtract(dt2).TotalSeconds);
But the result was 0. I am in CST, and I was expecting like a 5 hour difference. I feel like I am missing some fundamental concept here on how DateTimes work in C#.
A few things:
Subtracting DateTime values does not take DateTimeKind into account.
.NET Framework 4.6 and higher have conversion functions to/from Unix time built in to the DateTimeOffset class, so you don't need to do any subtraction at all.
When you say EST or CST, I'll assume you meant US Eastern Time or US Central Time. Keep in mind that because of daylight saving time, that EDT or CDT might apply to some of your values.
You shouldn't be parsing from strings if the value is coming from your database. I'll assume you just gave that for the example here. But in your actual code, you should be doing something like:
DateTime dt = (DateTime) dataReader("field");
(If you're using Entity Framework or some other ORM, then this part would be handled for you.)
It doesn't matter what time zone the SQL Server is located in. It matters rather what time zone the dbValue you have is intended to represent. A best practice is to keep time in terms of UTC, in which case the server's time zone should be irrelevant.
If the datetime stored in your SQL server is actually in UTC, then you can simply do this:
long apiValue = new DateTimeOffset(dt, TimeSpan.Zero).ToUnixTimeMilliseconds();
If the datetime stored in your SQL server really is in US Eastern Time, then you will need to first convert from Eastern Time to UTC:
TimeZoneInfo tz = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
DateTime utc = TimeZoneInfo.ConvertTimeToUtc(dt, tz);
long apiValue = new DateTimeOffset(utc).ToUnixTimeMilliseconds();
Note that the Windows time zone ID of "Eastern Standard Time" represents US Eastern Time including EDT when applicable, despite having the word "Standard" in the middle.
If you are running in .NET Core on a non-Windows platform, pass "America/New_York" instead. (And if you need to write for cross-platform resiliency, use my TimeZoneConverter library.)
Lastly, though it might be a bit dangerous to assume the time in the DB is in the same local time as the code accessing the DB, if you really wanted to make such a gamble, you could do it like this:
long apiValue = new DateTimeOffset(dt).ToUnixTimeMilliseconds();
That works only if dt.Kind is DateTimeKind.Unspecified or DateTimeKind.Local, as it would then apply the local time zone. But again, I would recommend against this.
Given a Local DateTime value on a computer configured for PST (which will implicitly change to PDT on March 10th when DST kicks in), how can one obtain a string including the appropriate timezone - eg. PST/PDT, not offset! - in the output?
DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss ???")
Expected output strings, eg:
"2019-02-09 13:04:22 PST" // right after lunch today
"2019-04-09 13:04:22 PDT" // right after lunch in two months
The MSDN DateTime Custom Format Strings page shows examples of explicitly hard-coding "PST" into the output, which will be wrong half the year and/or when the local TZ is changed. Computers and people move, so hard-coding TZ values is simply 'not appropriate'.
Preferably this can be done with just a Format String, allowing DateTime values to be supplied until the rendering/to-string phase - although there does not appear to be a 'ZZZ' format. I've specified 'Local' DateTime Kind to, hopefully, reduce some additional quirks..
Since a DateTime instance does not keep timezone information, there is no way to do it with custom date and time format strings. "zzz" specifier is for UTC Offset value, DateTime.Kind with "K" specifier does not reflect time zone abbrevation neither. Both are useless for your case.
However, there is nuget package called TimeZoneNames which is written by time zone geek Matt Johnson that you can get abbreviations of a timezone name (supports both IANA and Windows time zone identifier)
var tz = TZNames.GetAbbreviationsForTimeZone("Pacific Standard Time", "en-US");
Console.WriteLine(tz.Standard); // PST
Console.WriteLine(tz.Daylight); // PDT
If you wanna get your windows time zone identifier programmatically, you can use TimeZoneInfo.Local.Id property, if you wanna get the current language code, you can use CultureInfo.CurrentCulture.Name property by the way.
var tz = TZNames.GetAbbreviationsForTimeZone(TimeZoneInfo.Local.Id, CultureInfo.CurrentCulture.Name);
But before that, you should check your local time is daylight saving time to choose which abbreviation to append your formatted string.
DateTime now = DateTime.Now;
bool isDaylight = TimeZoneInfo.Local.IsDaylightSavingTime(now);
If isDaylight is true, you should use the result of the TimeZoneValues.Daylight property, otherwise you should use TimeZoneValues.Standard property of the first code part.
At the end, you need append one of those abbreviation at the end of DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss) string.
For an important note from Matt on the package page;
Time zone abbreviations are sometimes inconsistent, and are not
necessarily localized correctly for every time zone. In most cases,
you should use abbreviations for end-user display output only. Do not
attempt to use abbreviations when parsing input.
Second important note from Matt's comment;
What makes you think that a time zone abbreviation actually exists for
every time zone and every language in the world? Even, then what makes
you think that a time zone abbreviation is enough to identify the time
zone? Hint - both questions are amorphous. Consider "CST" or "IST" -
Each have three or four places in the world that they might belong to.
Many many many other cases as well...
TimeZoneInfo should be able to help here: https://learn.microsoft.com/en-us/dotnet/api/system.timezoneinfo.standardname?view=netframework-4.7.2
It looks like TimeZoneInfo gives full names ("Pacific Standard Time"/"Pacific Daylight Time") rather than abbreviations ("PST"/"PDT"). It that's a problem, you'll still need to find a source for the short names. There are some ideas on how to do that here: Timezone Abbreviations
using System;
using Xunit;
namespace Q54610867
{
public class TimeZoneTests
{
// I'm on Mac/Unix. If you're on Windows, change the ID to "Pacific Standard Time"
// See: https://github.com/dotnet/corefx/issues/2538
readonly TimeZoneInfo pacificStandardTime = TimeZoneInfo.FindSystemTimeZoneById("America/Los_Angeles");
[Fact]
public void Today()
{
var today = new DateTime(2019, 2, 9, 13, 4, 22, DateTimeKind.Local);
Assert.Equal("2019-02-09 13:04:22 Pacific Standard Time", ToStringWithTz(today, pacificStandardTime));
}
[Fact]
public void future()
{
var future = new DateTime(2019, 4, 9, 13, 4, 22, DateTimeKind.Local);
Assert.Equal("2019-04-09 13:04:22 Pacific Daylight Time", ToStringWithTz(future, pacificStandardTime));
}
static string ToStringWithTz(DateTime dateTime, TimeZoneInfo tz)
=> $"{dateTime.ToString("yyyy-MM-dd HH:mm:ss")} {(tz.IsDaylightSavingTime(dateTime) ? tz.DaylightName : tz.StandardName)}";
}
}
On my dev server I save a value called "ScheduledDateUtc" date to sqlserver e.g. 24-11-14 09:00:00. I also save a value called "UtcOffset" and calculate the "ScheduledDateLocal" like so:
var ScheduledDateLocal = ScheduledDateUtc.AddHours(UtcOffset); //a negative offset would be deducted and a positive offset would be added
On my dev server this works fine and its calculates the correct ScheduledDateLocal for alle timezones/UtcOffset's. However, when I deploy to a Azure server in a different timezone this calculation is a few hours off.
Can anyone explain why? I'm guessing there is some kind of setting or system specific conversion param? Thanks!
Is it possible that the incorrect result is actually on your machine and not Azure, and is because you are initialising ScheduledDateUtc as local time and not UTC?
Consider these two lines of code:
new DateTime(2015, 6, 1, 1, 1, 1).AddHours(5).ToUniversalTime().Dump();
new DateTime(2015, 6, 1, 1, 1, 1, DateTimeKind.Utc).AddHours(5).ToUniversalTime().Dump();
Here, we are in summer time which is UTC+1. The output of the above is:
01/06/2015 05:01:01
01/06/2015 06:01:01
They're 1 hour out as I 'forgot' to specify that the input is UTC in the constructor in the first line of code.
If you don't have access to the constructor because ScheduledDateUtc is initialised by an ORM, you can use SpecifyKind:
ScheduledDateUtc = DateTime.SpecifyKind(ScheduledDateUtc, DateTimeKind.Utc)
Its unfortunate that you're storing an offset not a timezone as you may have issues with daylight savings time. If you were storing a Windows timezone name, you could use TimeZoneInfo.ConvertTimeFromUtc (no need to specify Kind here as this assumes UTC) as per this example from MSDN:
TimeZoneInfo cstZone = TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time");
ScheduledDateLocal = TimeZoneInfo.ConvertTimeFromUtc(ScheduledDateUtc, cstZone);
You could instead instantiate a "custom" timezone using your offset and still use the above function.
I faced a similar issue where I had to schedule events based on a user's local time. I ended up storing the day of the week, hour, minute, and Olson timezone string, and used Noda-Time to convert to a DateTimeOffset and from there to server time.
I'm getting a
"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"
message when I try to convert a time to UTC. The client that's experiencing the issue is in France, but the issue appears to have started when the US went to daylight savings time. I see there's a spot in the code that's using Eastern Time Zone. I'm thinking it must be somehow trying to use Eastern Standard time, somehow.
At any rate I tried mimicking their scenario by calling the same method after setting my locale to French/France and my timezone to Amsterdam, Berlin, etc. My test code is doing:
DateTime eventDate = System.DateTime.Now.ToLocalTime();
What doesn't make sense is that it's returning a date time as of Eastern Daylight Savings Time US, not the time that's being displayed by my system clock. Is the BIOS clock set to EDT or something? My intention was to pass eventDate to the method that seems to be working incorrectly for my French client.
What you've written is absolutely not possible.
You said you're getting the error:
"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"
That can only happen when converting a local time to UTC - not the other way around.
Also, it can only occur when using the conversion methods on TimeZoneInfo. For example, the following code will create an exception with that message:
TimeZoneInfo tz = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
DateTime dt = new DateTime(2014, 3, 9, 2, 30, 0);
DateTime utc = TimeZoneInfo.ConvertTimeToUtc(dt, tz);
You might also think you'd get the same exception if your computer's time zone was set to "Eastern Standard Time" and you ran this code:
DateTime dt = new DateTime(2014, 3, 9, 2, 30, 0);
DateTime utc = dt.ToUniversalTime();
However, this method will silently allow invalid input without throwing an exception. It will just adjust forward an hour, as if you had passed 3:30 instead of 2:30.
Additionally, as others pointed out, DateTime.Now already is local and has Local kind. So calling ToLocalTime on it won't do anything. If you meant to call ToUniversalTime, then just use DateTime.UtcNow instead.
You may also be interested in reading the dst tag wiki, and the timezone tag wiki, which contain relevant details.
I added TimeZoneInfo.ClearCachedData(); and before my call to Now and it now returns the correct time.