← Back to snacks

Converting any string to date and considering "no date"

Converting strings to dates never seemed to be straightforward. First, specifying the source for all the different date component was difficult. Second, if the conversion is unsuccessful and "no date" has been defined, it could not be expressed with TDate or TDateTime.

Possible solutions for both issues will be presented in this article.

Format specifiers

You define the source of date components using format specifiers. Think of the like placeholders in your string. Delphi initializes the format settings depending on the system your application runs on. If you run on a US-based system, the short date format string will be initialized with mm/dd/yy. In Germany, it will be dd.mm.yy.

Example

Let us assume we have a string with the following numbers 230623. This is supposed to be interpret as a date. Each part of the consists of two digits:

  • Year
  • Month
  • Day

In order to convert this safely, we can use the following code snippet:

var
  LString: String;
  LPaidOn: TDateTime;
  LFormat: TFormatSettings;
 
begin
  // initialize format
  LFormat.ShortDateFormat := 'yymmdd';
 
  // try to convert...
  if TryStrToDate( LString, LPaidOn, LFormat ) then
  begin
    Result.PaidOn := LPaidOn;
  end
  else
  begin
    // what should we assign?
  end;
 

TryStrToDate will return true if the conversion was successful. The date will be stored in LPaidOn. Otherwise, LPaidOn will remain unchanged.

There is one big issue. How can we assign a value to our variable if the conversion was not successful and we want to indicate that the variable "has no value"? A lot of implementations use a dedicated date that they define as "No value" (for example, a date far in the past or future). However, there is a far better solution!

Introducing Nullable data types

In database programming, we can assign "no value" to a field. Sadly, Object Pascal so far has no "official" support for this in the language. However, 3rd party libraries come to the rescue.

In this case, I use the TMS Business Core Library (BCL).

Import Bcl.Types.Nullable.pas:

uses
  System.Generics.Collections
  , Bcl.Types.Nullable

Declare your date time value as Nullable. This can also be a field variable of your class. It is not restricted to local variables, for example.

FPaidOn: Nullable<TDateTime>;

Assign values as before or set it to "no value". This is done in the BCL using the variable SNull.

  if TryStrToDate( LString, LPaidOn, LFormat ) then
  begin
    FPaidOn := LPaidOn;
  end
  else
  begin
    FPaidOn := SNull;
  end;

In order to query if a value is assigned or not, you can use IsNull.

  if FPaidOn.IsNull then 
  begin
    // value is null
  end
  else
  begin
    // use value
  end;

You might be hesitant to use this fearing it might break IDE functionality like Code Completion. However, it works perfectly fine as at its core the Nullable implementation uses nothing that would confuse the IDE.

Screenshot

List of format specifiers

I always struggle to remember which specifiers can be used with TFormatSettings. Here is the table based on the Embarcadero documentation at https://docwiki.embarcadero.com/Libraries/Alexandria/en/System.SysUtils.FormatDateTime.

SpecifierDisplays
cDisplays the date using the format given by the ShortDateFormat global variable, followed by the time using the format given by the LongTimeFormat global variable. The time is not displayed if the date-time value indicates midnight precisely.
dDisplays the day as a number without a leading zero (1-31).
ddDisplays the day as a number with a leading zero (01-31).
dddDisplays the day as an abbreviation (Sun-Sat) using the strings given by the ShortDayNames global variable.
ddddDisplays the day as a full name (Sunday-Saturday) using the strings given by the LongDayNames global variable.
dddddDisplays the date using the format given by the ShortDateFormat global variable.
ddddddDisplays the date using the format given by the LongDateFormat global variable.
e(Windows only) Displays the year in the current period/era as a number without a leading zero (Japanese, Korean, and Taiwanese locales only).
ee(Windows only) Displays the year in the current period/era as a number with a leading zero (Japanese, Korean, and Taiwanese locales only).
g(Windows only) Displays the period/era as an abbreviation (Japanese and Taiwanese locales only).
gg(Windows only) Displays the period/era as a full name (Japanese and Taiwanese locales only).
mDisplays the month as a number without a leading zero (1-12). If the m specifier immediately follows an h or hh specifier, the minute rather than the month is displayed.
mmDisplays the month as a number with a leading zero (01-12). If the mm specifier immediately follows an h or hh specifier, the minute rather than the month is displayed.
mmmDisplays the month as an abbreviation (Jan-Dec) using the strings given by the ShortMonthNames global variable.
mmmmDisplays the month as a full name (January-December) using the strings given by the LongMonthNames global variable.
yyDisplays the year as a two-digit number (00-99).
yyyyDisplays the year as a four-digit number (0000-9999).
hDisplays the hour without a leading zero (0-23).
hhDisplays the hour with a leading zero (00-23).
nDisplays the minute without a leading zero (0-59).
nnDisplays the minute with a leading zero (00-59).
sDisplays the second without a leading zero (0-59).
ssDisplays the second with a leading zero (00-59).
zDisplays the millisecond without a leading zero (0-999).
zzzDisplays the millisecond with a leading zero (000-999).
tDisplays the time using the format given by the ShortTimeFormat global variable.
ttDisplays the time using the format given by the LongTimeFormat global variable.
am/pmUses the 12-hour clock for the preceding h or hh specifier, and displays 'am' for any hour before noon, and 'pm' for any hour after noon. The am/pm specifier can use lower, upper, or mixed case, and the result is displayed accordingly.
a/pUses the 12-hour clock for the preceding h or hh specifier, and displays 'a' for any hour before noon, and 'p' for any hour after noon. The a/p specifier can use lower, upper, or mixed case, and the result is displayed accordingly.
ampmUses the 12-hour clock for the preceding h or hh specifier, and displays the contents of the TimeAMString global variable for any hour before noon, and the contents of the TimePMString global variable for any hour after noon.
/Displays the date separator character given by the DateSeparator global variable.
:Displays the time separator character given by the TimeSeparator global variable.
'xx'/"xx"Characters enclosed in single or double quotation marks are displayed as such, and do not affect formatting.