Let's continue our C# In Simple Terms series by giving an introduction to one of the surprisingly complicated topics in the programming world: dates and times.

Photo by Behnam Norouzi / Unsplash

The Sample Project

exceptionnotfound/CSharpInSimpleTerms
Contribute to exceptionnotfound/CSharpInSimpleTerms development by creating an account on GitHub.
Project for this post: 20DatesAndTimes

A Quick Overview

This post is an introduction to the wide world of dates and times in C# and .NET. We are only going to cover a few of the basics, such as the DateTime and TimeSpan structs and the TimeZoneInfo class. There is much, much more that can be done than can be reasonably covered in a blog post. If you want some idea of the scope of things that can be done with dates and times in C#, check out the Microsoft official documentation.

Dates, times, and time zones

Dates and times, by default, rely on the set Culture of the machine the app is running on. This culture controls how dates are rendered as strings, what format it expects a date and time to exist in, and more. We discussed Cultures in the previous post.

With that out of the way, let's get into it!

DateTime

The DateTime struct represents a single moment in time. This is typically represented by a date and a time. Each of the following can be represented by an instance of DateTime:

15th June 1215
28th January 1986 1139 Hours
18th February 1930
24th October 1648
12th April 1945 3:35 PM 
Each of these dates are important historical dates; see the footnotes at the end of this post.

We can instantiate a new DateTime instance with a default value like so:

DateTime myDateTime = new DateTime(); //1 January 0001 0000 Hours (Midnight)

However, that's not very useful. What's more useful is to use overloads of DateTime's constructor to create actual dates.

DateTime fifteenthJune = new DateTime(1215, 6, 15);
var eighteenthFebruary = new DateTime(1930, 2, 18);
var twelfthApril = new DateTime(1945, 4, 12, 15, 35, 0); 

Let's look closer at that last line. To create a new DateTime instance with a specified date, we pass these parameters in this order:

  1. The year
  2. The month (1-12)
  3. The day (1-31)
  4. The hour (0-23)
  5. The minute (0-59)
  6. The second (0-59)

Using Computed Values

We can also use certain values that are computed for us by the machine our C# code is running on, such as:

var now = DateTime.Now; //The date and time now
var utcNow = DateTime.UtcNow; //The Universal Coordinated Time (UTC) value for now.
var today = DateTime.Today; //Today's date, with a time value of midnight.

Please note that, like most other so-called "primitive" types, DateTime is in fact a struct and not a class.

C# in Simple Terms - Structs and Enums
Two useful value types team up! Let’s see how structs and enums work.

DateTime Manipulation

We can manipulate instances of DateTime by calling methods defined by it, such as AddDays(), AddHours(), AddYears(), etc.

var date = DateTime.Now;
date = date.AddHours(12);
date = date.AddMonths(1);
date = date.AddDays(7);
date = date.AddMinutes(-30);

There are no methods on the DateTime struct that will "subtract" days or hours or years, so we can instead use negative numbers, like in the last line of the above sample.

Further, methods on the DateTime struct do not modify the value of the struct instance; rather they return a new instance with the modified value.

Each of these methods returns a new instance of DateTime with the changes applied.

Displaying Dates and Times

When we want to display the value of a DateTime instance, we normally need to convert it to a string first. There are several ways of doing this. The most basic is to call the ToString(), which uses the machine's culture to produce a "standard" string date and time.

Console.WriteLine(DateTime.Now.ToString()); //10/26/2020 1:21:27 PM

If we want to display a date and time in a different format, we can use an overload of ToString() along with date and time format strings.

Format Strings

Microsoft defines a bunch of string placeholders that we can use to create custom date and time format strings. A few of these placeholders include:

  • dd for the day of the month and dddd for the day name (e.g. Monday).
  • M for the number of the month (e.g. "1", "11"), MM for the number of the month with a leading zero (e.g. "01", "11"), MMM for the month's abbreviation (e.g. "Jan"), and MMMM for the entire month name (e.g. January).
  • h for the hour (e.g. "7", "12") and hh for the hour with a leading zero (e.g. "07", "12").
  • gg for the era (e.g. "A.D.")

Using these placeholders, some of the formats we can generate might include:

"MM/dd/yyyy" - 10/24/2020
"dd MMMM yyyy" - e.g. 24th October 2020
"dddd, dd MMM yyyy h:mm:ss gg" - Saturday, 24 Oct 2020 7:59:32 A.D.

When we want to convert a date using one of these formats, we call ToString() like so.

Console.WriteLine(DateTime.Now.ToString("dddd, dd MM yyyy h:mm:ss gg"));

More information on the various formats we can use is available on Microsoft's official C# documentation.

Custom date and time format strings
Learn to use custom date and time format strings to convert DateTime or DateTimeOffset values into text representations, or to parse strings for dates & times.

Shorthand Methods

There are a few shorthand methods on the DateTime struct that we can use instead of specifying an entire format.

var display = DateTime.Now.ToShortDateString();
Console.WriteLine(display); //10/26/2020

var displayTime = DateTime.Now.ToShortTimeString();
Console.WriteLine(displayTime); //1:51 PM

var longDisplay = DateTime.Now.ToLongDateString();
Console.WriteLine(longDisplay); //Monday, October 26th, 2020

var longDisplayTime = DateTime.Now.ToLongTimeString();
Console.WriteLine(longDisplayTime); //1:51:13 PM

All of these methods will use the current culture to generate these strings.

Parsing a String to a DateTime

