Luna Tech

Tutorials For Dummies.

All About Dates

2021-04-24



Note

Slides can be downloaded from here

LINQPad queries can be downloaded from here


Agenda

  1. Date Components

  2. Common Date Operations (C#)

  3. Best Practice

  4. Wrap up


1. Date Components

What day is it today?

What time is it now?

Which timezone are we using?

What is the current time zone offset?


1. Date Components

What day is it today?

2021.04.23

What time is it now?

3:15 pm

Which timezone are we using?

AEST

What is the current time zone offset?

+10:00

Depending on your current location, the answer may vary.

Your laptop probably is using your local time zone.


1.1 Date Terminologies

2021.04.16 - Date

2021-04-16 15:15:00 - DateTime

3:15 pm - Time

00:00:00 - Midnight

AEST - Time Zone

+10:00 - Offset

GMT, UTC, Universal Time - Offset = 00:00

3 hours 20 minutes - Elapsed Time (Duration, can be more than 24 hours)


Standards

ISO-8601 standard is commonly used nowadays.

1970.01.01 is the date of the Unix Epoch, anything from 1970 onward is much more reliable.

ISO-8601 standard Simple Format (YYYY-MM-DD) is recommended for Date Format (Avoid Ambiguity)


How Date and Time is stored in Computers


1.2 Time Zone & Offset

Time Zone != Offset

1 Time Zone can have different offsets.

1 offset can have multiple time zones

Essentially, Time Zone and Offset has a Many-to-Many Relationship.

Note: Time Zone names are not standardized, IANA time zone database ) might be the most commonly used one (not supported in native .NET).


1.3 Daylight Saving Time

Spring Forward Transition

Fall Back Transition


1.4 Midnight is not reliable

We tend to use Date + Midnight time (00:00:00) to represent a Local DateTime, but midnight is not always available in every time zone on every day.

Hint: DST transition time

Solution 1: Consider the scope (Date only? Date and Time? Duration?)

Solution 2: UTC midnight is always available


1.5 Whose Time Should We Use?

We need to consider this whenever we read, save, convert, calculate and compare date in our application.

User Time

Server Time


Solution

There’s nothing worse than a computer making assumptions on behalf of you.

Especially for Dates…


Speicial considerations

  1. Leap Year (366 days)
  2. February Date (29 in leap years)
  3. Financial Year (not always 01-01)
  4. ….

