How to convert multiple date formats to YYYY-MM-DD in Pascal

1 Answer

0 votes
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

}

 



answered Sep 24 by avibootz
...