Updated on

A DateTime format string tells ToString() how to turn a date and time into text. There are two kinds. A standard specifier is a single character, such as "d", "O" or "u", that stands for a whole pattern the culture supplies. A custom format string is anything longer, such as "yyyy-MM-dd HH:mm:ss", spelled out one piece at a time.

The same strings work in the other direction. DateTime.ParseExact() takes a format string and reads text back into a DateTime, which is why the two halves of this article apply to parsing a string back into a DateTime as well as to printing.

A DateTime value and a text string connected by two arrows, ToString going one way and ParseExact the other, both labelled with the same format string.

Culture decides what a standard specifier produces. "d" is 8/24/2017 in en-US and 2017/08/24 in ja-JP, so a format that has to look the same on every machine needs either an explicit CultureInfo or one of the culture-independent specifiers: "O", "R" and "s".

To download the source code for this article, you can visit our GitHub repository.

Let’s start.

What Are the Standard DateTime Format Specifiers in C#?

A standard DateTime format specifier is a single character passed to ToString() or to a parsing method, and it stands for a whole pattern that the culture supplies.

There are fifteen of them, in three groups. Culture-driven patterns: "d" and "D" for short and long date, "t" and "T" for short and long time, "f", "F", "g" and "G" for the four date-plus-time combinations, "M" for month and day, "Y" for year and month.

Fixed patterns that ignore the culture entirely: "O" for the round-trip ISO 8601 form, "R" for RFC 1123, "s" for the sortable ISO form, "u" for the universal sortable form.

And one that mixes both: "U" converts the value to UTC and then formats it with the culture’s full date and time pattern.

A single character that is not one of the fifteen throws a FormatException rather than being read as a custom format string.

Let’s format a value with the short date specifier:

var datetime = new DateTime(2017, 8, 24);
Console.WriteLine(datetime.ToString("d")); // 8/24/2017

This formatting operation uses the default or current culture. The output from our example corresponds to US English.

Now, let’s see the complete list of standard format specifiers that formatting operations in C# support:

Format SpecifierDescriptionRepresentation (US English)Culture-sensitive?
dShort date8/24/2017Yes
DLong dateThursday, August 24, 2017Yes
fFull date/time (short time)Thursday, August 24, 2017 2:35 PMYes
FFull date/time (long time)Thursday, August 24, 2017 2:35:30 PMYes
gGeneral date/time (short time)8/24/2017 2:35 PMYes
GGeneral date/time (long time)8/24/2017 2:35:30 PMYes
M, mMonth/dayAugust 24Yes
O, oRound-trip date/time2017-08-24T14:35:30.0000000ZNo
R, rRFC1123Thu, 24 Aug 2017 14:35:30 GMTNo
sSortable date/time2017-08-24T14:35:30No
tShort time2:35 PMYes
TLong time2:35:30 PMYes
uUniversal sortable date/time2017-08-24 14:35:30ZNo
UUniversal full date/timeThursday, August 24, 2017 2:35:30 PMYes
Y, yYear monthAugust 2017Yes

Standard Formats and Culture

Standard format specifiers are useful because the same format specifier will throw different representations of a date and time value depending on the culture we use in the operation:

var datetime = new DateTime(2017, 8, 24);
Console.WriteLine(datetime.ToString("d", new CultureInfo("en-US"))); // 8/24/2017
Console.WriteLine(datetime.ToString("d", new CultureInfo("es-ES"))); // 24/8/2017
Console.WriteLine(datetime.ToString("d", new CultureInfo("ja-JP"))); // 2017/08/24

Here, we pass a culture info specification as the value of the IFormatProvider parameter of the ToString method.

We use the US English date format that corresponds to the MM/dd/yyyy custom format string. Then the Spanish culture setting with the dd/MM/yyyy format representation. And lastly, we use the Japanese standard short date format, yyyy/MM/dd.

As an alternative to CultureInfo, we can use a more customizable DateTimeFormatInfo object:

var datetime = new DateTime(2017, 8, 24);
var formatInfo = new CultureInfo("en-US").DateTimeFormat;
formatInfo.DateSeparator = "-";

Console.WriteLine(datetime.ToString("d", formatInfo)); // 8-24-2017

