Welcome to collectivesolver - Programming & Software Q&A with code examples. A website with trusted programming answers. All programs are tested and work.

Contact: aviboots(AT)netvision.net.il

Semrush - keyword research tool

Create your online store today with Shopify

Turn ChatGPT, Claude, Gemini, And CoPilot Into Your Personal Assistant, Business Coach, Content Creator, And More

AFFILIATE MARKETING Your all-in-one performance engine Manage affiliates, creators, and customer referrals in one unified platform—turning every partnership into measurable growth

Secure & Reliable Web Hosting, Free Domain, Free SSL, 1-Click WordPress Install, Expert 24/7 Support

Disclosure: My content contains affiliate links.

43,563 questions

56,781 answers

573 users

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

1 Answer

0 votes
import Foundation

// =============================================================================
// Converts date strings written in several common formats into the canonical
// ISO-8601 form (YYYY-MM-DD).
//
// Architecture notes:
//   - Parsing is table-driven: a fixed array of DateFormatter instances
//     describes every accepted input shape. To parse a string we simply try
//     each formatter until one produces a non-nil Date. Adding a new
//     supported format means adding one pattern string to the list, with no
//     other code changes.
//   - Foundation's DateFormatter (backed by ICU) is used rather than manual
//     string splitting/parsing, since calendar arithmetic - leap years,
//     variable month lengths, day-of-week resolution - is exactly the kind
//     of logic that is easy to get subtly wrong by hand and is already
//     implemented correctly, and efficiently, in the platform library.
// =============================================================================

enum FlexibleDateConverter {

    /// Error describing why a date string could not be converted.
    enum ConversionError: Error, CustomStringConvertible {
        case unsupportedFormat

        var description: String {
            "Invalid or unsupported format"
        }
    }

    /// Builds a DateFormatter configured for strict, locale-independent
    /// parsing of one specific pattern.
    ///
    /// Pitfall this avoids: a DateFormatter created with the device's
    /// current locale/calendar can parse the very same digits differently
    /// - or refuse to parse at all - depending on where the app is running
    /// (12-hour vs 24-hour conventions, non-Gregorian calendars, different
    /// digit systems, etc.). Apple's own guidance (Technical Q&A QA1480)
    /// for any *fixed*, machine-defined format - as opposed to a format
    /// meant to be displayed to a person - is to pin both the locale to
    /// "en_US_POSIX" and the time zone explicitly, which is done here.
    private static func makeFormatter(pattern: String) -> DateFormatter {
        let formatter = DateFormatter()
        formatter.dateFormat = pattern
        formatter.locale = Locale(identifier: "en_US_POSIX")
        formatter.timeZone = TimeZone(identifier: "UTC")
        // isLenient already defaults to false, but it is set explicitly so
        // the intent is visible at the call site: reject calendar-impossible
        // dates such as "Feb 30" outright instead of rolling them over into
        // a nearby valid date (which is what a *lenient* formatter would do).
        formatter.isLenient = false
        return formatter
    }

    /// One formatter per supported input shape, built once and reused.
    ///
    /// Performance note: constructing a DateFormatter is comparatively
    /// expensive - it loads locale, calendar and symbol data on first use.
    /// Building this table once as a static property, instead of inside the
    /// parsing function, means that cost is paid a single time for the
    /// life of the program rather than on every call to parse(_:).
    ///
    /// A single-letter pattern symbol ("M", "d") parses a variable-width
    /// 1-or-2-digit number, so "7/5/2026" and "05/01/2024" are both handled
    /// by the same "M/d/yyyy" pattern - no separate fixed-width variant is
    /// needed.
    private static let supportedFormatters: [DateFormatter] = [
        "M/d/yyyy",      // 05/01/2024, 7/5/2026
        "M-d-yyyy",      // 12-31-2023
        "yyyy.M.d",      // 2024.02.09
        "yyyy/M/d",      // 2024/09/17
        "yyyy-M-d",      // 2024-09-17
        "MMMM d, yyyy",  // January 5, 2024
        "MMM d, yyyy",   // Jan 7, 2025
        "d MMM yyyy",    // 25 Dec 2025, 6 Jan 2025
        "d MMMM yyyy"    // 8 January 2025
    ].map(makeFormatter)

    /// The single output formatter every valid input is normalized to.
    private static let isoFormatter: DateFormatter = makeFormatter(pattern: "yyyy-MM-dd")