2. Common Date Operations (C#)

Structs in System namespace

  1. DateTime (Date + Time)
  2. DateTimeOffset (Date + Time + Offset)
  3. TimeSpan (represent time interval or a time of the day)

Related Classes

Nonexistent Types


2.2 Which Now are you talking about?

Local Now

DateTime dtnow = DateTime.Now; // 2021-04-17 13:53:49
DateTimeOffset dto = DateTimeOffset.Now; // 2021-04-17 13:53:49 +10:00

Utc Now

DateTime dtUTCnow = DateTime.UtcNow; // 2021-04-17 03:54:22
DateTimeOffset dtoUTC = DateTimeOffset.UtcNow; // 2021-04-17 03:54:22 +00:00

Note: Printed DateTime format changes based on your system settings.


2.2 Which Now are you talking about?

Local Now

DateTime dtnow = DateTime.Now; // 2021-04-17 13:53:49
DateTimeOffset dto = DateTimeOffset.Now; // 2021-04-17 13:53:49 +10:00

Utc Now

DateTime dtUTCnow = DateTime.UtcNow; // 2021-04-17 03:54:22
DateTimeOffset dtoUTC = DateTimeOffset.UtcNow; // 2021-04-17 03:54:22 +00:00

Is this a Local now or a Utc Now?

2021-04-18 03:54:22

Note: DateTimeOffset always have the converted time

E.g., 2021-04-17 13:53:49 +10:00 means local time is 2021-04-17 13:53:49, UTC time needs be calculated by substracting the timezoneoffset.


2.3 Date Creation

DateTime (has Kind)

DateTime dtDefault = new DateTime(2000, 2, 3, 10, 20, 30).Dump(); // Unspecified
DateTime dtLocal = new DateTime(2000, 2, 3, 10, 20, 30, DateTimeKind.Local).Dump(); // Local
DateTime dtUtc = new DateTime(2000, 2, 3, 10, 20, 30, DateTimeKind.Utc).Dump(); // Utc

DateTimeOffset

TimeSpan ts = TimeSpan.FromMinutes(90).Dump(); // offset +01:30
DateTimeOffset dto = new DateTimeOffset(2000, 2, 3, 10, 20, 30, ts).Dump();

Question:

  1. Does DateTimeOffset have Kind?
  2. Why or why not?

DateTimeKind

  1. Unspecified (default for date creation)
  2. Local (DateTime.Now)
  3. Utc (DateTime.UtcNow)

Update Kind

DateTime dtDefault = new DateTime(2000, 2, 3, 10, 20, 30).Dump(); // Unspecified
dtDefault = DateTime.SpecifyKind(dtDefault, DateTimeKind.Utc);
dtDefault.Kind.Dump(); // Utc

Get DateTime from DateTimeOffset with Kind

DateTimeOffset dtoUTC = DateTimeOffset.UtcNow.Dump();
dtoUTC.UtcDateTime.Kind.Dump(); // Utc
dtoUTC.DateTime.Kind.Dump(); // Unspecified (AVOID using this for UTC time)

Question

Given a DateTime object without knowing its created timezone, you want to convert it to a DateTimeOffset object with timezone = Utc, what would you do?

DateTime timezoneUnknown = new DateTime(2000, 2, 3, 10, 20, 30).Dump();
// Todo: Create a DateTimeOffset object with timezoneoffset = 0

Question

Given a DateTime object without knowing its created timezone, you want to convert it to a DateTimeOffset object with timezone = Utc, what would you do?


// Convert DateTime to DateTimeOffset with timezone = utc
// orginal date: 2000-02-03 10:20:30
DateTime timezoneUnknown = new DateTime(2000, 2, 3, 10, 20, 30).Dump();

// 1. set Kind to utc
timezoneUnknown = DateTime.SpecifyKind(timezoneUnknown, DateTimeKind.Utc);

// 2. cast it to a DateTimeOffset object
DateTimeOffset dtoUtcTime = (DateTimeOffset)timezoneUnknown;
dtoUtcTime.Dump(); // 2000-02-03 10:20:30 +00:00

Another way of doing this is to use a method ToUniversalTime(), but we still need to consider the kind.

DateTime timezoneUnknown2 = new DateTime(2000, 2, 3, 10, 20, 30).Dump();
timezoneUnknown2.ToUniversalTime().Dump(); // 2000-02-02 23:20:30

timezoneUnknown2 = DateTime.SpecifyKind(timezoneUnknown2, DateTimeKind.Utc);
timezoneUnknown2.ToUniversalTime().Dump(); // 2000-02-03 10:20:30

DateTimeOffset dtoUtcTime2 = (DateTimeOffset)timezoneUnknown2;
dtoUtcTime2.Dump(); // 2000-02-03 10:20:30 +00:00

Question

Given a DateTime object, you know its created timezone is UTC+8, you want to convert it to a DateTimeOffset object with its correct timezone, what would you do?

DateTime timezoneUnknown = new DateTime(2000, 2, 3, 10, 20, 30).Dump();
// Todo: Create a DateTimeOffset object with timezoneoffset = 8

Question

Given a DateTime object, you know its created timezone is UTC+8, you want to convert it to a DateTimeOffset object with its correct timezone, what would you do?

DateTime timezoneAny = new DateTime(2000, 2, 3, 10, 20, 30).Dump();
try
{
	DateTimeOffset dtoAny = new DateTimeOffset(timezoneAny,
				   TimeZoneInfo.FindSystemTimeZoneById("China Standard Time").GetUtcOffset(timezoneAny));
	dtoAny.Dump();
}
// Handle exception if time zone is not defined in registry
catch (TimeZoneNotFoundException)
{
	Console.WriteLine("Unable to identify target time zone for conversion.");
}

Question

Given a DateTime object, you know its created timezone is UTC+8, you want to convert it to a DateTimeOffset object with its correct timezone, what would you do?

DateTime timezoneAny = new DateTime(2000, 2, 3, 10, 20, 30).Dump();
try
{
	DateTimeOffset dtoAny = new DateTimeOffset(timezoneAny,
				   TimeZoneInfo.FindSystemTimeZoneById("China Standard Time").GetUtcOffset(timezoneAny));
	dtoAny.Dump();
}
// Handle exception if time zone is not defined in registry
catch (TimeZoneNotFoundException)
{
	Console.WriteLine("Unable to identify target time zone for conversion.");
}

But….Is this really correct?


TimeZone and TimeZoneOffset

Recall: There’s a many-to-many relationship between them.

What we’ve learnt


2.4 TimeZone Conversion

Common Scenarios:

  1. Convert a DateTime to a DateTimeOffset (mentioned above);
  2. Convert a DateTimeOffset to a DateTime;

Convert a DateTimeOffset to a DateTime

Solution 1: Directly get DateTime from DateTimeOffset

DateTimeOffset dtoLocal = DateTimeOffset.Now.Dump("Local DateTimeOffset");
DateTime convertedDateTime = dtoLocal.DateTime.Dump("Local DateTime");
convertedDateTime.Kind.Dump("Kind"); // Unspecified

DateTimeOffset dtoUtc = DateTimeOffset.UtcNow.Dump("Utc DateTimeOffset");
DateTime convertedDateTime2 = dtoUtc.DateTime.Dump("Utc DateTime");
convertedDateTime2.Kind.Dump("Kind"); // Unspecified

DateTime timezoneAny = new DateTime(2000, 2, 3, 10, 20, 30).Dump("Any TimeZone DateTime");
DateTimeOffset dtoAny = new DateTimeOffset(timezoneAny,
			   TimeZoneInfo.FindSystemTimeZoneById("China Standard Time").GetUtcOffset(timezoneAny)).Dump("Any TimeZone DateTimeOffset");
DateTime convertedDateTime3 = dtoAny.DateTime.Dump("Any TimeZone DateTime");
convertedDateTime3.Kind.Dump("Kind"); // Unspecified

Convert a DateTimeOffset to a DateTime

Solution 1: Directly get DateTime from DateTimeOffset

Problem:

We will lose the Kind information during conversion from DateTimeOffset to DateTime.


Convert a DateTimeOffset to a DateTime

Solution 2: Convert to UtcDateTime and then get Local DateTime

// Convert to UtcDateTime first
DateTimeOffset dtoLocal2 = DateTimeOffset.Now.Dump("Local DateTimeOffset");
DateTime convertedDateTimeUtc = dtoLocal2.ToUniversalTime().UtcDateTime.Dump("Utc DateTime");
convertedDateTimeUtc.Kind.Dump("Kind"); // Utc

// Local DateTime, don't forget to change kind!!!
DateTime convertedDateTimeLocal = convertedDateTimeUtc.AddHours(8).Dump();
convertedDateTimeLocal.Kind.Dump("Kind"); // Utc
convertedDateTimeLocal = DateTime.SpecifyKind(convertedDateTimeLocal, DateTimeKind.Local).Dump();
convertedDateTimeLocal.Kind.Dump("Kind"); // Local

// another way, convert to a different timezone
DateTime anyLocalDateTime = TimeZoneInfo.ConvertTime(convertedDateTimeUtc, TimeZoneInfo.FindSystemTimeZoneById("China Standard Time")).Dump("Another TimeZone DateTime");
anyLocalDateTime.Kind.Dump("Kind"); // Unspecified
anyLocalDateTime = DateTime.SpecifyKind(convertedDateTimeLocal, DateTimeKind.Local).Dump();
anyLocalDateTime.Kind.Dump("Kind"); // Local

Convert a DateTimeOffset to a DateTime

Solution 2: Convert to UtcDateTime and then get Local DateTime

This is a much better way of doing conversion because we get back a DateTime with Kind.

Problem:

What does local mean? Does it mean your current machine time or the timezoneoffset you are adding?

// Don't use these 2 methods
convertedDateTimeLocal.ToUniversalTime().Dump("To UTC (Local kind)");
convertedDateTimeLocal.ToLocalTime().Dump("To Local (Local kind)");

anyLocalDateTime.ToUniversalTime().Dump("To UTC (Local kind)");
anyLocalDateTime.ToLocalTime().Dump("To Local (Local kind)");

Convert a DateTimeOffset to a DateTime

Solution 3 (Perferred): get local DateTimeOffset object, then get local DateTime


Convert a DateTimeOffset to a DateTime

Solution 3 (Perferred): get local DateTimeOffset object, then get local DateTime


// UtcDateTime first
DateTimeOffset dtoUtcNow = DateTimeOffset.UtcNow.Dump("Utc DateTimeOffset");
DateTime dtoUtcDateTime = dtoUtcNow.UtcDateTime.Dump("Utc DateTime");
dtoUtcDateTime.Kind.Dump("Kind"); // Utc

// convert to local DateTimeOffset
DateTimeOffset localDateTimeOffset = TimeZoneInfo.ConvertTime(dtoUtcNow, TimeZoneInfo.FindSystemTimeZoneById("China Standard Time")).Dump("Another TimeZone DateTime");
localDateTimeOffset.DateTime.Dump("The best way to get Local DateTime!!!");

ToUniversalTime and ToLocalTime

Kind plays an important role in the conversion.

DateTime timezoneUnknown = new DateTime(2000, 2, 3, 10, 20, 30).Dump("Original DateTime");
timezoneUnknown.ToUniversalTime().Dump("To UTC (Unspecified Kind)");
timezoneUnknown.ToLocalTime().Dump("To Local (Unspecified Kind)");

timezoneUnknown = DateTime.SpecifyKind(timezoneUnknown, DateTimeKind.Local);
timezoneUnknown.ToUniversalTime().Dump("To UTC (Local kind)");
timezoneUnknown.ToLocalTime().Dump("To Local (Local kind)");

timezoneUnknown = DateTime.SpecifyKind(timezoneUnknown, DateTimeKind.Utc);
timezoneUnknown.ToUniversalTime().Dump("To UTC (UTC kind)");
timezoneUnknown.ToLocalTime().Dump("To Local (UTC kind)");

ToUniversalTime and ToLocalTime

  1. If kind = Unspecified (conversion could be wrong)

    • ToUniversalTime will substract current timezoneoffset
    • ToLocalTime will add current timezoneoffset (treating the original DateTime as UTC)
  2. If kind = Local (conversion is correct)

    • ToUniversalTime will substract current timezoneoffset
    • ToLocalTime will remain the same
  3. If kind = Utc (conversion is correct)

    • ToUniversalTime will remain the same
    • ToLocalTime will add current timezoneoffset

Best Practice


2.5 DateTime(Offset) & String Conversion

Common Scenarios:

  1. Format a DateTime object to a string in a specified format;
  2. Format a DateTimeOffset object to a string in a specified format;
  3. Parse a Date string and convert it to a DateTime object;
  4. Parse a Date string and convert it to a DateTimeOffset object;

Format (convert Date to string)

  1. To avoid the dependency of system date format, always use ToString and pass in the format you want;
  2. s and o are shorthand formats that you can use;
  3. UTC time is represented with a “Z” at the end in ISO8601 standard, not “+00:00”, o works for DateTime, not DateTimeOffset;
  4. DateTime won’t have any timezone info (even with Kind = Local), but o format will add a Z at the end of the string;

DateTime format


// DateTime
DateTime dtnow = DateTime.Now.Dump();
DateTime dtUTCnow = DateTime.UtcNow.Dump();

dtnow.ToLongDateString().Dump("ToLongDateString");
dtnow.ToLongTimeString().Dump("ToLongTimeString");
dtnow.ToOADate().Dump("ToOADate");
dtnow.ToShortDateString().Dump("ToShortDateString");
dtnow.ToShortTimeString().Dump("ToShortTimeString");
dtnow.ToString().Dump("ToString Default");
dtnow.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture).Dump("ToString custom format");
dtnow.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture).Dump("ToString custom format"); // this can be wrong!!!! it's actually local time
dtnow.ToString("o").Dump("ToString o");
dtnow.ToString("s").Dump("ToString s");
//dtnow.GetDateTimeFormats().Dump("GetDateTimeFormats"); // all supported formats