First, we access the DateTimeFormat property of DateTimeFormatInfo type associated with the American English culture and store it in the formatInfo variable. After that, we customize it by setting the date separator to the hyphen character - instead of the culture default forward slash /. Finally, we use DateTimeFormat along with the short date standard format specifier to print a DateTime value to the console.

The same format strings work in reverse, and our guide on converting a string to a DateTime in C# covers the parsing side in full:

var dateLiteral = "8/24/2017";
var date = DateTime.ParseExact(dateLiteral, "d", new CultureInfo("en-US"));

Date Format Specifiers

We can use the short date format specifier d to show the short date value in combination with a specific culture that we use:

Console.WriteLine(datetime.ToString("d", CultureInfo.CreateSpecificCulture("en-US"))); // 8/24/2017

With the long date format specifier D we can show the long date format:

Console.WriteLine(datetime.ToString("D", CultureInfo.CreateSpecificCulture("en-US"))); // Thursday, August 24, 2017

We can find an additional implementation of Spanish and Japanese cultures in our source code.

Date and Time Format Specifiers

The full date short time format specifier f is a combination of the long date and short time standard formats:

Console.WriteLine(datetime.ToString("f", CultureInfo.CreateSpecificCulture("en-US")));

The output:

Thursday, August 24, 2017 2:35 PM

With the full date long time format specifier F we can show a combination of the long date and long time standard formats:

Console.WriteLine(datetime.ToString("F", CultureInfo.CreateSpecificCulture("en-US")));

And this is the output:

Thursday, August 24, 2017 2:35:00 PM

We can use the general date short time format specifier g to show a combination of short date and short time patterns:

Console.WriteLine(datetime.ToString("g", CultureInfo.CreateSpecificCulture("en-US"))); // 8/24/2017 2:35 PM

The general date long time format specifier G shows a value that combines the short date pattern and the long time patterns:

Console.WriteLine(datetime.ToString("G", CultureInfo.CreateSpecificCulture("en-US"))); // 8/24/2017 2:35:00 PM

With the round-trip format specifier O or o we can show an ISO 8601 representation. This formatting preserves time zone information:

var dateTimeUtc = new DateTime(2017, 8, 24, 14, 35, 0, DateTimeKind.Utc);
Console.WriteLine(dateTimeUtc.ToString("O")); // 2017-08-24T14:35:00.0000000Z

var dateTimeOffset = new DateTimeOffset(2017, 8, 24, 14, 35, 0, TimeSpan.FromHours(2));
Console.WriteLine(dateTimeOffset.ToString("O")); // 2017-08-24T14:35:00.0000000+02:00

Here, we print a formatted version of a DateTime value. This DateTime value has zero offset since its kind is UTC. Then, we do the same with a DateTimeOffset value.

Note that our UTC DateTime representation shows the fact that it has zero offset by adding a capital Z to the end of the string.

The DateTimeOffset value represents the offset time value at the end. In this case, +02:00 means 2 hours over UTC.

The RFC1123 Format Specifier

With the format specifier R or r we can show a value that follows the RFC1123 standard.

RFC1123 always represents the time in UTC format. However, for DateTime values, we will need to make sure that we are providing a UTC value to the formatting operation. The easier way to do that is by calling the DateTime.ToUniversalTime() method.

For DateTimeOffset values, it’s easier since the conversion to universal time is performed automatically:

var datetime = new DateTime(2017, 8, 24, 14, 35, 0, DateTimeKind.Unspecified);
Console.WriteLine(datetime.ToUniversalTime().ToString("R")); // Thu, 24 Aug 2017 14:35:00 GMT

var datetimeOffset = new DateTimeOffset(2017, 8, 24, 14, 35, 0, TimeSpan.FromHours(2));
Console.WriteLine(datetimeOffset.ToString("R")); // Thu, 24 Aug 2017 12:35:00 GMT

ToUniversalTime() on an Unspecified value treats it as local time, so the first result depends on the machine’s time zone. The line above is what a machine running on UTC prints.

Sortable and Full Format Specifiers

We can use the sortable format specifier s to show a value that complies with the ISO 8601 standard and, as the name suggests, sorts correctly according to their date and time values. This format doesn’t represent time offset information:

Console.WriteLine(datetime.ToString("s")); // 2017-08-24T14:35:00

With the universal sortable format specifier u we can generate a value that complies with the ISO 8601 standard and produces sortable string representation. This format only works with universal date and time values. Therefore, we need to make sure we are using a UTC value, usually, by calling DateTime.ToUniversalTime():

Console.WriteLine(datetime.ToUniversalTime().ToString("u")); // 2017-08-24 14:35:00Z

Note that the zero offset marker Z at the end of the string will appear regardless of the DateTime.Kind property of the value used. That’s why it is on us to explicitly convert the value to UTC.

In contrast, for DateTimeOffset values, this is done automatically.

Lastly, using the universal full format specifier U we can produce a value that will be identical to the full date and time pattern described before. However, the difference is that the DateTime value used is automatically converted to UTC before applying the format operation.

Note that the universal full format specifier only works with DateTime values. Any attempt to use the U standard format on a DateTimeOffset value with throw a FormatException.

Let’s see how to format a DateTime value using this specifier:

var datetime = new DateTime(2017, 8, 24, 14, 35, 0, DateTimeKind.Utc);
Console.WriteLine(datetime.ToUniversalTime().ToString("U", CultureInfo.CreateSpecificCulture("en-US")));

Here, we create a DateTime value specifying a DateTime.Kind UTC even though it is not strictly necessary:

Thursday, August 24, 2017 2:35:00 PM

Time Format Specifiers

With the short time format specifier t we can represent a short time value using different cultures:

Console.WriteLine(datetime.ToString("t", CultureInfo.CreateSpecificCulture("en-US"))); // 2:35 PM

Alternatively, we can use the long time format specifier T to represent a short time value that changes depending on the culture we use:

Console.WriteLine(datetime.ToString("T", CultureInfo.CreateSpecificCulture("en-US"))); // 2:35:00 PM

We can find additional implementation examples for different cultures in our source code.

How Do We Build a Custom DateTime Format String in C#?

A custom DateTime format string is anything longer than one character, assembled from specifiers that each stand for one piece of the value.

The pieces repeat a pattern. One letter gives the shortest form, two pad to two digits, and for d, M and y three or four give a name or a full year: d gives 4, dd gives 04, ddd gives Fri, and dddd gives Friday.

Case is significant everywhere. M is the month and m is the minute. H is the 24-hour clock and h the 12-hour one. f prints fractional-second digits including trailing zeros; F drops them.

Anything that is not a specifier passes through untouched, but / and : are specifiers, emitting the culture’s date and time separators. Under de-DE, "dd/MM/yyyy" prints 24.08.2017. Escape them, or pass CultureInfo.InvariantCulture.

A single-character custom format needs a % in front of it, as in "%d", or the runtime reads it as a standard specifier.

Let’s use one with a method that supports composite formatting:

Console.WriteLine("{0:MM/dd/yy H:mm:ss}", datetime); // 08/24/17 14:35:00

Any formatting or parsing operation always interprets single character format strings as standard format specifiers. Then, if the character doesn’t match any of the supported standard formats a FormatException will be thrown.

In case we want to use a single character custom format string like, for example, y for the one-digit year, we just have to precede it either with a space or a % symbol:

Console.WriteLine(datetime.ToString("%d", CultureInfo.CreateSpecificCulture("en-US"))); // 4
Console.WriteLine(datetime.ToString(" y", CultureInfo.CreateSpecificCulture("en-US")).Trim()); // 17

We see how the format string "%d" is interpreted as the custom format specifier for days. After that, we prepend a space before the character y to avoid it being interpreted as a standard format specifier.

The following table contains the complete list of custom format specifiers supported by formatting operations in C#:

Format SpecifierDescriptionRepresentation
d, ddDay of the month1, 01
ddd, ddddDay of the weekFri, Friday
M, MMMonth8, 08
MMM, MMMMName of the monthAug, August
y, yy, yyy, yyyy, yyyyyYear1, 01, 2001, 2001, 02001
h, hhHour using 12-hour clock9, 09
H, HHHour using 24-hour clock21, 21
m, mmMinutes 0-591, 01
s, ssSeconds 0-591, 01
f
ff
fff
ffff
fffff
ffffff
fffffff
Fraction of a second0
01
018
0180
01800
018000
0180000
F
FF
FFF
FFFF
FFFFF
FFFFFF
FFFFFFF
Fraction of a second if non-zero(no output)
01
018
018
018
018
018
t, ttMeridiem designator AM/PMP, PM
KTime zone+02:00, Z
z, zz, zzzHours offset from UTC+2, +02, +02:00
%g, ggEraAD (the era of the value)
:Time separatorCulture defined time separator
/Date separatorCulture defined date separator
"abc", 'abc'Literal string delimiterabc
%Following character is a custom specifier"%d" -> 4 (forces a one-character custom format)
\Escape character"\g" -> g (emits the next character literally)

Day Format Specifiers

The d specifier shows the day of the month from 1 to 31 while dd shows the day of the month from 01 to 31 with a leading zero for the single-digit days:

var datetime = new DateTime(2017, 8, 4);

Console.WriteLine(datetime.ToString("d MMMM", CultureInfo.CreateSpecificCulture("en-US"))); // 4 August
Console.WriteLine(datetime.ToString("dd MMMM", CultureInfo.CreateSpecificCulture("en-US"))); // 04 August

We can use the ddd specifier to show the localized abbreviated name of the weekday while dddd shows the full localized weekday name:

Console.WriteLine(datetime.ToString("ddd, d MMMM", new CultureInfo("en-US"))); // Fri, 4 August
Console.WriteLine(datetime.ToString("dddd, d MMMM", new CultureInfo("en-US"))); // Friday, 4 August

Month Format Specifier

The M specifier shows the month from 1 to 12 while with the MM specifier we can show the month from 01 to 12 with a leading zero for the single-digit months:

Console.WriteLine(datetime.ToString("%M", CultureInfo.CreateSpecificCulture("en-US"))); // 8
Console.WriteLine(datetime.ToString("MM", CultureInfo.CreateSpecificCulture("en-US"))); // 08

If we use the MMM specifier, we can show the localized abbreviated month name while MMMM shows the full localized month name:

Console.WriteLine(datetime.ToString("MMM", CultureInfo.CreateSpecificCulture("en-US"))); // Aug
Console.WriteLine(datetime.ToString("MMMM", CultureInfo.CreateSpecificCulture("en-US"))); // August

Year Format Specifier

We can use the y, yy, yyy and yyyy specifiers to represent the year with one to four digits:

var datetime = new DateTime(2001, 8, 4);

Console.WriteLine(datetime.ToString("yy", CultureInfo.CreateSpecificCulture("en-US"))); // 01
Console.WriteLine(datetime.ToString("yyy", CultureInfo.CreateSpecificCulture("en-US"))); // 2001
Console.WriteLine(datetime.ToString("yyyy", CultureInfo.CreateSpecificCulture("en-US"))); // 2001

Hour Format Specifier

With the h specifier, we can show the hour of the day from 1 to 12 using all relevant digits. If we use the hh specifier, we will get the hour of the day from 01 to 12 with a leading zero for the single-digit hour:

Console.WriteLine(datetime.ToString("hh", CultureInfo.CreateSpecificCulture("en-US"))); // 10

Similarly, if we use the H specifier we can show the hour of the day from 1 to 23 using all relevant digits while the HH specifier shows the hour of the day from 01 to 23 with a leading zero for the single-digit hour:

Console.WriteLine(datetime.ToString("HH", CultureInfo.CreateSpecificCulture("en-US"))); // 22

Minute Format Specifier

We can use the m specifier to show the minute of the hour from 1 to 59 using all relevant digits. Also, with the mm specifier we can show the minute of the hour from 01 to 59 with a leading zero for the single-digit hour:

Console.WriteLine(datetime.ToString("mm", CultureInfo.CreateSpecificCulture("en-US"))); // 35

Seconds Format Specifier

If we use the s specifier we can show the seconds of the minute from 1 to 59 using all relevant digits while the ss specifier shows the seconds of the minute from 01 to 59 with a leading zero for the single-digit hour:

Console.WriteLine(datetime.ToString("ss", CultureInfo.CreateSpecificCulture("en-US"))); // 15

Seconds Fraction Specifier