    /// Parses a date string against the supported formatter table, returning
    /// the first successful match.
    ///
    /// Algorithm / complexity: this is a linear scan over a small, fixed
    /// array of formatters (currently 9), and each parse attempt itself runs
    /// in time proportional to the length of the input string. Since the
    /// formatter count is a constant, the whole lookup is O(1) in the number
    /// of formatters and O(n) in string length - there is no meaningfully
    /// faster approach when the accepted shapes are unrelated grammars, and
    /// nine linear scans over a short string cost microseconds in practice.
    ///
    /// Memory: `first(where:)` short-circuits at the first match, so no
    /// intermediate array of results is ever materialized.
    ///
    /// - Parameter input: the raw date text, in any of the supported formats
    /// - Returns: the parsed `Date`, or `nil` if no formatter matched or the
    ///   text does not represent a real calendar date
    static func parse(_ input: String) -> Date? {
        let trimmed = input.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmed.isEmpty else { return nil }

        return supportedFormatters
            .lazy
            .compactMap { $0.date(from: trimmed) }
            .first
    }

    /// Public convenience API: converts a date string straight to its
    /// "YYYY-MM-DD" representation.
    ///
    /// Error handling: returns `Result` rather than throwing or returning an
    /// optional, so a caller gets an explicit, inspectable value and - in
    /// the failure case - a specific, named error rather than a bare `nil`
    /// that carries no information about what went wrong.
    ///
    /// Security note: parsing uses a fixed, hard-coded set of formatters and
    /// a fixed locale/time zone - user input never influences which pattern
    /// or code path executes (no dynamic format-string construction from
    /// untrusted input), so the input string cannot be crafted to change
    /// parsing behavior beyond matching or failing to match a known shape.
    static func toISODate(_ input: String) -> Result<String, ConversionError> {
        guard let date = parse(input) else {
            return .failure(.unsupportedFormat)
        }
        return .success(isoFormatter.string(from: date))
    }
}

/// Entry point: runs the converter against every supported format plus a
/// set of edge cases that must be rejected - unparseable text, and dates
/// that are syntactically well-formed but do not exist on the calendar.
func main() {
    let testCases = [
        "05/01/2024",       // M/d/yyyy
        "12-31-2023",       // M-d-yyyy
        "2024.02.09",       // yyyy.M.d
        "January 5, 2024",  // MMMM d, yyyy
        "2024/09/17",       // yyyy/M/d
        "2024-09-17",       // yyyy-M-d (already ISO)
        "25 Dec 2025",      // d MMM yyyy
        "6 Jan 2025",       // d MMM yyyy
        "Jan 7, 2025",      // MMM d, yyyy
        "8 January 2025",   // d MMMM yyyy
        "7/5/2026",         // M/d/yyyy, single-digit month and day
        "BadFormat",        // edge case: not a date at all
        "Feb 30, 2024",     // edge case: no such day in February
        "April 31, 2024",   // edge case: April only has 30 days
        "",                 // edge case: empty input
        "2024-13-01"        // edge case: month out of range (13)
    ]

    let header = "Input".padding(toLength: 20, withPad: " ", startingAt: 0)
    print("\(header) -> Result")
    print(String(repeating: "-", count: 45))

    for input in testCases {
        let result: String
        switch FlexibleDateConverter.toISODate(input) {
        case .success(let iso):
            result = iso
        case .failure(let error):
            result = error.description
        }
        let paddedInput = input.padding(toLength: 20, withPad: " ", startingAt: 0)
        print("\(paddedInput) -> \(result)")
    }
}

main()


/*
run:

Input                -> Result
---------------------------------------------
05/01/2024           -> 2024-05-01
12-31-2023           -> 2023-12-31
2024.02.09           -> 2024-02-09
January 5, 2024      -> 2024-01-05
2024/09/17           -> 2024-09-17
2024-09-17           -> 2024-09-17
25 Dec 2025          -> 2025-12-25
6 Jan 2025           -> 2025-01-06
Jan 7, 2025          -> 2025-01-07
8 January 2025       -> 2025-01-08
7/5/2026             -> 2026-07-05
BadFormat            -> Invalid or unsupported format
Feb 30, 2024         -> Invalid or unsupported format
April 31, 2024       -> Invalid or unsupported format
                     -> Invalid or unsupported format
2024-13-01           -> Invalid or unsupported format

*/

 



answered Sep 25 by avibootz

Related questions

...