dtUTCnow.ToString("o").Dump("UtcDateTime - o");
dtUTCnow.ToString("s").Dump("UtcDateTime - s");

DateTimeOffset format


// DateTimeOffset
DateTimeOffset dto = DateTimeOffset.Now.Dump();
DateTimeOffset dtoUTC = DateTimeOffset.UtcNow.Dump();

dto.ToString("o").Dump("DateTimeOffset - Local o");
dto.ToString("s").Dump("DateTimeOffset - Local s");
dto.ToString("yyyy-MM-ddTHH:mm:sszzz").Dump("DateTimeOffset - Local custom");

dtoUTC.ToString("o").Dump("DateTimeOffset - UTC o");
dtoUTC.ToString("s").Dump("DateTimeOffset - UTC s");
dtoUTC.ToString("yyyy-MM-ddTHH:mm:sszzz").Dump("DateTimeOffset - UTC custom");
dtoUTC.ToString("yyyy-MM-ddTHH:mm:ssZ").Dump("DateTimeOffset - UTC custom");

Date Formatting Best Practice - use “o” (character o, not number 0)

DateTime dtnow = DateTime.Now.Dump();
DateTime dtUTCnow = DateTime.UtcNow.Dump();
DateTimeOffset dto = DateTimeOffset.Now.Dump();
DateTimeOffset dtoUTC = DateTimeOffset.UtcNow.Dump();