The f specifier shows the seconds fraction. We can use up to seven f characters smaller fractions of a second:

Console.WriteLine(datetime.ToString("HH:mm:ss.%f", new CultureInfo("en-US"))); // 22:35:15.0
Console.WriteLine(datetime.ToString("HH:mm:ss.ff", new CultureInfo("en-US"))); // 22:35:15.01
Console.WriteLine(datetime.ToString("HH:mm:ss.fff", new CultureInfo("en-US"))); // 22:35:15.018
Console.WriteLine(datetime.ToString("HH:mm:ss.ffff", new CultureInfo("en-US"))); // 22:35:15.0180
Console.WriteLine(datetime.ToString("HH:mm:ss.fffff", new CultureInfo("en-US"))); // 22:35:15.01800
Console.WriteLine(datetime.ToString("HH:mm:ss.ffffff", new CultureInfo("en-US"))); // 22:35:15.018000
Console.WriteLine(datetime.ToString("HH:mm:ss.fffffff", new CultureInfo("en-US"))); // 22:35:15.0180000

Alternatively, with the F custom format specifier we can obtain the second fractions but, this time, only relevant non-zero digits will be shown. We can use up to seven F characters to increase precision up to ten-millionth of a second:

Console.WriteLine(datetime.ToString("HH:mm:ss.%F", new CultureInfo("en-US"))); // 22:35:15
Console.WriteLine(datetime.ToString("HH:mm:ss.FF", new CultureInfo("en-US"))); // 22:35:15.01
Console.WriteLine(datetime.ToString("HH:mm:ss.FFF", new CultureInfo("en-US"))); // 22:35:15.018
Console.WriteLine(datetime.ToString("HH:mm:ss.FFFF", new CultureInfo("en-US"))); // 22:35:15.018
Console.WriteLine(datetime.ToString("HH:mm:ss.FFFFF", new CultureInfo("en-US"))); // 22:35:15.018
Console.WriteLine(datetime.ToString("HH:mm:ss.FFFFFF", new CultureInfo("en-US"))); // 22:35:15.018
Console.WriteLine(datetime.ToString("HH:mm:ss.FFFFFFF", new CultureInfo("en-US"))); // 22:35:15.018

Meridiem DateTime Format Specifier in C#

The t and tt custom format specifiers represent the AM/PM label in time expression. Note that t will only show the label’s first letter.

We will typically use the meridiem designator in combination with the h or hh custom hour specifier since, otherwise, times that are twelve hours apart would be indistinguishable:

Console.WriteLine(datetime.ToString("hh:mm:ss %t", CultureInfo.CreateSpecificCulture("en-US"))); // 10:35:15 P
Console.WriteLine(datetime.ToString("hh:mm:ss tt", CultureInfo.CreateSpecificCulture("en-US"))); // 10:35:15 PM

The character between the time and the AM/PM designator is not always an ordinary space. It comes from the culture data the machine happens to have, and on newer ICU data en-US uses a narrow no-break space (U+202F) instead. The text looks identical and a comparison against a hand-typed "2:35 PM" fails, most often when the same code runs on a Linux container and a Windows desktop. Compare against ToString() output or DateTimeFormatInfo, never against a typed literal.

Time Zone Format Specifier

We can use the K custom format specifier to show the time zone information included in date and time values. For DateTime values this specifier depends on the value’s Kind property:

var datetimeLocal = new DateTime(2017, 8, 4, 22, 35, 15, DateTimeKind.Local);
Console.WriteLine(datetimeLocal.ToString("%K", CultureInfo.CreateSpecificCulture("en-US"))); // the machine's UTC offset, e.g. +02:00

var datetimeUtc = new DateTime(2017, 8, 4, 22, 35, 15, DateTimeKind.Utc);
Console.WriteLine(datetimeUtc.ToString("%K", CultureInfo.CreateSpecificCulture("en-US"))); // Z

Here, we see how the K format string represents the DateTime object time zone with local kind as an offset over the UTC time. Right after that, the value with UTC kind generates a time zone representation of the UTC time Z. If that distinction matters in our code, our guide on DateTime.Now versus DateTime.UtcNow covers which value to format in the first place.

For DateTimeOffset objects, the internal offset value is used:

var datetimeLocal = new DateTimeOffset(2017, 8, 4, 22, 35, 15, TimeSpan.FromHours(2));
Console.WriteLine(datetimeLocal.ToString("%K", CultureInfo.CreateSpecificCulture("en-US"))); // +02:00

var datetimeUtc = new DateTimeOffset(2017, 8, 4, 22, 35, 15, TimeSpan.Zero);
Console.WriteLine(datetimeUtc.ToString("%K", CultureInfo.CreateSpecificCulture("en-US"))); // +00:00

We can see how, in this case, the formatting operation for the UTC time (the time with zero offset) shows the numeric value of the offset +00:00 instead of the Z designator.

Offset DateTime Format Specifier in C#

With the z, zz, zzz format specifiers, we can represent the signed time offset.

For DateTime values, the Kind property is not taken into consideration. It just uses the time zone of the operating system clock. Generally speaking, there are very few scenarios in which this is useful.

For DateTimeOffset values, these format specifiers use the internal offset information:

var datetimeLocal = new DateTimeOffset(2017, 8, 4, 22, 35, 15, TimeSpan.FromHours(2));
Console.WriteLine(datetimeLocal.ToString("%z", CultureInfo.CreateSpecificCulture("en-US"))); // +2
Console.WriteLine(datetimeLocal.ToString("zz", CultureInfo.CreateSpecificCulture("en-US"))); // +02
Console.WriteLine(datetimeLocal.ToString("zzz", CultureInfo.CreateSpecificCulture("en-US"))); // +02:00

Era Format Specifier

Any of the g and gg format specifiers will show the era, for instance, "AD", in our DateTime string representation.

The era representation may vary for different cultures, and it varies further once we work with calendars other than the Gregorian one:

Console.WriteLine(datetime.ToString("%g", CultureInfo.CreateSpecificCulture("en-US"))); // AD

Time Separator Specifiers

The custom format specifier: outputs the time separator defined by the chosen culture. Standard time formats place the time separator in-between hours, minutes, and seconds values:

var datetime = new DateTime(2017, 8, 24, 4, 22, 35, 15);
var formatInfo = CultureInfo.CreateSpecificCulture("en-US").DateTimeFormat;
formatInfo.TimeSeparator = ";";

Console.WriteLine(datetime.ToString("HH:mm:ss", formatInfo)); // 04;22;35

First, we store the DateTimeFormatInfo associated to the American English culture in the formatInfo local variable. After that, we set the default time separator for that DateTimeFormatInfo to semicolon ;. Finally, we use that format information to obtain the string representation of a DateTime value using the custom format string HH:mm:ss.

Date Separator Specifiers

The custom format specifier / outputs the date separator defined by the chosen culture. Standard date formats place the date separator in-between day, month, and year values:

var datetime = new DateTime(2017, 8, 24);
var formatInfo = CultureInfo.CreateSpecificCulture("en-US").DateTimeFormat;
formatInfo.DateSeparator = "-";

Console.WriteLine(datetime.ToString("dd/MM/yyyy", formatInfo)); // 24-08-2017

First, we store the DateTimeFormatInfo associated with the American English culture in the formatInfo local variable. After that, we set the default date separator for that DateTimeFormatInfo to hyphens -. Finally, we use that format information to obtain the string representation of a DateTime value using the custom format string dd/MM/yyyy.

Character Literals in DateTime Format in C#

All the aforementioned custom format specifiers characters in addition to the symbols :, \, /, ' and " are always interpreted as formatting or special characters. If we include any other character in a format string, C# will treat it as literal and will not modify it in the resulting string:

var datetime = new DateTime(2017, 8, 24, 4, 22, 35, 15);
Console.WriteLine(datetime.ToString("dd/MM/yyyy hh:mm:ss EST", CultureInfo.CreateSpecificCulture("en-US")));

The output shows the unchanged EST characters:

24/08/2017 04:22:35 EST

Additionally, parsing operations using a format string with literal characters require those characters to appear in the input string.

We can force any character to be interpreted as a literal character either by using the escape character \\ or by surrounding the literal string with quotation marks or apostrophes:

Console.WriteLine(datetime.ToString("dd/MM/yyyy hh:mm:ss \\g\\m\\t", CultureInfo.CreateSpecificCulture("en-US")));
Console.WriteLine(datetime.ToString("dd/MM/yyyy hh:mm:ss 'gmt'", CultureInfo.CreateSpecificCulture("en-US")));

Here, we use two equivalent format strings that contain the literal string gmt. Since these characters are, also, custom format specifiers, we use the escape character in the format string to force them into literal characters. Then, in the second operation, we enclose the entire literal string in apostrophes:

24/08/2017 04:22:35 gmt
24/08/2017 04:22:35 gmt

Which DateTime Format String Do We Use for a Given Output?

Start from the output and work backwards to the string. Most of the time the answer is one of five.

For a value another program will read back, use "O". It round-trips through DateTime.Parse() with DateTimeStyles.RoundtripKind, and it keeps the offset and all seven fractional-second digits.

For an HTTP header or anything else specified as RFC 1123, use "R", and convert the value to UTC ourselves first.

For a log line that has to sort correctly as plain text, use "s", or the custom string "yyyy-MM-dd HH:mm:ss".

For something a person reads, use "d", "g" or "F" with an explicit CultureInfo, so the output does not change when the process moves to a machine with different regional settings.

For a layout that must not vary at all, write the custom string out in full and pass CultureInfo.InvariantCulture.

The table below covers the rest.

I need…Format stringExample output
A value another program will parse back"O"2017-08-24T14:35:00.0000000Z
An HTTP Date header / RFC 1123"R" (convert to UTC first)Thu, 24 Aug 2017 14:35:00 GMT
A log line that sorts correctly as text"s"2017-08-24T14:35:00
A sortable line marked as UTC"u" (convert to UTC first)2017-08-24 14:35:00Z
A date only, in the reader's culture"d"8/24/2017 (en-US)
A date only, fixed layout"yyyy-MM-dd"2017-08-24
Date and time, fixed layout"yyyy-MM-dd HH:mm:ss"2017-08-24 14:35:00
24-hour time"HH:mm"14:35
12-hour time with AM/PM"hh:mm tt"02:35 PM
Day and month name"dddd, d MMMM"Thursday, 24 August
Month and year"Y"August 2017
Two-digit year"yy"17
Milliseconds"fff"018
The UTC offset"zzz"+02:00
A literal word inside the format"'UTC'" or "\U\T\C"UTC

Two cautions on the fixed-layout rows. / and : are specifiers that emit the culture’s date and time separator, so "yyyy-MM-dd HH:mm:ss" prints 2017-08-24 14.35.30 under fi-FI and "HH:mm" prints 14.35; pass CultureInfo.InvariantCulture, or escape the separators. The - character is not a specifier and does pass through, so the "yyyy-MM-dd" row is safe as written. The zzz row is host-dependent for a DateTime, because it prints the machine’s own offset; prefer a DateTimeOffset there.

Do the u, R and s Specifiers Convert a DateTime to UTC?

No. None of the three converts anything. They change how the value is written down, not what the value is.

"u" prints the date and time exactly as stored and appends a Z, whatever the Kind property says. A DateTime holding local time formatted with "u" claims to be UTC and is not, which is why the conversion has to be ours: call ToUniversalTime() first.

"R" behaves the same way. It appends GMT to whatever it was handed.

"s" is the honest one of the three, because it appends no zone marker at all and so cannot claim anything false.

Only "O" reads the Kind property. A UTC value gets a trailing Z, a local value gets the machine’s offset, and an unspecified value gets neither.

DateTimeOffset avoids the whole question, because it carries its own offset and has nothing to guess.

That is the strongest argument for picking the type deliberately, and our comparison of whether to reach for DateTimeOffset instead works through the rest of the trade-off.

Conclusion

In this article, we’ve learned about the DateTime format in C# and how formatting and parsing operations use different formats. We’ve seen how standard date and time format strings always consist of a single character that maps to a culture-defined custom format string. Then, we learned about every standard format specifier available.

After that, we went through all the custom format specifiers that allow for fully customized formatting.

To wrap things up, we’ve learned how to override the default date and time separator characters and how to use character literals in our format strings. The same ideas carry over to DateOnly and TimeOnly, which have their own format strings.

Tested with .NET 10.