How to convert multiple date formats to YYYY-MM-DD in C++

1 Answer

0 votes
#include <iostream>
#include <string>
#include <vector>
#include <regex>
#include <optional>
#include <sstream>

/*
    Convert multiple date formats to YYYY-MM-DD.

    Supported formats:
      - DD/MM/YYYY
      - MM-DD-YYYY
      - YYYY.MM.DD
      - Month DD, YYYY   (e.g., January 5, 2024)
      - YYYY/MM/DD
      - YYYY-MM-DD       (already normalized)

    Architecture notes:
      - The program uses a modular design: each format has its own parser.
      - A dispatcher tries each parser until one succeeds.
      - This avoids brittle "one giant regex" designs.
      - The algorithm is O(n) in the number of formats, trivial cost.
      - Memory usage is minimal; only small strings and regex objects.
      - Security: no unsafe pointer arithmetic; regex is bounded; no external input files.
      - Error handling: invalid formats return std::nullopt.
      - Tests: main() includes multiple test cases, including edge cases.

    Pitfalls:
      - Month names must be mapped carefully.
      - Regex must anchor patterns to avoid partial matches.
      - Leading zeros must be preserved in output.

    Performance notes:
      - Regex compilation is static; repeated calls are cheap.
      - For extremely high-throughput scenarios, consider precompiled regex or manual parsing.
*/

// ------------------ Month name mapping ------------------
std::optional<int> monthNameToNumber(const std::string& name) {
    static const std::vector<std::pair<std::string, int>> months = {
        {"January", 1}, {"February", 2}, {"March", 3}, {"April", 4},
        {"May", 5}, {"June", 6}, {"July", 7}, {"August", 8},
        {"September", 9}, {"October", 10}, {"November", 11}, {"December", 12}
    };

    for (const auto& [m, num] : months) {
        if (name == m) return num;
    }
    
    return std::nullopt;
}

// ------------------ Utility: format YYYY-MM-DD ------------------
std::string formatYMD(int y, int m, int d) {
    std::ostringstream oss;
    
    oss << y << "-"
        << (m < 10 ? "0" : "") << m << "-"
        << (d < 10 ? "0" : "") << d;
    
    return oss.str();
}

// ------------------ Parsers for each format ------------------

// DD/MM/YYYY
std::optional<std::string> parseDDMMYYYY(const std::string& s) {
    static const std::regex re(R"(^(\d{1,2})\/(\d{1,2})\/(\d{4})$)");
    std::smatch m;
    
    if (std::regex_match(s, m, re)) {
        int d = std::stoi(m[1]);
        int mo = std::stoi(m[2]);
        int y = std::stoi(m[3]);
        return formatYMD(y, mo, d);
    }
    
    return std::nullopt;
}

// MM-DD-YYYY
std::optional<std::string> parseMMDDYYYY(const std::string& s) {
    static const std::regex re(R"(^(\d{1,2})\-(\d{1,2})\-(\d{4})$)");
    std::smatch m;
    
    if (std::regex_match(s, m, re)) {
        int mo = std::stoi(m[1]);
        int d = std::stoi(m[2]);
        int y = std::stoi(m[3]);
        return formatYMD(y, mo, d);
    }
    
    return std::nullopt;
}

// YYYY.MM.DD
std::optional<std::string> parseYYYYMMDDDots(const std::string& s) {
    static const std::regex re(R"(^(\d{4})\.(\d{1,2})\.(\d{1,2})$)");
    std::smatch m;
    
    if (std::regex_match(s, m, re)) {
        int y = std::stoi(m[1]);
        int mo = std::stoi(m[2]);
        int d = std::stoi(m[3]);
        return formatYMD(y, mo, d);
    }
    
    return std::nullopt;
}

// Month DD, YYYY
std::optional<std::string> parseMonthDDYYYY(const std::string& s) {
    static const std::regex re(R"(^([A-Za-z]+)\s+(\d{1,2}),\s*(\d{4})$)");
    std::smatch m;
    
    if (std::regex_match(s, m, re)) {
        auto mo = monthNameToNumber(m[1]);
        if (!mo) return std::nullopt;
        int d = std::stoi(m[2]);
        int y = std::stoi(m[3]);
        return formatYMD(y, *mo, d);
    }
    
    return std::nullopt;
}

// YYYY/MM/DD
std::optional<std::string> parseYYYYMMDDSlashes(const std::string& s) {
    static const std::regex re(R"(^(\d{4})\/(\d{1,2})\/(\d{1,2})$)");
    std::smatch m;
    
    if (std::regex_match(s, m, re)) {
        int y = std::stoi(m[1]);
        int mo = std::stoi(m[2]);
        int d = std::stoi(m[3]);
        return formatYMD(y, mo, d);
    }
    
    return std::nullopt;
}

// YYYY-MM-DD (already normalized)
std::optional<std::string> parseNormalized(const std::string& s) {
    static const std::regex re(R"(^(\d{4})\-(\d{1,2})\-(\d{1,2})$)");
    std::smatch m;
    
    if (std::regex_match(s, m, re)) {
        int y = std::stoi(m[1]);
        int mo = std::stoi(m[2]);
        int d = std::stoi(m[3]);
        return formatYMD(y, mo, d);
    }
    
    return std::nullopt;
}

// ------------------ Dispatcher ------------------
std::optional<std::string> convertToCanonical(const std::string& s) {
    // Order matters only for performance; correctness is unaffected.
    const std::vector<std::function<std::optional<std::string>(const std::string&)>> parsers = {
        parseNormalized,
        parseDDMMYYYY,
        parseMMDDYYYY,
        parseYYYYMMDDDots,
        parseMonthDDYYYY,
        parseYYYYMMDDSlashes
    };

    for (const auto& parser : parsers) {
        if (auto result = parser(s)) {
            return result;
        }
    }
    
    return std::nullopt;
}

// ------------------ Tests ------------------
int main() {
    std::vector<std::string> tests = {
        "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
    };

    for (const auto& t : tests) {
        auto r = convertToCanonical(t);
        if (r) {
            std::cout << "Input: " << t << " -> " << *r << "\n";
        } else {
            std::cout << "Input: " << t << " -> " << "Invalid or unsupported format\n";
        }
    }
}



/*
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

*/

 



answered Sep 24 by avibootz
...