// 2021-04-17T20:22:53.0631934+10:00
dtnow.ToString("o").Dump("DateTime Local - use o");

// 2021-04-17T10:22:53.0634756Z
dtUTCnow.ToString("o").Dump("DateTime UTC - use o");

// 2021-04-17T20:22:53.0641225+10:00
dto.ToString("o").Dump("DateTimeOffset Local - use o");

// 2021-04-17T10:22:53.0641638+00:00
dtoUTC.ToString("o").Dump("DateTimeOffset Utc - use o");

// 2021-04-17T10:22:53.0641638Z
dtoUTC.UtcDateTime.ToString("o").Dump("DateTimeOffset Utc - convert and use o");

Parsing (convert string to Date)

Both DateTime and DateTimeOffset parse have 4 variations:

  1. Parse
  2. TryParse
  3. ParseExact
  4. TryParseExact

Note: Each method also has multiple overloads, the chart is simplified.

Method Error Handling Format Provider DateTimeStyle
Parse N N N N
ParseExact N Y Optional N
TryParse Y N N N
TryParseExact Y Y Optional Y

Parsing

DateTimeOffset is similar, we’ll omit the example code for that..

// Local Now
DateTime dtnow = DateTime.Now.Dump("DateTime Local Now");
CultureInfo provider = CultureInfo.InvariantCulture;

