I have users that can be in different timezones and I'm looking to determine the UTC value of the beginning of their days and their months. Inside an object, I have a method that attempts to do that; it looks like this:
private void SetUserStartTimesUTC()
{
DateTime TheNow = DateTime.UtcNow.ConvertUTCTimeToUserTime(this.UserTimezoneID);
DateTime TheUserStartDateUserTime = TheNow.Date;
DateTime TheUserStartMonthUserTime = new DateTime(TheNow.Year, TheNow.Month, 1);
DateTime TheUserEndMonthUserTime = TheUserStartMonthUserTime.AddMonths(1);
this.UserStartOfDayUTC = TheUserStartDateUserTime.ConvertUserTimeToUTCTime(this.UserTimezoneID);
this.UserStartOfMonthUTC = TheUserStartMonthUserTime.ConvertUserTimeToUTCTime(this.UserTimezoneID);
this.UserEndOfMonthUTC = TheUserEndMonthUserTime.ConvertUserTimeToUTCTime(this.UserTimezoneID);
}
And this method depends on two other extension methods that do the conversions between a user's time and UTC time
public static DateTime ConvertUserTimeToUTCTime(this DateTime TheUserTime, string TheTimezoneID)
{
TimeZoneInfo TheTZ = TimeZoneInfo.FindSystemTimeZoneById(TheTimezoneID);
DateTime TheUTCTime = new DateTime();
if (TheTZ != null)
{
DateTime UserTime = new DateTime(TheUserTime.Year, TheUserTime.Month, TheUserTime.Day, TheUserTime.Hour, TheUserTime.Minute, TheUserTime.Second);
TheUTCTime = TimeZoneInfo.ConvertTimeToUtc(UserTime, TheTZ);
}
return TheUTCTime;
}
public static DateTime ConvertUTCTimeToUserTime(this DateTime TheUTCTime, string TheTimezoneID)
{
TimeZoneInfo TheTZ = TimeZoneInfo.FindSystemTimeZoneById(TheTimezoneID);
DateTime TheUserTime = new DateTime();
if (TheTZ != null)
{
DateTime UTCTime = new DateTime(TheUTCTime.Year, TheUTCTime.Month, TheUTCTime.Day, TheUTCTime.Hour, TheUTCTime.Minute, 0, DateTimeKind.Utc);
TheUserTime = TimeZoneInfo.ConvertTime(UTCTime, TheTZ);
}
return TheUserTime;
}
Now I've been dealing with timezone issues for a while and I know that timezone issues can introduce off-by-one bugs that can be hard to detect.
Does my implementation of timezones seem to be correct or is there an edge case that will create some sort of off-by-one bug?
Thanks for your suggestions.
Your methods seem needlessly complicated, to be honest.
Why would you have a parameter called TheUTCTime and then create a UTC version of it? Shouldn't it already have a Kind of UTC? Even if it didn't, you would be better off using DateTime.SpecifyKind - currently when converting one way you wipe out the seconds, whereas converting the other way you don't... in both cases you wipe out any sub-second values.
Also:
TimeZoneInfo.FindSystemTimeZoneById never returns null
Returning new DateTime() (i.e. January 1st 0001 AD) if the time zone can't be found seems like a really poor way of indicating an error
There's no need to have a local variable in your conversion methods; just return the result of calling ConvertTime directly
Your "end of month" is really "start of the next month"; that may be what you want, but it's not clear.
Personally I would strongly advise you to avoid the BCL DateTime for all of this entirely. I'm entirely biased being the main author, but I'd at least hope that you'd find Noda Time more pleasant to work with... it separates out the idea of "date with no time component", "time with no date component", "local date and time with no specific time zone" and "date and time in a particular time zone"... so the type system helps you to only do sensible things.
EDIT: If you really have to do this within the BCL types, I'd write it like this:
private void SetUserStartTimesUTC()
{
DateTime nowUtc = DateTime.UtcNow;
TimeZoneInfo zone = TimeZoneInfo.FindSystemTimeZoneById(UserTimeZoneID);
// User-local values, all with a Kind of Unspecified.
DateTime now = TimeZoneInfo.ConvertTime(nowUtc, zone);
DateTime today = now.Date;
DateTime startOfThisMonth = todayUser.AddDays(1 - today.Day);
DateTime startOfNextMonth = startOfThisMonth.AddMonths(1);
// Now convert back to UTC... see note below
UserStartOfDayUTC = TimeZoneInfo.ConvertTimeToUtc(today, zone);
UserStartOfMonthUTC = TimeZoneInfo.ConvertTimeToUtc(startOfThisMonth, zone);
UserEndOfMonthUTC = TimeZoneInfo.ConvertTimeToUtc(startOfNextMonth, zone);
}
The extension methods you've added really don't provide much benefit, as you can see.
Now, the code mentions a "note" - you're currently always assuming that midnight always exists and is unambiguous. That's not true in all time zones. For example, in Brazil, on daylight saving changes forward, the time skips from midnight to 1am - so midnight itself is invalid, basically.
In Noda Time we fix this by having DateTimeZone.AtStartOfDay(LocalDate) but it's not as easy with the BCL.
For comparison, the equivalent Noda Time code would look like this:
private void SetUserStartTimesUTC()
{
// clock would be a dependency; you *could* use SystemClock.Instance.Now,
// but the code would be much more testable if you injected it.
Instant now = clock.Now;
// You can choose to use TZDB or the BCL time zones
DateTimeZone zone = zoneProvider.FindSystemTimeZoneById(UserTimeZoneID);
LocalDateTime userLocalNow = now.InZone(zone);
LocalDate today = userLocalNow.Date;
LocalDate startOfThisMonth = today.PlusDays(1 - today.Day);
LocalDate startOfNextMonth = startOfThisMonth.PlusMonths(1);
UserStartOfDayUTC = zone.AtStartOfDay(today);
UserStartOfMonthUTC = zone.AtStartOfDay(startOfThisMonth);
UserEndOfMonthUTC = zone.AtStartOfDay(startOfNextMonth);
}
... where the properties would be of type ZonedDateTime (which remembers the time zone). You could change them to be of type Instant (which is just a point in time) if you want, just chaining a ToInstant call for each property setter.
Related
I am trying to make a short date, but in the result I get one day more. With date like "2014-01-03 00:00:00" its okay, but it fails when time is "23:59:59".
EntryDate= "2014-01-03 23:59:59"
but getting result = "2014-01-04"
try
{
DateTime exact = DateTime.ParseExact(EntryDate, "yyyyMMdd", (IFormatProvider)CultureInfo.InvariantCulture);
mventryAttrib.Value = (exact.ToLocalTime().ToString("yyyy-MM-dd"));
}
catch (FormatException ex)
{
try
{
DateTime exact = DateTime.ParseExact(EntryDate, "yyyy-MM-dd HH:mm:ss", (IFormatProvider)CultureInfo.InvariantCulture);
mventryAttrib.Value = (exact.ToLocalTime().ToString("yyyy-MM-dd"));
}
catch
{
}
This is due to ParseExact returns a DateTime with a Kind property value of DateTimeKind.Unspecified.
This, when coupled with a call to .ToLocalTime() when you're in a timezone that has a positive offset from UTC, will bump the DateTime value forward by that many hours and return a DateTime value with a Kind property value of DateTimeKind.Local.
Here is a short program that will demonstrate:
var exact = DateTime.ParseExact("2014-01-03 23:59:59", "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
Console.WriteLine($"{exact} - {exact.Kind}");
var local = exact.ToLocalTime();
Console.WriteLine($"{local} - {local.Kind}");
Console.WriteLine(TimeZone.CurrentTimeZone.GetUtcOffset(exact));
Output (on my machine):
03.01.2014 23.59.59 - Unspecified
04.01.2014 00.59.59 - Local
01:00.00
If you intended the parsed DateTime value to be local from the outset you should make a new value that is specifically local, with the same values:
exact = new DateTime(exact.Ticks, DateTimeKind.Local);
Be aware though that this may have unforseen consequences when dealing with timezone boundaries. I would urge you to find a better library than the built in DateTime types, such as Noda Time.
It looks as though you are setting the time in exact as a UTC time and then converting this to a local time. This conversion is adding a number of hours to the time and consequently moving the date along.
Try exact.ToUniversalTime() and you should get the date you set.
I think this site could help you to solve the problem.
https://msdn.microsoft.com/en-gb/library/8kb3ddd4(v=vs.110).aspx
Can anyone give me the most straightforward way to create a ZonedDateTime, given "4:30pm" and "America/Chicago".
I want this object to represent that time for the current date in that timezone.
Thanks!
I tried this... but it seems to actually give me an instant in the local timezone which gets offset when creating the zonedDateTime.
string time = "4:30pm";
string timezone = "America/Chicago";
DateTime dateTime;
if (DateTime.TryParse(time, out dateTime))
{
var instant = new Instant(dateTime.Ticks);
DateTimeZone tz = DateTimeZoneProviders.Tzdb[timezone];
var zonedDateTime = instant.InZone(tz);
using NodaTime;
using NodaTime.Text;
// your inputs
string time = "4:30pm";
string timezone = "America/Chicago";
// parse the time string using Noda Time's pattern API
LocalTimePattern pattern = LocalTimePattern.CreateWithCurrentCulture("h:mmtt");
ParseResult<LocalTime> parseResult = pattern.Parse(time);
if (!parseResult.Success) {
// handle parse failure
}
LocalTime localTime = parseResult.Value;
// get the current date in the target time zone
DateTimeZone tz = DateTimeZoneProviders.Tzdb[timezone];
IClock clock = SystemClock.Instance;
Instant now = clock.Now;
LocalDate today = now.InZone(tz).Date;
// combine the date and time
LocalDateTime ldt = today.At(localTime);
// bind it to the time zone
ZonedDateTime result = ldt.InZoneLeniently(tz);
A few notes:
I intentionally separated many items into separate variables so you could see the progression from one type to the next. You may condense them as desired for fewer lines of code. I also used the explicit type names. Feel free to use var.
You may want to put this in a function. When you do, you should pass in the clock variable as a parameter. This will let you replace the system clock for a FakeClock in your unit tests.
Be sure to understand how InZoneLeniently behaves, and note how it's changing in the upcoming 2.0 release. See "Lenient resolver changes" in the 2.x migration guide.
I am working on writing a managed wrapper around Massachusetts Bay Transportation Authority (MBTA) Realtime API. They have a API which returns the server time which is unix timestamp (epoch). The library under which I am implementing it is PCL Profile 78 which means I have limited support for BCL TimeZone, so I resorted to using Nodatime
I am trying to convert the time returned from server to Eastern Time which is America/New_York as a DateTime object and reverse way. My current code is very dirty
public static class TimeUtils
{
static readonly DateTimeZone mbtaTimeZone = DateTimeZoneProviders.Tzdb["America/New_York"];
static readonly DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public static DateTime GetMbtaDateTime (long unixTimestamp)
{
var mbtaEpochTime = epoch.AddSeconds (unixTimestamp);
var instant = Instant.FromUtc (mbtaEpochTime.Year, mbtaEpochTime.Month,
mbtaEpochTime.Day, mbtaEpochTime.Hour, mbtaEpochTime.Minute, mbtaEpochTime.Second);
var nodaTime = instant.InZone (mbtaTimeZone);
return nodaTime.ToDateTimeUnspecified ();
}
public static long MbtaDateTimeToUnixTimestamp (DateTime time)
{
TimeSpan secondsSinceEpochMbtaTz = time - epoch;
var instant = Instant.FromUtc (time.Year, time.Month, time.Day, time.Hour, time.Minute, time.Second);
var mbtaTzSpan = mbtaTimeZone.GetUtcOffset (instant).ToTimeSpan ();
var epochDiff = secondsSinceEpochMbtaTz - mbtaTzSpan;
return (long)epochDiff.TotalSeconds;
}
}
Is there another way to write this simply. I hope Nodatime should have support for converting an epoch time to America/New_York DateTime and America/New_York DateTime to epoch time. My method MbtaDateTimeToUnixTimestamp is a brutal hack
Firstly, as mentioned in comments, it would be best to use Noda Time types throughout your code - only resort to DateTime when you really have to. This should lead to significantly cleaner code throughout.
Converting a Unix timestamp to an Instant is really easy:
Instant instant = Instant.FromUnixTimeSeconds(seconds);
You can then convert into a ZonedDateTime as per your current code... and using ToDateTimeUnspecified is fine if you really need to use DateTime.
For the reverse, your current code looks broken to me - you're assuming the DateTime is a UTC value, effectively. That would be at odds with your later use of the time zone. I suspect you want to convert the input to a LocalDateTime, and then apply the time zone. For example:
public static long MbtaDateTimeToUnixTimestamp(DateTime time)
{
var local = LocalDateTime.FromDateTime(time);
var zoned = local.InZoneStrictly(mbtaTimeZone);
var instant = zoned.ToInstant();
return instant.Ticks / NodaConstants.TicksPerSecond;
}
Note the InZoneStrictly call. This will throw an exception if either you pass in a local time which didn't exist or one that existed twice - in both cases due to DST transitions. This may well not be what you want - you really need to think about what you want to happen in those cases, or try to avoid them being feasible. See the time zones section of the documentation for more details and options.
I'm currently trying to ensure that our legacy back-end can support resolving date times based on the user's current time zone (or, more specifically offset). Our servers are in eastern standard time, and most of our date times originate there. However, for users that are in other time zones, a conversion to their time zone (or, in this case, offset) is needed when retrieving those date times. Also, date times coming from the user will have to be converted to eastern standard time before persistence on the server. Given that the front end we are developing is web-based, I am able to retrieve the user's offset in minutes and pass that value into my service layer within the header. I looked at Noda Time and think it's a great API. It did force me to think about time in a more refined matter, but I am still not 100% sure that I've properly used it correctly. Here are the methods that I wrote for the conversions described above. I've tested them and they seem to work. Given the scenario above, does this look like a proper use of the library? Am I thinking about date times properly?
public static DateTime ConvertToUtcFromEasternTimeZone(DateTime easternDateTime)
{
NodaTime.DateTimeZone easternTimeZone = NodaTime.DateTimeZoneProviders.Tzdb.GetZoneOrNull("America/New_York");
ZoneLocalMappingResolver customResolver = Resolvers.CreateMappingResolver(Resolvers.ReturnLater, Resolvers.ReturnStartOfIntervalAfter);
var easternLocalDateTime = LocalDateTime.FromDateTime(easternDateTime);
var easternZonedDateTime = easternTimeZone.ResolveLocal(easternLocalDateTime, customResolver);
return easternZonedDateTime.ToDateTimeUtc();
}
public static DateTime ConvertToEasternTimeZoneFromUtc(DateTime utcDateTime)
{
NodaTime.DateTimeZone easternTimeZone = NodaTime.DateTimeZoneProviders.Tzdb.GetZoneOrNull("America/New_York");
NodaTime.DateTimeZone utcTimeZone = NodaTime.DateTimeZoneProviders.Tzdb.GetZoneOrNull("UTC");
ZoneLocalMappingResolver customResolver = Resolvers.CreateMappingResolver(Resolvers.ReturnLater, Resolvers.ReturnStartOfIntervalAfter);
var utcLocal = LocalDateTime.FromDateTime(utcDateTime);
var utcZonedDateTime = utcTimeZone.ResolveLocal(utcLocal, customResolver);
var easternZonedDateTime = utcZonedDateTime.ToInstant().InZone(easternTimeZone);
return easternZonedDateTime.ToDateTimeUnspecified();
}
public static DateTime ConvertToUtc(DateTime dateTime, int offsetInMinutes)
{
LocalDateTime localDateTime = LocalDateTime.FromDateTime(dateTime);
var convertedDateTime = localDateTime.PlusMinutes(offsetInMinutes).ToDateTimeUnspecified();
return convertedDateTime;
}
public static DateTime ConvertFromUtc(DateTime dateTime, int offsetInMinutes)
{
LocalDateTime localDateTime = LocalDateTime.FromDateTime(dateTime);
var convertedDateTime = localDateTime.PlusMinutes(-offsetInMinutes).ToDateTimeUnspecified();
return convertedDateTime;
}
The idea here is that time zone matters when I'm resolving between UTC time and the time zone in the database. When I'm resolving between the client time and UTC time then offset matters.
In the future, we can persist UTC time, and this will be easier. Currently, this solution is a stop gap.
The idea is that we are going to go from...
client -> UTC +/- offset -> UTC -> Eastern Time -> database
database -> Eastern Time -> UTC -> UTC +/- offset -> client
to eventually...
client -> UTC +/- offset -> UTC -> database
database -> UTC -> UTC +/- offset -> client
Your first method looks okay, although we don't know what customResolver is.
Your second method is a bit off. I'd suggest:
public static DateTime ConvertToEasternTimeZoneFromUtc(DateTime utcDateTime)
{
var easternTimeZone = DateTimeZoneProviders.Tzdb["America/New_York"];
return Instant.FromDateTimeUtc(utcDateTime)
.InZone(easternTimeZone)
.ToDateTimeUnspecified();
}
Note that you don't need to look up the Eastern time zone in every method call - just have:
private static readonly DateTimeZone EasternTimeZone =
DateTimeZoneProviders.Tzdb["America/New_York"];
... then use that everywhere.
Your third and fourth methods aren't what I'd think of as idiomatic - for the third method you should use:
public static DateTime ConvertToUtc(DateTime dateTime, int offsetInMinutes)
{
var offset = Offset.FromMinutes(offsetInMinutes);
var localDateTime = LocalDateTime.FromDateTime(dateTime);
return new OffsetDateTime(localDateTime, offset).ToInstant()
.ToDateTimeUtc();
}
The fourth method seems a bit trickier, as we don't provide everything we should in terms of conversions with OffsetDateTime. The code you've used is probably okay, but it would certainly be cleaner if you could use OffsetDateTime.
EDIT: I've now added a method to Instant to make the fourth method cleaner. It will be part of 1.2.0, and you can use:
public static DateTime ConvertFromUtc(DateTime dateTime, int offsetInMinutes)
{
var offset = Offset.FromMinutes(offsetInMinutes);
var instant = Instant.FromDateTimeUtc(dateTime);
return instant.WithOffset(offset)
.LocalDateTime
.ToDateTimeUnspecified();
}
I would like to add that the first method could be rewritten without customResolver.
using System;
using NodaTime;
namespace qwerty
{
class Program
{
static void Main(string[] args)
{
var convertedInUTC = ConvertToUtcFromCustomTimeZone("America/Chihuahua", DateTime.Now);
Console.WriteLine(convertedInUTC);
}
private static DateTime ConvertToUtcFromCustomTimeZone(string timezone, DateTime datetime)
{
DateTimeZone zone = DateTimeZoneProviders.Tzdb[timezone];
var localtime = LocalDateTime.FromDateTime(datetime);
var zonedtime = localtime.InZoneLeniently(zone);
return zonedtime.ToInstant().InZone(zone).ToDateTimeUtc();
}
}
}
The user enters a date and a time in separate textboxes. I then combine the date and time into a datetime. I need to convert this datetime to UTC to save it in the database. I have the user's time zone id saved in the database (they select it when they register). First, I tried the following:
string userTimeZoneID = "sometimezone"; // Retrieved from database
TimeZoneInfo userTimeZone = TimeZoneInfo.FindSystemTimeZoneById(userTimeZoneID);
DateTime dateOnly = someDate;
DateTime timeOnly = someTime;
DateTime combinedDateTime = dateOnly.Add(timeOnly.TimeOfDay);
DateTime convertedTime = TimeZoneInfo.ConvertTimeToUtc(combinedDateTime, userTimeZone);
This resulted in an exception:
The conversion could not be completed because the supplied DateTime did not have the Kind property set correctly. For example, when the Kind property is DateTimeKind.Local, the source time zone must be TimeZoneInfo.Local
I then tried setting the Kind property like so:
DateTime.SpecifyKind(combinedDateTime, DateTimeKind.Local);
This didn't work, so I tried:
DateTime.SpecifyKind(combinedDateTime, DateTimeKind.Unspecified);
This didn't work either. Can anyone explain what I need to do? Am I even going about this the correct way? Should I be using DateTimeOffset?
Just like all the other methods on DateTime, SpecifyKind doesn't change an existing value - it returns a new value. You need:
combinedDateTime = DateTime.SpecifyKind(combinedDateTime,
DateTimeKind.Unspecified);
Personally I'd recommend using Noda Time which makes this kind of thing rather clearer in my rather biased view (I'm the main author). You'd end up with this code instead:
DateTimeZone zone = ...;
LocalDate date = ...;
LocalTime time = ...;
LocalDateTime combined = date + time;
ZonedDateTime zoned = combined.InZoneLeniently(zone);
// You can now get the "Instant", or convert to UTC, or whatever...
The "leniently" part is because when you convert local times to a specific zone, there's the possibility for the local value being invalid or ambiguous in the time zone due to DST changes.
You can also try this
var combinedLocalTime = new DateTime((dateOnly + timeOnly.TimeOfDay).Ticks,DateTimeKind.Local);
var utcTime = combinedLocalTime.ToUniversalTime();