program DateConverter;
{$mode objfpc}{$H+}
{
Convert multiple date formats to YYYY-MM-DD.
Supported formats:
1. DD/MM/YYYY
2. MM-DD-YYYY
3. YYYY.MM.DD
4. Month DD, YYYY (e.g., January 5, 2024)
5. YYYY/MM/DD
6. YYYY-MM-DD (already normalized)
Architecture notes:
- Each format has its own parser function.
- A dispatcher tries each parser until one succeeds.
- This avoids fragile, monolithic parsing logic.
- Complexity: O(n) over number of formats; trivial cost.
- Memory: small stack buffers; no dynamic allocation.
- Security: careful bounds checking; no unsafe pointer arithmetic.
- Error handling: invalid formats return false.
- Tests: main includes multiple test cases, including edge cases.
Pitfalls:
- Month names must be mapped carefully.
- Whitespace around month names must be handled.
- Leading zeros must be preserved in output.
Performance notes:
- String scanning is cheap; no regex engine overhead.
- For extremely high-throughput scenarios, consider pre-tokenizing input.
}
uses
SysUtils; { For Trim, StrToInt, etc. }
{ ------------------ Month name mapping ------------------ }
function MonthNameToNumber(const Name: string): Integer;
const
Months: array[1..12] of string = (
'January','February','March','April','May','June',
'July','August','September','October','November','December'
);
var
i: Integer;
begin
for i := 1 to 12 do
if SameText(Name, Months[i]) then
begin
Result := i;
Exit;
end;
Result := 0; { invalid }
end;
{ ------------------ Utility: format YYYY-MM-DD ------------------ }
function FormatYMD(Y, M, D: Integer): string;
begin
Result := Format('%4d-%2d-%2d', [Y, M, D]);
{ Format pads with spaces; fix with leading zeros }
Result := Format('%4d-%0.2d-%0.2d', [Y, M, D]);
end;
{ ------------------ Parsers ------------------ }
{ 1. DD/MM/YYYY }
function ParseDDMMYYYY(const S: string; var OutStr: string): Boolean;
var
D, M, Y: Integer;
Parts: TStringArray;
begin
Result := False;
Parts := S.Split(['/']);
if Length(Parts) = 3 then
begin
try
D := StrToInt(Parts[0]);
M := StrToInt(Parts[1]);
Y := StrToInt(Parts[2]);
if (Y > 0) and (M in [1..12]) and (D in [1..31]) then
begin
OutStr := FormatYMD(Y, M, D);
Result := True;
end;
except
Result := False;
end;
end;
end;
{ 2. MM-DD-YYYY }
function ParseMMDDYYYY(const S: string; var OutStr: string): Boolean;
var
D, M, Y: Integer;
Parts: TStringArray;
begin
Result := False;
Parts := S.Split(['-']);
if Length(Parts) = 3 then
begin
try
M := StrToInt(Parts[0]);
D := StrToInt(Parts[1]);
Y := StrToInt(Parts[2]);
if (Y > 0) and (M in [1..12]) and (D in [1..31]) then
begin
OutStr := FormatYMD(Y, M, D);
Result := True;
end;
except
Result := False;
end;
end;
end;
{ 3. YYYY.MM.DD }
function ParseYYYYMMDDDots(const S: string; var OutStr: string): Boolean;
var
D, M, Y: Integer;
Parts: TStringArray;
begin
Result := False;
Parts := S.Split(['.']);
if Length(Parts) = 3 then
begin
try
Y := StrToInt(Parts[0]);
M := StrToInt(Parts[1]);
D := StrToInt(Parts[2]);
if (Y > 0) and (M in [1..12]) and (D in [1..31]) then
begin
OutStr := FormatYMD(Y, M, D);
Result := True;
end;
except
Result := False;
end;
end;
end;
{ 4. Month DD, YYYY }
function ParseMonthDDYYYY(const S: string; var OutStr: string): Boolean;
var
MonthName: string;
D, Y, M: Integer;
CommaPos, SpacePos: Integer;
DayStr, YearStr: string;
begin
Result := False;
{ Find comma separating day and year }
CommaPos := Pos(',', S);
if CommaPos = 0 then Exit;
{ Extract month name and day }
SpacePos := Pos(' ', S);
if SpacePos = 0 then Exit;
MonthName := Trim(Copy(S, 1, SpacePos - 1));
DayStr := Trim(Copy(S, SpacePos + 1, CommaPos - SpacePos - 1));
YearStr := Trim(Copy(S, CommaPos + 1, Length(S)));
try
D := StrToInt(DayStr);
Y := StrToInt(YearStr);
M := MonthNameToNumber(MonthName);
if (M > 0) and (D in [1..31]) and (Y > 0) then
begin
OutStr := FormatYMD(Y, M, D);
Result := True;
end;
except
Result := False;
end;
end;
{ 5. YYYY/MM/DD }
function ParseYYYYMMDDSlashes(const S: string; var OutStr: string): Boolean;
var
D, M, Y: Integer;
Parts: TStringArray;
begin
Result := False;
Parts := S.Split(['/']);
if Length(Parts) = 3 then
begin
try
Y := StrToInt(Parts[0]);
M := StrToInt(Parts[1]);
D := StrToInt(Parts[2]);
if (Y > 0) and (M in [1..12]) and (D in [1..31]) then
begin
OutStr := FormatYMD(Y, M, D);
Result := True;
end;
except
Result := False;
end;
end;
end;
{ 6. YYYY-MM-DD (already normalized) }
function ParseNormalized(const S: string; var OutStr: string): Boolean;
var
D, M, Y: Integer;
Parts: TStringArray;
begin
Result := False;
Parts := S.Split(['-']);
if Length(Parts) = 3 then
begin
try
Y := StrToInt(Parts[0]);
M := StrToInt(Parts[1]);
D := StrToInt(Parts[2]);
if (Y > 0) and (M in [1..12]) and (D in [1..31]) then
begin
OutStr := FormatYMD(Y, M, D);
Result := True;
end;
except
Result := False;
end;
end;
end;
{ ------------------ Dispatcher ------------------ }
function ConvertToCanonical(const S: string; var OutStr: string): Boolean;
begin
if ParseNormalized(S, OutStr) then Exit(True);
if ParseDDMMYYYY(S, OutStr) then Exit(True);
if ParseMMDDYYYY(S, OutStr) then Exit(True);
if ParseYYYYMMDDDots(S, OutStr) then Exit(True);
if ParseMonthDDYYYY(S, OutStr) then Exit(True);
if ParseYYYYMMDDSlashes(S, OutStr) then Exit(True);
Result := False;
end;
{ ------------------ Tests ------------------ }
procedure RunTests;
const
Tests: array[1..10] of string = (
'05/01/2024',
'12-31-2023',
'2024.02.09',
'January 5, 2024',
'2024/09/17',
'2024-09-17',
'BadFormat',
'Feb 30, 2024', { invalid month name }
'April 31, 2024', { invalid day }
'7/7/2024' { single-digit day/month }
);
var
i: Integer;
OutStr: string;
begin
for i := 1 to Length(Tests) do
begin
if ConvertToCanonical(Tests[i], OutStr) then
WriteLn('Input: ', Tests[i]:20, ' -> ', OutStr)
else
WriteLn('Input: ', Tests[i]:20, ' -> Invalid or unsupported format');
end;
end;
begin
RunTests;
end.
{
run:
Input: 05/01/2024 -> 2024-01-05
Input: 12-31-2023 -> 2023-12-31
Input: 2024.02.09 -> 2024-02-09
Input: January 5, 2024 -> 2024-01-05
Input: 2024/09/17 -> 2024-09-17
Input: 2024-09-17 -> 2024-09-17
Input: BadFormat -> Invalid or unsupported format
Input: Feb 30, 2024 -> Invalid or unsupported format
Input: April 31, 2024 -> 2024-04-31
Input: 7/7/2024 -> 2024-07-07
}