There are multiple ways to parse a string into an instance of DateTime, and which one we use depends on what the original string is and whether or not we want to parse using a specific format.

Parse()

The DateTime.Parse() method will attempt to read a string and convert it to an instance of DateTime. Note that this method will use the current culture of the machine it is running one.

var date = DateTime.Parse("10/24/2020 05:50AM");
My culture is en-US, so my code examples will use American-style dates like MM/dd/yyyy.

If the string cannot be parsed to an instance of DateTime, the Parse() method will throw an exception.

TryParse()

If we do not wish for the parsing method to throw an exception if it fails, we can use the DateTime.TryParse() method, which returns a bool for whether or not the string can be parsed and uses an out parameter for the actual DateTime instance.

DateTime parsedDate;
bool isParsable = DateTime.TryParse("5/4/2012 11:30", out parsedDate);
Console.WriteLine(parsedDate.ToString());

ParseExact() and TryParseExact()

Both parse methods also have sibling methods, ParseExact() and TryParseExact(). These methods will attempt to parse a string using a given string format. If the string does not match the given format exactly, the parsing will fail.

string format = "ddd MM/dd/yyyy h:mm:ss";
parsedDate = DateTime.ParseExact("Mon 10/26/2020 9:15:45", format, null);
Console.WriteLine(parsedDate);

TimeSpan

The TimeSpan struct represents a duration of time, whereas DateTime represents a single point in time. Instances of TimeSpan can be expressed in seconds, minutes, hours, or days, and can be either negative or positive.

We can create a default instance of TimeSpan using the parameterless constructor; the value of such an instance is TimeSpan.Zero.

TimeSpan newTimeSpan = new TimeSpan();
Console.WriteLine(newTimeSpan);

Alternately, we can instantiate a new TimeSpan by passing the time duration that it represents.

//7 days, 8 hours, 10 minutes, and 35 seconds
TimeSpan newTimeSpan = new TimeSpan(7, 8, 10, 35);
Console.WriteLine(newTimeSpan); //7.08:10:35

Calculations with DateTime and TimeSpan

A subtraction operation with two DateTime instances result in a TimeSpan instance. For example, if we know the start time and date and the end time and date of a train journey, we can calculate the duration between them.

DateTime startDate = new DateTime(2020, 11, 10, 9, 35, 0);
DateTime endDate = new DateTime(2020, 11, 14, 15, 10, 20);

TimeSpan duration = endDate - startDate;
Console.WriteLine(duration); //4.05:35:20

We can also use the DateTime.Add() and DateTime.Subtract() methods to add or subtract TimeSpan durations from DateTime instances.

DateTime initialDate = DateTime.Now;
var newDate = initialDate.Add(duration);
Console.WriteLine(newDate);

newDate = newDate.Subtract(duration);
Console.WriteLine(newDate); //Now

Because TimeSpan instances can be positive or negative, C# includes the Duration() and Negate() methods, which can be used to get the absolute value and negative value, respectively.

TimeSpan negative = new TimeSpan(-4, 30, 12);
Console.WriteLine(negative);
Console.WriteLine(negative.Duration()); //Positive value
Console.WriteLine(negative.Negate()); //Positive value; negating a negative value results in a positive value.

TimeZoneInfo

C# and the .NET Framework can represent Earth's time zones using the TimeZoneInfo class.

We normally instantiate the TimeZoneInfo class by using the TimeZoneInfo.FindSystemTimeZoneById() method and passing in the name of the time zone. For example, here's how I would get my home time zone.

TimeZoneInfo mountainStandard = TimeZoneInfo.FindSystemTimeZoneById("Mountain Standard Time");
Console.WriteLine(mountainStandard); //(UTC-07:00) Mountain Time (US & Canada)

Time zones are defined by the system running the C# application. We can display all time zones defined on the current system in the command line by running the following code:

var allTimeZones = TimeZoneInfo.GetSystemTimeZones();
foreach(var timeZone in allTimeZones)
{
    Console.WriteLine(timeZone);
}

There are many more things we can do with time zones, such as instantiating adjustment rules for daylight savings time. Check out the official Microsoft documentation for more.

Summary

The DateTime struct represents a single point in time. We can instantiate it using years, months, days, and so on. We manipulate DateTime values by calling methods such as AddDays(), which returns a new instance of DateTime.

We can use format strings such as "MM/dd/yyyy" both to display instances of DateTime as strings and to parse new DateTime values from strings. For simple outputs, we can use shorthand methods such as GetShortDateString().

Parsing a string to a DateTime value can be done either using a specific format with ParseExact() or TryParseExact() or using the machine's default culture with Parse() and TryParse().

The TimeSpan struct represents a duration of time, expressed in days, hours, minutes, and seconds, and can be either positive or negative. We can subtract two instances of DateTime to get a TimeSpan value of the duration between them, and we can add TimeSpan instances to a DateTime to get a new DateTime value.

Finally, the TimeZoneInfo class represents time zones. We can find an individual time zone using FindSystemTimeZoneByID(), or get all system time zones using GetSystemTimeZones().

In the next post of this series, we'll take a look at a cool but often-underused feature of C#: indexers.

Happy Coding!

Footnotes

15th June 1215 is the date that the Magna Carta was signed.
28th January 1986 at 1139 Hours is the date and time of the Space Shuttle Challenger disaster.
18th February 1930 is the day that Pluto was discovered.
24th October 1648 is the date of the signing of the Peace of Westphalia.
12th April 1945 3:35 PM is the date and time of Franklin Delano Roosevelt's death.