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

1 Answer

0 votes
package main

import (
    "fmt"
    "strings"
    "time"
)

/*
Architecture notes:
- Uses only Go’s standard library: time.Parse, time.Date, and string utilities.
- Strict validation: after parsing, the resulting date is checked against the expected
  year, month, and day to prevent silent auto-correction (e.g., Feb 30 → Mar 1).
- Modular design: normalizeDate handles all parsing attempts; strictDate constructs
  and validates dates; parseWithLayout tries specific layouts.
- Complexity: O(n) per date, where n = number of layouts attempted.
- Memory: minimal; only small structs created.
- Security: avoids unsafe parsing; relies on trusted standard library.
- Pitfalls: ambiguous formats (e.g., 7/5/2026 could be MM/DD or DD/MM). Here we assume MM/DD.
- Error handling: invalid or impossible dates return an error.
- Tests in main cover valid, invalid, edge cases, and mixed formats.
*/

// strictDate ensures the constructed date matches exactly.
func strictDate(year, month, day int) (time.Time, error) {
    dt := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC)
  
    if dt.Year() != year || int(dt.Month()) != month || dt.Day() != day {
        return time.Time{}, fmt.Errorf("invalid date")
    }
  
    return dt, nil
}

// parseWithLayout tries a specific layout and validates strictly.
func parseWithLayout(input, layout string) (time.Time, error) {
    dt, err := time.Parse(layout, input)
  
    if err != nil {
        return time.Time{}, err
    }
  
    return strictDate(dt.Year(), int(dt.Month()), dt.Day())
}

// normalizeDate attempts multiple formats and returns YYYY-MM-DD.
func normalizeDate(raw string) (string, error) {
    clean := strings.TrimSpace(raw)
    clean = strings.Trim(clean, `"'`)

    layouts := []string{
        "01/02/2006",     // zero-padded MM/DD/YYYY
        "1/2/2006",       // non-zero-padded MM/DD/YYYY (fix for 7/5/2026)
        "01-02-2006",
        "2006.01.02",
        "January 2, 2006",
        "2006/01/02",
        "2006-01-02",
        "2 Jan 2006",
        "Jan 2, 2006",
        "2 January 2006",
    }

    for _, layout := range layouts {
        if dt, err := parseWithLayout(clean, layout); err == nil {
            return dt.Format("2006-01-02"), nil
        }
    }

    // Fallback: RFC3339, still strictly validated.
    if dt, err := time.Parse(time.RFC3339, clean); err == nil {
        if strict, err := strictDate(dt.Year(), int(dt.Month()), dt.Day()); err == nil {
            return strict.Format("2006-01-02"), nil
        }
    }

    return "", fmt.Errorf("unsupported or invalid format")
}

func main() {
    tests := []string{
        "05/01/2024",
        "12-31-2023",
        "2024.02.09",
        "January 5, 2024",
        "2024/09/17",
        "2024-09-17",
        "25 Dec 2025",
        "6 Jan 2025",
        "Jan 7, 2025",
        "8 January 2025",
        "7/5/2026",      
        "BadFormat",
        "Feb 30, 2024",
        "April 31, 2024",
    }

    for _, t := range tests {
        result, err := normalizeDate(t)
        if err != nil {
            fmt.Printf("Input: %-15s => Output: INVALID\n", t)
        } else {
            fmt.Printf("Input: %-15s => Output: %s\n", t, result)
        }
    }
}



/*
run:

Input: 05/01/2024      => Output: 2024-05-01
Input: 12-31-2023      => Output: 2023-12-31
Input: 2024.02.09      => Output: 2024-02-09
Input: January 5, 2024 => Output: 2024-01-05
Input: 2024/09/17      => Output: 2024-09-17
Input: 2024-09-17      => Output: 2024-09-17
Input: 25 Dec 2025     => Output: 2025-12-25
Input: 6 Jan 2025      => Output: 2025-01-06
Input: Jan 7, 2025     => Output: 2025-01-07
Input: 8 January 2025  => Output: 2025-01-08
Input: 7/5/2026        => Output: 2026-07-05
Input: BadFormat       => Output: INVALID
Input: Feb 30, 2024    => Output: INVALID
Input: April 31, 2024  => Output: INVALID

*/

 



answered Sep 24 by avibootz
edited Oct 29 by avibootz
...