string customStringToParse = "2021-01-01".Dump("Custom String - input");
DateTime customStringParseResult = DateTime.Parse(customStringToParse);

DateTime parseResultSuccess;
DateTime parseExactResultSuccess;
DateTime.ParseExact(customStringToParse, "yyyy-mm-dd", provider).Dump("DateTime ParseExact - valid DateTime string");
DateTime.TryParse(customStringToParse, out parseResultSuccess).Dump("DateTime TryParse - valid DateTime string");
DateTime.TryParseExact(customStringToParse, "yyyy-mm-dd", provider, DateTimeStyles.None, out parseExactResultSuccess).Dump("DateTime TryParseExact - valid DateTime string");

Click for more format information


Date comparison

Common Scenarios:

  1. Compare Dates
  2. Compare DateTime
  3. Compare DateTimeOffset

To compare, we need to first make sure d1 and d2 are in the same timezone, usually we convert them to UTC time.


Date Comparison - DateTime

The last one will produce a wrong result, cos we are not comparing 2 utc datetime.

DateTime utcNow = DateTime.UtcNow.Dump();
DateTime utcOneHourBefore = DateTime.UtcNow.AddHours(-1).Dump();
DateTime localOneHourBefore = DateTime.Now.AddHours(-1).Dump();

(utcOneHourBefore.ToUniversalTime() > utcNow).Dump("utcOneHourBefore > utcNow");
(utcOneHourBefore > utcNow).Dump("utcOneHourBefore > utcNow");

(localOneHourBefore.ToUniversalTime() > utcNow).Dump("localOneHourBefore > utcNow");
(localOneHourBefore > utcNow).Dump("localOneHourBefore > utcNow");

Date Comparison - DateTimeOffset

DateTimeOffset doesn’t have this problem, since it has the offset information.

DateTimeOffset utcNow = DateTimeOffset.UtcNow.Dump();
DateTimeOffset utcOneHourBefore = DateTimeOffset.UtcNow.AddHours(-1).Dump();
DateTimeOffset localOneHourBefore = DateTimeOffset.Now.AddHours(-1).Dump();

(utcOneHourBefore.ToUniversalTime() > utcNow).Dump("utcOneHourBefore > utcNow");
(utcOneHourBefore > utcNow).Dump("utcOneHourBefore > utcNow");

(localOneHourBefore.ToUniversalTime() > utcNow).Dump("localOneHourBefore > utcNow");
(localOneHourBefore > utcNow).Dump("localOneHourBefore > utcNow");

3. Best Practice

  1. Understand the difference between timezone and timezoneoffset

  2. Daylight Saving Time is complicated but you don’t need to remember or hardcode the rules

  3. Avoid using local DateTime in your db and application internal logic, utc datetimeoffset is the best option

  4. Do not compare dates in different timezone

  5. Use “o” or “yyyy-MM-ddTHH:mm:sszzz” to format date string

  6. Be careful when you are talking about today, next week, yesterday.. ask yourself, whose day is it?

A nice course to watch - Date and Time Fundamentals


4. Wrap up

  1. Date Components

    • terminologies
    • standards
    • storage and precision
    • daylight saving time (DST)
    • don’t trust local midnight time
    • user time vs server time
  2. Common Date Operations

    • C# Date Related Types
    • Different Now (Local & Utc)
    • Create a Date properly
    • DateTimeKind (the complicated part)
    • TimeZone Conversion (not that easy)
    • ToUniversalTime and ToLocalTime (better not use if Kind != Utc)
    • DateTime and String Conversion
    • Date Comparison
  3. Best Practice