From c035d64de0dbafa326b6da1d30e683dfe94d0319 Mon Sep 17 00:00:00 2001 From: owdax Date: Thu, 16 Jan 2025 03:20:10 +0330 Subject: [PATCH 01/23] feat: Add string conversion module with SOLID principles, tests, and documentation --- Package.swift | 8 +- .../Documentation.docc/StringConversion.md | 76 ++++++++++++++++ .../NumericStringConvertible.swift | 24 +++++ .../StringConversion/StringConvertible.swift | 41 +++++++++ .../StringConversionTests.swift | 88 +++++++++++++++++++ 5 files changed, 233 insertions(+), 4 deletions(-) create mode 100644 Sources/SwiftDevKit/Documentation.docc/StringConversion.md create mode 100644 Sources/SwiftDevKit/StringConversion/NumericStringConvertible.swift create mode 100644 Sources/SwiftDevKit/StringConversion/StringConvertible.swift create mode 100644 Tests/SwiftDevKitTests/StringConversion/StringConversionTests.swift diff --git a/Package.swift b/Package.swift index e8b2fdd..d56afe3 100644 --- a/Package.swift +++ b/Package.swift @@ -7,7 +7,7 @@ let package = Package( name: "SwiftDevKit", platforms: [ .iOS(.v13), - .macOS(.v10_15), + .macOS("10.15.4"), .tvOS(.v13), .watchOS(.v6), ], @@ -28,13 +28,13 @@ let package = Package( .target( name: "SwiftDevKit", dependencies: [], + resources: [ + .process("Resources") + ], swiftSettings: [ .define("DEBUG", .when(configuration: .debug)), .enableUpcomingFeature("BareSlashRegexLiterals"), .enableExperimentalFeature("StrictConcurrency"), - ], - plugins: [ - .plugin(name: "Swift-DocC", package: "swift-docc-plugin"), ]), .testTarget( name: "SwiftDevKitTests", diff --git a/Sources/SwiftDevKit/Documentation.docc/StringConversion.md b/Sources/SwiftDevKit/Documentation.docc/StringConversion.md new file mode 100644 index 0000000..2c453a8 --- /dev/null +++ b/Sources/SwiftDevKit/Documentation.docc/StringConversion.md @@ -0,0 +1,76 @@ +# String Conversion + +Convert values to and from their string representations with type safety and error handling. + +## Overview + +The String Conversion module provides a standardized way to convert values between their string representations and native types. It follows SOLID principles and provides type-safe conversions with comprehensive error handling. + +### Key Features + +- Protocol-based design following Interface Segregation Principle +- Type-safe conversions with clear error handling +- Built-in support for numeric types +- Extensible for custom types +- Comprehensive test coverage + +## Topics + +### Essentials + +- ``StringConvertible`` +- ``StringConversionError`` + +### Basic Usage + +```swift +// Converting to string +let number = 42 +let stringValue = try number.toString() // "42" + +// Converting from string +let value = try Int.fromString("42") // 42 + +// Error handling +do { + let invalid = try Int.fromString("not a number") +} catch StringConversionError.invalidInput(let value) { + print("Invalid input: \(value)") +} +``` + +### Best Practices + +When implementing `StringConvertible` for your custom types: + +1. Provide clear error cases +2. Handle edge cases appropriately +3. Document conversion format requirements +4. Include validation in `fromString` +5. Maintain round-trip consistency + +```swift +extension MyCustomType: StringConvertible { + func toString() throws -> String { + // Implement conversion to string + } + + static func fromString(_ string: String) throws -> Self { + // Implement conversion from string + // Include proper validation + // Handle edge cases + } +} +``` + +### Error Handling + +The module uses `StringConversionError` to handle common conversion errors: + +- `.invalidInput`: The input string is not valid for the requested conversion +- `.unsupportedConversion`: The conversion operation is not supported +- `.custom`: Custom error cases with specific messages + +### Thread Safety + +All conversion operations are thread-safe and can be used in concurrent environments. \ No newline at end of file diff --git a/Sources/SwiftDevKit/StringConversion/NumericStringConvertible.swift b/Sources/SwiftDevKit/StringConversion/NumericStringConvertible.swift new file mode 100644 index 0000000..8ae8898 --- /dev/null +++ b/Sources/SwiftDevKit/StringConversion/NumericStringConvertible.swift @@ -0,0 +1,24 @@ +import Foundation + +/// Extends numeric types to provide string conversion capabilities. +/// This extension follows the Single Responsibility Principle by focusing solely on numeric string conversion. +extension StringConvertible where Self: Numeric & LosslessStringConvertible { + public func toString() throws -> String { + String(describing: self) + } + + public static func fromString(_ string: String) throws -> Self { + guard let value = Self(string) else { + throw StringConversionError.invalidInput(string) + } + return value + } +} + +// Conformance for basic numeric types +extension Int: StringConvertible {} +extension Double: StringConvertible {} +extension Float: StringConvertible {} +extension Int64: StringConvertible {} +extension UInt: StringConvertible {} +extension UInt64: StringConvertible {} \ No newline at end of file diff --git a/Sources/SwiftDevKit/StringConversion/StringConvertible.swift b/Sources/SwiftDevKit/StringConversion/StringConvertible.swift new file mode 100644 index 0000000..631f7eb --- /dev/null +++ b/Sources/SwiftDevKit/StringConversion/StringConvertible.swift @@ -0,0 +1,41 @@ +import Foundation + +/// A type that can be converted to and from a string representation. +/// +/// This protocol provides a standardized way to convert types to and from their string representations. +/// It follows the Interface Segregation Principle by keeping the interface focused and minimal. +public protocol StringConvertible { + /// Converts the instance to its string representation. + /// + /// - Returns: A string representation of the instance. + /// - Throws: `StringConversionError` if the conversion fails. + func toString() throws -> String + + /// Creates an instance from its string representation. + /// + /// - Parameter string: The string to convert from. + /// - Returns: An instance of the conforming type. + /// - Throws: `StringConversionError` if the conversion fails. + static func fromString(_ string: String) throws -> Self +} + +/// Errors that can occur during string conversion operations. +public enum StringConversionError: Error, LocalizedError, Equatable { + /// The input string is invalid for the requested conversion. + case invalidInput(String) + /// The conversion operation is not supported for this type. + case unsupportedConversion + /// A custom error with a specific message. + case custom(String) + + public var errorDescription: String? { + switch self { + case .invalidInput(let value): + return "Invalid input string: \(value)" + case .unsupportedConversion: + return "Unsupported conversion operation" + case .custom(let message): + return message + } + } +} \ No newline at end of file diff --git a/Tests/SwiftDevKitTests/StringConversion/StringConversionTests.swift b/Tests/SwiftDevKitTests/StringConversion/StringConversionTests.swift new file mode 100644 index 0000000..914a20b --- /dev/null +++ b/Tests/SwiftDevKitTests/StringConversion/StringConversionTests.swift @@ -0,0 +1,88 @@ +import Testing +import _TestingInternals +import SwiftDevKit + +/// Test suite for string conversion functionality. +/// Following the Arrange-Act-Assert pattern and including both positive and negative test cases. +struct StringConversionTests { + /// Tests string conversion for various numeric types. + func testNumericStringConversion() async throws { + // MARK: - Integer Tests + + // Test valid integer conversion + let intValue = try Int.fromString("42") + #expect(intValue == 42) + #expect(try intValue.toString() == "42") + + // Test invalid integer input + #expect(throws: StringConversionError.invalidInput("invalid")) { + try Int.fromString("invalid") + } + + // MARK: - Double Tests + + // Test valid double conversion + let doubleValue = try Double.fromString("3.14") + #expect(doubleValue == 3.14) + #expect(try doubleValue.toString() == "3.14") + + // Test invalid double input + #expect(throws: StringConversionError.invalidInput("invalid")) { + try Double.fromString("invalid") + } + + // MARK: - Float Tests + + // Test valid float conversion + let floatValue = try Float.fromString("1.23") + #expect(floatValue == 1.23) + #expect(try floatValue.toString() == "1.23") + + // Test invalid float input + #expect(throws: StringConversionError.invalidInput("invalid")) { + try Float.fromString("invalid") + } + + // MARK: - Edge Cases + + // Test maximum values + let maxInt = try Int.fromString(String(Int.max)) + #expect(maxInt == Int.max) + + // Test minimum values + let minInt = try Int.fromString(String(Int.min)) + #expect(minInt == Int.min) + + // Test zero + let zero = try Int.fromString("0") + #expect(zero == 0) + + // Test negative numbers + let negative = try Int.fromString("-42") + #expect(negative == -42) + } + + /// Tests error handling in string conversion. + func testStringConversionErrors() async throws { + // Test empty string + #expect(throws: StringConversionError.invalidInput("")) { + try Int.fromString("") + } + + // Test whitespace + #expect(throws: StringConversionError.invalidInput(" ")) { + try Int.fromString(" ") + } + + // Test non-numeric characters + #expect(throws: StringConversionError.invalidInput("12.34.56")) { + try Int.fromString("12.34.56") + } + + // Test overflow scenarios + let overflowString = "999999999999999999999999999999" + #expect(throws: StringConversionError.invalidInput(overflowString)) { + try Int.fromString(overflowString) + } + } +} \ No newline at end of file From 87a921ff4e7b08e7e2b3315a880dfdf54c9e7f43 Mon Sep 17 00:00:00 2001 From: owdax Date: Thu, 16 Jan 2025 03:20:28 +0330 Subject: [PATCH 02/23] style: Fix case alignment in StringConversionError --- Package.swift | 2 +- .../NumericStringConvertible.swift | 16 ++++--- .../StringConversion/StringConvertible.swift | 24 ++++++---- .../StringConversionTests.swift | 44 +++++++++++-------- 4 files changed, 52 insertions(+), 34 deletions(-) diff --git a/Package.swift b/Package.swift index d56afe3..31c3b69 100644 --- a/Package.swift +++ b/Package.swift @@ -29,7 +29,7 @@ let package = Package( name: "SwiftDevKit", dependencies: [], resources: [ - .process("Resources") + .process("Resources"), ], swiftSettings: [ .define("DEBUG", .when(configuration: .debug)), diff --git a/Sources/SwiftDevKit/StringConversion/NumericStringConvertible.swift b/Sources/SwiftDevKit/StringConversion/NumericStringConvertible.swift index 8ae8898..6cf4d5e 100644 --- a/Sources/SwiftDevKit/StringConversion/NumericStringConvertible.swift +++ b/Sources/SwiftDevKit/StringConversion/NumericStringConvertible.swift @@ -1,13 +1,19 @@ +// NumericStringConvertible.swift +// SwiftDevKit +// +// Copyright (c) 2025 owdax and The SwiftDevKit Contributors +// MIT License - https://opensource.org/licenses/MIT + import Foundation /// Extends numeric types to provide string conversion capabilities. /// This extension follows the Single Responsibility Principle by focusing solely on numeric string conversion. -extension StringConvertible where Self: Numeric & LosslessStringConvertible { - public func toString() throws -> String { +public extension StringConvertible where Self: Numeric & LosslessStringConvertible { + func toString() throws -> String { String(describing: self) } - - public static func fromString(_ string: String) throws -> Self { + + static func fromString(_ string: String) throws -> Self { guard let value = Self(string) else { throw StringConversionError.invalidInput(string) } @@ -21,4 +27,4 @@ extension Double: StringConvertible {} extension Float: StringConvertible {} extension Int64: StringConvertible {} extension UInt: StringConvertible {} -extension UInt64: StringConvertible {} \ No newline at end of file +extension UInt64: StringConvertible {} diff --git a/Sources/SwiftDevKit/StringConversion/StringConvertible.swift b/Sources/SwiftDevKit/StringConversion/StringConvertible.swift index 631f7eb..ab923d8 100644 --- a/Sources/SwiftDevKit/StringConversion/StringConvertible.swift +++ b/Sources/SwiftDevKit/StringConversion/StringConvertible.swift @@ -1,3 +1,9 @@ +// StringConvertible.swift +// SwiftDevKit +// +// Copyright (c) 2025 owdax and The SwiftDevKit Contributors +// MIT License - https://opensource.org/licenses/MIT + import Foundation /// A type that can be converted to and from a string representation. @@ -10,7 +16,7 @@ public protocol StringConvertible { /// - Returns: A string representation of the instance. /// - Throws: `StringConversionError` if the conversion fails. func toString() throws -> String - + /// Creates an instance from its string representation. /// /// - Parameter string: The string to convert from. @@ -27,15 +33,15 @@ public enum StringConversionError: Error, LocalizedError, Equatable { case unsupportedConversion /// A custom error with a specific message. case custom(String) - + public var errorDescription: String? { switch self { - case .invalidInput(let value): - return "Invalid input string: \(value)" - case .unsupportedConversion: - return "Unsupported conversion operation" - case .custom(let message): - return message + case .invalidInput(let value): + return "Invalid input string: \(value)" + case .unsupportedConversion: + return "Unsupported conversion operation" + case .custom(let message): + return message } } -} \ No newline at end of file +} diff --git a/Tests/SwiftDevKitTests/StringConversion/StringConversionTests.swift b/Tests/SwiftDevKitTests/StringConversion/StringConversionTests.swift index 914a20b..1069edb 100644 --- a/Tests/SwiftDevKitTests/StringConversion/StringConversionTests.swift +++ b/Tests/SwiftDevKitTests/StringConversion/StringConversionTests.swift @@ -1,6 +1,12 @@ -import Testing +// StringConversionTests.swift +// SwiftDevKit +// +// Copyright (c) 2025 owdax and The SwiftDevKit Contributors +// MIT License - https://opensource.org/licenses/MIT + import _TestingInternals import SwiftDevKit +import Testing /// Test suite for string conversion functionality. /// Following the Arrange-Act-Assert pattern and including both positive and negative test cases. @@ -8,81 +14,81 @@ struct StringConversionTests { /// Tests string conversion for various numeric types. func testNumericStringConversion() async throws { // MARK: - Integer Tests - + // Test valid integer conversion let intValue = try Int.fromString("42") #expect(intValue == 42) #expect(try intValue.toString() == "42") - + // Test invalid integer input #expect(throws: StringConversionError.invalidInput("invalid")) { try Int.fromString("invalid") } - + // MARK: - Double Tests - + // Test valid double conversion let doubleValue = try Double.fromString("3.14") #expect(doubleValue == 3.14) #expect(try doubleValue.toString() == "3.14") - + // Test invalid double input #expect(throws: StringConversionError.invalidInput("invalid")) { try Double.fromString("invalid") } - + // MARK: - Float Tests - + // Test valid float conversion let floatValue = try Float.fromString("1.23") #expect(floatValue == 1.23) #expect(try floatValue.toString() == "1.23") - + // Test invalid float input #expect(throws: StringConversionError.invalidInput("invalid")) { try Float.fromString("invalid") } - + // MARK: - Edge Cases - + // Test maximum values let maxInt = try Int.fromString(String(Int.max)) #expect(maxInt == Int.max) - + // Test minimum values let minInt = try Int.fromString(String(Int.min)) #expect(minInt == Int.min) - + // Test zero let zero = try Int.fromString("0") #expect(zero == 0) - + // Test negative numbers let negative = try Int.fromString("-42") #expect(negative == -42) } - + /// Tests error handling in string conversion. func testStringConversionErrors() async throws { // Test empty string #expect(throws: StringConversionError.invalidInput("")) { try Int.fromString("") } - + // Test whitespace #expect(throws: StringConversionError.invalidInput(" ")) { try Int.fromString(" ") } - + // Test non-numeric characters #expect(throws: StringConversionError.invalidInput("12.34.56")) { try Int.fromString("12.34.56") } - + // Test overflow scenarios let overflowString = "999999999999999999999999999999" #expect(throws: StringConversionError.invalidInput(overflowString)) { try Int.fromString(overflowString) } } -} \ No newline at end of file +} From 5832dea28629d837cd16cff38ba7d33318af8171 Mon Sep 17 00:00:00 2001 From: owdax Date: Thu, 16 Jan 2025 03:20:50 +0330 Subject: [PATCH 03/23] style: Fix switch case alignment in StringConversionError --- .../StringConversion/StringConvertible.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/SwiftDevKit/StringConversion/StringConvertible.swift b/Sources/SwiftDevKit/StringConversion/StringConvertible.swift index ab923d8..c86cc86 100644 --- a/Sources/SwiftDevKit/StringConversion/StringConvertible.swift +++ b/Sources/SwiftDevKit/StringConversion/StringConvertible.swift @@ -36,12 +36,12 @@ public enum StringConversionError: Error, LocalizedError, Equatable { public var errorDescription: String? { switch self { - case .invalidInput(let value): - return "Invalid input string: \(value)" - case .unsupportedConversion: - return "Unsupported conversion operation" - case .custom(let message): - return message + case .invalidInput(let value): + return "Invalid input string: \(value)" + case .unsupportedConversion: + return "Unsupported conversion operation" + case .custom(let message): + return message } } } From 3c84370aad7bddd023de6c9d55f779a8015d19d1 Mon Sep 17 00:00:00 2001 From: owdax Date: Thu, 16 Jan 2025 03:45:04 +0330 Subject: [PATCH 04/23] refactor: Separate sync and async string conversion protocols - Make StringConvertible protocol synchronous for simple conversions - Create separate DateConvertible protocol for async date operations - Update tests to reflect the sync/async separation - Improve thread safety tests with concurrent operations --- Package.swift | 15 ++- .../StringConversion/Date+Convertible.swift | 87 +++++++++++++ .../StringConversion/DateConvertible.swift | 72 +++++++++++ .../NumericStringConvertible.swift | 2 +- .../DateConversionTests.swift | 115 ++++++++++++++++++ .../StringConversionTests.swift | 41 ++++--- Tests/SwiftDevKitTests/SwiftDevKitTests.swift | 4 +- 7 files changed, 306 insertions(+), 30 deletions(-) create mode 100644 Sources/SwiftDevKit/StringConversion/Date+Convertible.swift create mode 100644 Sources/SwiftDevKit/StringConversion/DateConvertible.swift create mode 100644 Tests/SwiftDevKitTests/StringConversion/DateConversionTests.swift diff --git a/Package.swift b/Package.swift index 31c3b69..d8aa9b3 100644 --- a/Package.swift +++ b/Package.swift @@ -6,10 +6,10 @@ import PackageDescription let package = Package( name: "SwiftDevKit", platforms: [ - .iOS(.v13), - .macOS("10.15.4"), - .tvOS(.v13), - .watchOS(.v6), + .macOS(.v13), + .iOS(.v16), + .tvOS(.v16), + .watchOS(.v9), ], products: [ // Products define the executables and libraries a package produces, making them visible to other packages. @@ -19,8 +19,8 @@ let package = Package( ], dependencies: [ // Dependencies will be added as needed - .package(url: "https://github.com/apple/swift-testing.git", from: "0.5.0"), .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-testing", from: "0.5.0"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. @@ -29,12 +29,11 @@ let package = Package( name: "SwiftDevKit", dependencies: [], resources: [ - .process("Resources"), + .process("Resources") ], swiftSettings: [ - .define("DEBUG", .when(configuration: .debug)), .enableUpcomingFeature("BareSlashRegexLiterals"), - .enableExperimentalFeature("StrictConcurrency"), + .enableExperimentalFeature("StrictConcurrency") ]), .testTarget( name: "SwiftDevKitTests", diff --git a/Sources/SwiftDevKit/StringConversion/Date+Convertible.swift b/Sources/SwiftDevKit/StringConversion/Date+Convertible.swift new file mode 100644 index 0000000..0b9d293 --- /dev/null +++ b/Sources/SwiftDevKit/StringConversion/Date+Convertible.swift @@ -0,0 +1,87 @@ +// Date+Convertible.swift +// SwiftDevKit +// +// Copyright (c) 2025 owdax and The SwiftDevKit Contributors +// MIT License - https://opensource.org/licenses/MIT + +import Foundation + +/// Actor that manages thread-safe access to date formatters +private actor DateFormatterCache { + /// Cache of date formatters + private var formatters: [String: DateFormatter] = [:] + + /// Gets or creates a date formatter for the specified format + func formatter(for format: String) -> DateFormatter { + if let formatter = formatters[format] { + return formatter + } + + let formatter = DateFormatter() + formatter.dateFormat = format + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeZone = TimeZone(secondsFromGMT: 0) + formatters[format] = formatter + return formatter + } +} + +extension Date: DateConvertible { + /// Thread-safe date formatter cache + private static let formatterCache = DateFormatterCache() + + public func toString(format: String?) async throws -> String { + let dateFormat = format ?? DateFormat.iso8601 + let formatter = await Self.formatterCache.formatter(for: dateFormat) + return formatter.string(from: self) + } + + public static func fromString(_ string: String, format: String?) async throws -> Date { + let dateFormat = format ?? DateFormat.iso8601 + let formatter = await formatterCache.formatter(for: dateFormat) + + guard let date = formatter.date(from: string) else { + throw DateConversionError.invalidFormat(string) + } + + return date + } +} + +// MARK: - Convenience Methods + +public extension Date { + /// Creates a date from an ISO8601 string + /// + /// - Parameter iso8601String: The ISO8601 formatted string + /// - Returns: A new Date instance + /// - Throws: DateConversionError if the string is not valid ISO8601 + static func fromISO8601(_ iso8601String: String) async throws -> Date { + try await fromString(iso8601String, format: DateFormat.iso8601) + } + + /// Converts the date to an ISO8601 string + /// + /// - Returns: An ISO8601 formatted string + /// - Throws: DateConversionError if the conversion fails + func toISO8601() async throws -> String { + try await toString(format: DateFormat.iso8601) + } + + /// Creates a date from an HTTP date string + /// + /// - Parameter httpDateString: The HTTP date formatted string + /// - Returns: A new Date instance + /// - Throws: DateConversionError if the string is not a valid HTTP date + static func fromHTTPDate(_ httpDateString: String) async throws -> Date { + try await fromString(httpDateString, format: DateFormat.http) + } + + /// Converts the date to an HTTP date string + /// + /// - Returns: An HTTP date formatted string + /// - Throws: DateConversionError if the conversion fails + func toHTTPDate() async throws -> String { + try await toString(format: DateFormat.http) + } +} diff --git a/Sources/SwiftDevKit/StringConversion/DateConvertible.swift b/Sources/SwiftDevKit/StringConversion/DateConvertible.swift new file mode 100644 index 0000000..b436843 --- /dev/null +++ b/Sources/SwiftDevKit/StringConversion/DateConvertible.swift @@ -0,0 +1,72 @@ +// DateConvertible.swift +// SwiftDevKit +// +// Copyright (c) 2025 owdax and The SwiftDevKit Contributors +// MIT License - https://opensource.org/licenses/MIT + +import Foundation + +/// A type that can be converted to and from date representations. +/// This protocol provides date-specific conversion capabilities with thread-safe operations. +public protocol DateConvertible { + /// Converts the instance to a date string using the specified format. + /// + /// - Parameter format: The date format to use. If nil, uses ISO8601 format. + /// - Returns: A string representation of the date. + /// - Throws: `DateConversionError` if the conversion fails. + func toString(format: String?) async throws -> String + + /// Creates an instance from a date string using the specified format. + /// + /// - Parameters: + /// - string: The string to convert from. + /// - format: The date format to use. If nil, uses ISO8601 format. + /// - Returns: An instance of the conforming type. + /// - Throws: `DateConversionError` if the conversion fails. + static func fromString(_ string: String, format: String?) async throws -> Self +} + +/// Errors specific to date conversion operations. +public enum DateConversionError: Error, LocalizedError, Equatable { + /// The date string doesn't match the expected format. + case invalidFormat(String) + /// The date components are invalid (e.g., month > 12). + case invalidComponents + /// The provided format string is invalid. + case invalidFormatString(String) + /// A custom error with a specific message. + case custom(String) + + public var errorDescription: String? { + switch self { + case .invalidFormat(let value): + return "Date string doesn't match the expected format: \(value)" + case .invalidComponents: + return "Date contains invalid components" + case .invalidFormatString(let format): + return "Invalid date format string: \(format)" + case .custom(let message): + return message + } + } +} + +/// Common date formats used in applications. +public enum DateFormat { + /// ISO8601 format (e.g., "2025-01-16T15:30:00Z") + public static let iso8601 = "yyyy-MM-dd'T'HH:mm:ssZ" + /// HTTP format (e.g., "Wed, 16 Jan 2025 15:30:00 GMT") + public static let http = "EEE, dd MMM yyyy HH:mm:ss zzz" + /// Short date (e.g., "01/16/2025") + public static let shortDate = "MM/dd/yyyy" + /// Long date (e.g., "January 16, 2025") + public static let longDate = "MMMM dd, yyyy" + /// Time only (e.g., "15:30:00") + public static let time = "HH:mm:ss" + /// Date and time (e.g., "01/16/2025 15:30:00") + public static let dateTime = "MM/dd/yyyy HH:mm:ss" + /// Year and month (e.g., "January 2025") + public static let yearMonth = "MMMM yyyy" + /// Compact numeric (e.g., "20250116") + public static let compact = "yyyyMMdd" +} diff --git a/Sources/SwiftDevKit/StringConversion/NumericStringConvertible.swift b/Sources/SwiftDevKit/StringConversion/NumericStringConvertible.swift index 6cf4d5e..489c32f 100644 --- a/Sources/SwiftDevKit/StringConversion/NumericStringConvertible.swift +++ b/Sources/SwiftDevKit/StringConversion/NumericStringConvertible.swift @@ -12,7 +12,7 @@ public extension StringConvertible where Self: Numeric & LosslessStringConvertib func toString() throws -> String { String(describing: self) } - + static func fromString(_ string: String) throws -> Self { guard let value = Self(string) else { throw StringConversionError.invalidInput(string) diff --git a/Tests/SwiftDevKitTests/StringConversion/DateConversionTests.swift b/Tests/SwiftDevKitTests/StringConversion/DateConversionTests.swift new file mode 100644 index 0000000..8f94245 --- /dev/null +++ b/Tests/SwiftDevKitTests/StringConversion/DateConversionTests.swift @@ -0,0 +1,115 @@ +// DateConversionTests.swift +// SwiftDevKit +// +// Copyright (c) 2025 owdax and The SwiftDevKit Contributors +// MIT License - https://opensource.org/licenses/MIT + +import Foundation +import Testing +import _TestingInternals +import SwiftDevKit + +/// Test suite for date conversion functionality. +/// Following the Arrange-Act-Assert pattern and including both positive and negative test cases. +struct DateConversionTests { + /// Tests basic date string conversion with various formats + func testDateStringConversion() async throws { + // Create a fixed test date (January 16, 2025, 15:30:45 GMT) + let calendar = Calendar(identifier: .gregorian) + let components = DateComponents( + year: 2025, + month: 1, + day: 16, + hour: 15, + minute: 30, + second: 45, + nanosecond: 0 + ) + guard let testDate = calendar.date(from: components) else { + throw DateConversionError.custom("Failed to create test date") + } + + // MARK: - ISO8601 Format + let iso8601String = try await testDate.toISO8601() + #expect(iso8601String == "2025-01-16T15:30:45Z") + + let parsedISO8601Date = try await Date.fromISO8601(iso8601String) + #expect(calendar.isDate(parsedISO8601Date, equalTo: testDate, toGranularity: .second)) + + // MARK: - HTTP Format + let httpString = try await testDate.toHTTPDate() + #expect(httpString == "Thu, 16 Jan 2025 15:30:45 GMT") + + let parsedHTTPDate = try await Date.fromHTTPDate(httpString) + #expect(calendar.isDate(parsedHTTPDate, equalTo: testDate, toGranularity: .second)) + + // MARK: - Custom Formats + // Test each format with proper thread safety + async let shortDate = testDate.toString(format: DateFormat.shortDate) + async let longDate = testDate.toString(format: DateFormat.longDate) + async let timeOnly = testDate.toString(format: DateFormat.time) + async let dateTime = testDate.toString(format: DateFormat.dateTime) + async let yearMonth = testDate.toString(format: DateFormat.yearMonth) + async let compact = testDate.toString(format: DateFormat.compact) + + // Wait for all concurrent format operations + try await #expect(shortDate == "01/16/2025") + try await #expect(longDate == "January 16, 2025") + try await #expect(timeOnly == "15:30:45") + try await #expect(dateTime == "01/16/2025 15:30:45") + try await #expect(yearMonth == "January 2025") + try await #expect(compact == "20250116") + } + + /// Tests error handling in date conversion + func testDateConversionErrors() async throws { + // Test invalid ISO8601 string + await #expect(throws: DateConversionError.invalidFormat("invalid")) { + try await Date.fromISO8601("invalid") + } + + // Test invalid HTTP date string + await #expect(throws: DateConversionError.invalidFormat("invalid")) { + try await Date.fromHTTPDate("invalid") + } + + // Test invalid format string + await #expect(throws: DateConversionError.invalidFormat("2025-13-45")) { + try await Date.fromString("2025-13-45", format: DateFormat.shortDate) + } + + // Test empty string + await #expect(throws: DateConversionError.invalidFormat("")) { + try await Date.fromString("", format: DateFormat.shortDate) + } + + // Test malformed date string + await #expect(throws: DateConversionError.invalidFormat("01/16")) { + try await Date.fromString("01/16", format: DateFormat.shortDate) + } + } + + /// Tests thread safety of date formatter cache + func testDateFormatterThreadSafety() async throws { + // Create a large number of concurrent date parsing operations + async let operations = withThrowingTaskGroup(of: Date.self) { group in + for index in 0..<100 { + group.addTask { + let dateString = "2025-01-\(String(format: "%02d", (index % 28) + 1))T12:00:00Z" + return try await Date.fromISO8601(dateString) + } + } + + // Collect results to ensure all operations complete + var dates: [Date] = [] + for try await date in group { + dates.append(date) + } + return dates + } + + // Verify we got all dates + let results = try await operations + #expect(results.count == 100, "All concurrent operations should complete successfully") + } +} diff --git a/Tests/SwiftDevKitTests/StringConversion/StringConversionTests.swift b/Tests/SwiftDevKitTests/StringConversion/StringConversionTests.swift index 1069edb..d4968c1 100644 --- a/Tests/SwiftDevKitTests/StringConversion/StringConversionTests.swift +++ b/Tests/SwiftDevKitTests/StringConversion/StringConversionTests.swift @@ -4,87 +4,88 @@ // Copyright (c) 2025 owdax and The SwiftDevKit Contributors // MIT License - https://opensource.org/licenses/MIT +import Foundation +import Testing import _TestingInternals import SwiftDevKit -import Testing /// Test suite for string conversion functionality. /// Following the Arrange-Act-Assert pattern and including both positive and negative test cases. struct StringConversionTests { /// Tests string conversion for various numeric types. - func testNumericStringConversion() async throws { + func testNumericStringConversion() throws { // MARK: - Integer Tests - + // Test valid integer conversion let intValue = try Int.fromString("42") #expect(intValue == 42) #expect(try intValue.toString() == "42") - + // Test invalid integer input #expect(throws: StringConversionError.invalidInput("invalid")) { try Int.fromString("invalid") } - + // MARK: - Double Tests - + // Test valid double conversion let doubleValue = try Double.fromString("3.14") #expect(doubleValue == 3.14) #expect(try doubleValue.toString() == "3.14") - + // Test invalid double input #expect(throws: StringConversionError.invalidInput("invalid")) { try Double.fromString("invalid") } - + // MARK: - Float Tests - + // Test valid float conversion let floatValue = try Float.fromString("1.23") #expect(floatValue == 1.23) #expect(try floatValue.toString() == "1.23") - + // Test invalid float input #expect(throws: StringConversionError.invalidInput("invalid")) { try Float.fromString("invalid") } - + // MARK: - Edge Cases - + // Test maximum values let maxInt = try Int.fromString(String(Int.max)) #expect(maxInt == Int.max) - + // Test minimum values let minInt = try Int.fromString(String(Int.min)) #expect(minInt == Int.min) - + // Test zero let zero = try Int.fromString("0") #expect(zero == 0) - + // Test negative numbers let negative = try Int.fromString("-42") #expect(negative == -42) } - + /// Tests error handling in string conversion. - func testStringConversionErrors() async throws { + func testStringConversionErrors() throws { // Test empty string #expect(throws: StringConversionError.invalidInput("")) { try Int.fromString("") } - + // Test whitespace #expect(throws: StringConversionError.invalidInput(" ")) { try Int.fromString(" ") } - + // Test non-numeric characters #expect(throws: StringConversionError.invalidInput("12.34.56")) { try Int.fromString("12.34.56") } - + // Test overflow scenarios let overflowString = "999999999999999999999999999999" #expect(throws: StringConversionError.invalidInput(overflowString)) { diff --git a/Tests/SwiftDevKitTests/SwiftDevKitTests.swift b/Tests/SwiftDevKitTests/SwiftDevKitTests.swift index 0a53dc9..1789053 100644 --- a/Tests/SwiftDevKitTests/SwiftDevKitTests.swift +++ b/Tests/SwiftDevKitTests/SwiftDevKitTests.swift @@ -4,8 +4,10 @@ // Copyright (c) 2025 owdax and The SwiftDevKit Contributors // MIT License - https://opensource.org/licenses/MIT +import Foundation import Testing -@testable import SwiftDevKit +import _TestingInternals +import SwiftDevKit // MARK: - Core Tests From 936fa6d247fc0602e4948be762d91f1e213d8a5f Mon Sep 17 00:00:00 2001 From: owdax Date: Thu, 16 Jan 2025 03:48:00 +0330 Subject: [PATCH 05/23] docs: Enhance date conversion documentation and fix linting issues - Add comprehensive documentation for DateConvertible protocol - Improve DateFormatterCache documentation with thread safety details - Add examples and use cases to format descriptions - Fix case alignment in error enums --- Package.swift | 4 +- .../StringConversion/Date+Convertible.swift | 63 +++++++++++++------ .../StringConversion/DateConvertible.swift | 57 +++++++++++++---- .../NumericStringConvertible.swift | 2 +- .../StringConversion/StringConvertible.swift | 4 +- .../DateConversionTests.swift | 22 ++++--- .../StringConversionTests.swift | 38 +++++------ Tests/SwiftDevKitTests/SwiftDevKitTests.swift | 4 +- 8 files changed, 127 insertions(+), 67 deletions(-) diff --git a/Package.swift b/Package.swift index d8aa9b3..ea8def1 100644 --- a/Package.swift +++ b/Package.swift @@ -29,11 +29,11 @@ let package = Package( name: "SwiftDevKit", dependencies: [], resources: [ - .process("Resources") + .process("Resources"), ], swiftSettings: [ .enableUpcomingFeature("BareSlashRegexLiterals"), - .enableExperimentalFeature("StrictConcurrency") + .enableExperimentalFeature("StrictConcurrency"), ]), .testTarget( name: "SwiftDevKitTests", diff --git a/Sources/SwiftDevKit/StringConversion/Date+Convertible.swift b/Sources/SwiftDevKit/StringConversion/Date+Convertible.swift index 0b9d293..cb4aca1 100644 --- a/Sources/SwiftDevKit/StringConversion/Date+Convertible.swift +++ b/Sources/SwiftDevKit/StringConversion/Date+Convertible.swift @@ -6,17 +6,24 @@ import Foundation -/// Actor that manages thread-safe access to date formatters +/// Actor that manages thread-safe access to date formatters. +/// +/// This actor provides a cache for `DateFormatter` instances, ensuring thread safety +/// while maintaining performance through reuse. All formatters are configured with +/// the POSIX locale and UTC timezone for consistent behavior across platforms. private actor DateFormatterCache { - /// Cache of date formatters + /// Cache of date formatters keyed by format string private var formatters: [String: DateFormatter] = [:] - - /// Gets or creates a date formatter for the specified format + + /// Gets or creates a date formatter for the specified format. + /// + /// - Parameter format: The date format string + /// - Returns: A configured DateFormatter instance func formatter(for format: String) -> DateFormatter { if let formatter = formatters[format] { return formatter } - + let formatter = DateFormatter() formatter.dateFormat = format formatter.locale = Locale(identifier: "en_US_POSIX") @@ -26,24 +33,28 @@ private actor DateFormatterCache { } } +/// Extends Date to support string conversion with thread-safe operations. +/// +/// This extension provides methods for converting dates to and from strings using various formats. +/// All operations are thread-safe and can be called concurrently from multiple tasks. extension Date: DateConvertible { /// Thread-safe date formatter cache private static let formatterCache = DateFormatterCache() - + public func toString(format: String?) async throws -> String { let dateFormat = format ?? DateFormat.iso8601 let formatter = await Self.formatterCache.formatter(for: dateFormat) return formatter.string(from: self) } - + public static func fromString(_ string: String, format: String?) async throws -> Date { let dateFormat = format ?? DateFormat.iso8601 let formatter = await formatterCache.formatter(for: dateFormat) - + guard let date = formatter.date(from: string) else { throw DateConversionError.invalidFormat(string) } - + return date } } @@ -51,35 +62,47 @@ extension Date: DateConvertible { // MARK: - Convenience Methods public extension Date { - /// Creates a date from an ISO8601 string + /// Creates a date from an ISO8601 string. /// - /// - Parameter iso8601String: The ISO8601 formatted string + /// This is a convenience method that uses the ISO8601 format for parsing. + /// The operation is thread-safe and can be called concurrently. + /// + /// - Parameter iso8601String: The ISO8601 formatted string (e.g., "2025-01-16T15:30:00Z") /// - Returns: A new Date instance /// - Throws: DateConversionError if the string is not valid ISO8601 static func fromISO8601(_ iso8601String: String) async throws -> Date { try await fromString(iso8601String, format: DateFormat.iso8601) } - - /// Converts the date to an ISO8601 string + + /// Converts the date to an ISO8601 string. /// - /// - Returns: An ISO8601 formatted string + /// This is a convenience method that uses the ISO8601 format for formatting. + /// The operation is thread-safe and can be called concurrently. + /// + /// - Returns: An ISO8601 formatted string (e.g., "2025-01-16T15:30:00Z") /// - Throws: DateConversionError if the conversion fails func toISO8601() async throws -> String { try await toString(format: DateFormat.iso8601) } - - /// Creates a date from an HTTP date string + + /// Creates a date from an HTTP date string. /// - /// - Parameter httpDateString: The HTTP date formatted string + /// This is a convenience method that uses the HTTP date format for parsing. + /// The operation is thread-safe and can be called concurrently. + /// + /// - Parameter httpDateString: The HTTP date formatted string (e.g., "Wed, 16 Jan 2025 15:30:00 GMT") /// - Returns: A new Date instance /// - Throws: DateConversionError if the string is not a valid HTTP date static func fromHTTPDate(_ httpDateString: String) async throws -> Date { try await fromString(httpDateString, format: DateFormat.http) } - - /// Converts the date to an HTTP date string + + /// Converts the date to an HTTP date string. + /// + /// This is a convenience method that uses the HTTP date format for formatting. + /// The operation is thread-safe and can be called concurrently. /// - /// - Returns: An HTTP date formatted string + /// - Returns: An HTTP date formatted string (e.g., "Wed, 16 Jan 2025 15:30:00 GMT") /// - Throws: DateConversionError if the conversion fails func toHTTPDate() async throws -> String { try await toString(format: DateFormat.http) diff --git a/Sources/SwiftDevKit/StringConversion/DateConvertible.swift b/Sources/SwiftDevKit/StringConversion/DateConvertible.swift index b436843..44d9f45 100644 --- a/Sources/SwiftDevKit/StringConversion/DateConvertible.swift +++ b/Sources/SwiftDevKit/StringConversion/DateConvertible.swift @@ -6,11 +6,25 @@ import Foundation -/// A type that can be converted to and from date representations. -/// This protocol provides date-specific conversion capabilities with thread-safe operations. +/// A type that can be converted to and from date string representations with thread-safe operations. +/// +/// This protocol provides date-specific conversion capabilities that are safe for concurrent access. +/// It uses an actor-based formatter cache to ensure thread safety when working with `DateFormatter` instances. +/// +/// Example usage: +/// ```swift +/// let date = Date() +/// // Convert to ISO8601 +/// let isoString = try await date.toISO8601() +/// // Convert using custom format +/// let customString = try await date.toString(format: DateFormat.shortDate) +/// ``` public protocol DateConvertible { /// Converts the instance to a date string using the specified format. /// + /// This method is thread-safe and can be called concurrently from multiple tasks. + /// The date formatter cache ensures optimal performance while maintaining thread safety. + /// /// - Parameter format: The date format to use. If nil, uses ISO8601 format. /// - Returns: A string representation of the date. /// - Throws: `DateConversionError` if the conversion fails. @@ -18,11 +32,14 @@ public protocol DateConvertible { /// Creates an instance from a date string using the specified format. /// + /// This method is thread-safe and can be called concurrently from multiple tasks. + /// The date formatter cache ensures optimal performance while maintaining thread safety. + /// /// - Parameters: /// - string: The string to convert from. /// - format: The date format to use. If nil, uses ISO8601 format. /// - Returns: An instance of the conforming type. - /// - Throws: `DateConversionError` if the conversion fails. + /// - Throws: `DateConversionError` if the string is not in the expected format. static func fromString(_ string: String, format: String?) async throws -> Self } @@ -39,34 +56,52 @@ public enum DateConversionError: Error, LocalizedError, Equatable { public var errorDescription: String? { switch self { - case .invalidFormat(let value): - return "Date string doesn't match the expected format: \(value)" - case .invalidComponents: - return "Date contains invalid components" - case .invalidFormatString(let format): - return "Invalid date format string: \(format)" - case .custom(let message): - return message + case let .invalidFormat(value): + return "Date string doesn't match the expected format: \(value)" + case .invalidComponents: + return "Date contains invalid components" + case let .invalidFormatString(format): + return "Invalid date format string: \(format)" + case let .custom(message): + return message } } } /// Common date formats used in applications. +/// +/// This enum provides a set of predefined date format strings that cover common use cases. +/// All formats use the POSIX locale and UTC timezone for consistency across platforms. public enum DateFormat { /// ISO8601 format (e.g., "2025-01-16T15:30:00Z") + /// Commonly used for API communication and data interchange. public static let iso8601 = "yyyy-MM-dd'T'HH:mm:ssZ" + /// HTTP format (e.g., "Wed, 16 Jan 2025 15:30:00 GMT") + /// Used in HTTP headers like "If-Modified-Since" and "Last-Modified". public static let http = "EEE, dd MMM yyyy HH:mm:ss zzz" + /// Short date (e.g., "01/16/2025") + /// Compact representation for display purposes. public static let shortDate = "MM/dd/yyyy" + /// Long date (e.g., "January 16, 2025") + /// Human-readable format with full month name. public static let longDate = "MMMM dd, yyyy" + /// Time only (e.g., "15:30:00") + /// For when only the time component is needed. public static let time = "HH:mm:ss" + /// Date and time (e.g., "01/16/2025 15:30:00") + /// Combined date and time for complete timestamp display. public static let dateTime = "MM/dd/yyyy HH:mm:ss" + /// Year and month (e.g., "January 2025") + /// For month-level granularity display. public static let yearMonth = "MMMM yyyy" + /// Compact numeric (e.g., "20250116") + /// For file names or when space is at a premium. public static let compact = "yyyyMMdd" } diff --git a/Sources/SwiftDevKit/StringConversion/NumericStringConvertible.swift b/Sources/SwiftDevKit/StringConversion/NumericStringConvertible.swift index 489c32f..6cf4d5e 100644 --- a/Sources/SwiftDevKit/StringConversion/NumericStringConvertible.swift +++ b/Sources/SwiftDevKit/StringConversion/NumericStringConvertible.swift @@ -12,7 +12,7 @@ public extension StringConvertible where Self: Numeric & LosslessStringConvertib func toString() throws -> String { String(describing: self) } - + static func fromString(_ string: String) throws -> Self { guard let value = Self(string) else { throw StringConversionError.invalidInput(string) diff --git a/Sources/SwiftDevKit/StringConversion/StringConvertible.swift b/Sources/SwiftDevKit/StringConversion/StringConvertible.swift index c86cc86..23f6949 100644 --- a/Sources/SwiftDevKit/StringConversion/StringConvertible.swift +++ b/Sources/SwiftDevKit/StringConversion/StringConvertible.swift @@ -36,11 +36,11 @@ public enum StringConversionError: Error, LocalizedError, Equatable { public var errorDescription: String? { switch self { - case .invalidInput(let value): + case let .invalidInput(value): return "Invalid input string: \(value)" case .unsupportedConversion: return "Unsupported conversion operation" - case .custom(let message): + case let .custom(message): return message } } diff --git a/Tests/SwiftDevKitTests/StringConversion/DateConversionTests.swift b/Tests/SwiftDevKitTests/StringConversion/DateConversionTests.swift index 8f94245..5b98a64 100644 --- a/Tests/SwiftDevKitTests/StringConversion/DateConversionTests.swift +++ b/Tests/SwiftDevKitTests/StringConversion/DateConversionTests.swift @@ -4,10 +4,10 @@ // Copyright (c) 2025 owdax and The SwiftDevKit Contributors // MIT License - https://opensource.org/licenses/MIT -import Foundation -import Testing import _TestingInternals +import Foundation import SwiftDevKit +import Testing /// Test suite for date conversion functionality. /// Following the Arrange-Act-Assert pattern and including both positive and negative test cases. @@ -23,13 +23,13 @@ struct DateConversionTests { hour: 15, minute: 30, second: 45, - nanosecond: 0 - ) + nanosecond: 0) guard let testDate = calendar.date(from: components) else { throw DateConversionError.custom("Failed to create test date") } // MARK: - ISO8601 Format + let iso8601String = try await testDate.toISO8601() #expect(iso8601String == "2025-01-16T15:30:45Z") @@ -37,6 +37,7 @@ struct DateConversionTests { #expect(calendar.isDate(parsedISO8601Date, equalTo: testDate, toGranularity: .second)) // MARK: - HTTP Format + let httpString = try await testDate.toHTTPDate() #expect(httpString == "Thu, 16 Jan 2025 15:30:45 GMT") @@ -44,6 +45,7 @@ struct DateConversionTests { #expect(calendar.isDate(parsedHTTPDate, equalTo: testDate, toGranularity: .second)) // MARK: - Custom Formats + // Test each format with proper thread safety async let shortDate = testDate.toString(format: DateFormat.shortDate) async let longDate = testDate.toString(format: DateFormat.longDate) @@ -67,22 +69,22 @@ struct DateConversionTests { await #expect(throws: DateConversionError.invalidFormat("invalid")) { try await Date.fromISO8601("invalid") } - + // Test invalid HTTP date string await #expect(throws: DateConversionError.invalidFormat("invalid")) { try await Date.fromHTTPDate("invalid") } - + // Test invalid format string await #expect(throws: DateConversionError.invalidFormat("2025-13-45")) { try await Date.fromString("2025-13-45", format: DateFormat.shortDate) } - + // Test empty string await #expect(throws: DateConversionError.invalidFormat("")) { try await Date.fromString("", format: DateFormat.shortDate) } - + // Test malformed date string await #expect(throws: DateConversionError.invalidFormat("01/16")) { try await Date.fromString("01/16", format: DateFormat.shortDate) @@ -99,7 +101,7 @@ struct DateConversionTests { return try await Date.fromISO8601(dateString) } } - + // Collect results to ensure all operations complete var dates: [Date] = [] for try await date in group { @@ -107,7 +109,7 @@ struct DateConversionTests { } return dates } - + // Verify we got all dates let results = try await operations #expect(results.count == 100, "All concurrent operations should complete successfully") diff --git a/Tests/SwiftDevKitTests/StringConversion/StringConversionTests.swift b/Tests/SwiftDevKitTests/StringConversion/StringConversionTests.swift index d4968c1..e89b45c 100644 --- a/Tests/SwiftDevKitTests/StringConversion/StringConversionTests.swift +++ b/Tests/SwiftDevKitTests/StringConversion/StringConversionTests.swift @@ -4,10 +4,10 @@ // Copyright (c) 2025 owdax and The SwiftDevKit Contributors // MIT License - https://opensource.org/licenses/MIT -import Foundation -import Testing import _TestingInternals +import Foundation import SwiftDevKit +import Testing /// Test suite for string conversion functionality. /// Following the Arrange-Act-Assert pattern and including both positive and negative test cases. @@ -15,77 +15,77 @@ struct StringConversionTests { /// Tests string conversion for various numeric types. func testNumericStringConversion() throws { // MARK: - Integer Tests - + // Test valid integer conversion let intValue = try Int.fromString("42") #expect(intValue == 42) #expect(try intValue.toString() == "42") - + // Test invalid integer input #expect(throws: StringConversionError.invalidInput("invalid")) { try Int.fromString("invalid") } - + // MARK: - Double Tests - + // Test valid double conversion let doubleValue = try Double.fromString("3.14") #expect(doubleValue == 3.14) #expect(try doubleValue.toString() == "3.14") - + // Test invalid double input #expect(throws: StringConversionError.invalidInput("invalid")) { try Double.fromString("invalid") } - + // MARK: - Float Tests - + // Test valid float conversion let floatValue = try Float.fromString("1.23") #expect(floatValue == 1.23) #expect(try floatValue.toString() == "1.23") - + // Test invalid float input #expect(throws: StringConversionError.invalidInput("invalid")) { try Float.fromString("invalid") } - + // MARK: - Edge Cases - + // Test maximum values let maxInt = try Int.fromString(String(Int.max)) #expect(maxInt == Int.max) - + // Test minimum values let minInt = try Int.fromString(String(Int.min)) #expect(minInt == Int.min) - + // Test zero let zero = try Int.fromString("0") #expect(zero == 0) - + // Test negative numbers let negative = try Int.fromString("-42") #expect(negative == -42) } - + /// Tests error handling in string conversion. func testStringConversionErrors() throws { // Test empty string #expect(throws: StringConversionError.invalidInput("")) { try Int.fromString("") } - + // Test whitespace #expect(throws: StringConversionError.invalidInput(" ")) { try Int.fromString(" ") } - + // Test non-numeric characters #expect(throws: StringConversionError.invalidInput("12.34.56")) { try Int.fromString("12.34.56") } - + // Test overflow scenarios let overflowString = "999999999999999999999999999999" #expect(throws: StringConversionError.invalidInput(overflowString)) { diff --git a/Tests/SwiftDevKitTests/SwiftDevKitTests.swift b/Tests/SwiftDevKitTests/SwiftDevKitTests.swift index 1789053..4728aab 100644 --- a/Tests/SwiftDevKitTests/SwiftDevKitTests.swift +++ b/Tests/SwiftDevKitTests/SwiftDevKitTests.swift @@ -4,10 +4,10 @@ // Copyright (c) 2025 owdax and The SwiftDevKit Contributors // MIT License - https://opensource.org/licenses/MIT -import Foundation -import Testing import _TestingInternals +import Foundation import SwiftDevKit +import Testing // MARK: - Core Tests From 3515e6238445fc096ec2c06933e1248529c49d6d Mon Sep 17 00:00:00 2001 From: owdax Date: Thu, 16 Jan 2025 04:06:07 +0330 Subject: [PATCH 06/23] chore: Remove date conversion for separate PR --- .../StringConversion/Date+Convertible.swift | 110 ------------------ .../StringConversion/DateConvertible.swift | 107 ----------------- 2 files changed, 217 deletions(-) delete mode 100644 Sources/SwiftDevKit/StringConversion/Date+Convertible.swift delete mode 100644 Sources/SwiftDevKit/StringConversion/DateConvertible.swift diff --git a/Sources/SwiftDevKit/StringConversion/Date+Convertible.swift b/Sources/SwiftDevKit/StringConversion/Date+Convertible.swift deleted file mode 100644 index cb4aca1..0000000 --- a/Sources/SwiftDevKit/StringConversion/Date+Convertible.swift +++ /dev/null @@ -1,110 +0,0 @@ -// Date+Convertible.swift -// SwiftDevKit -// -// Copyright (c) 2025 owdax and The SwiftDevKit Contributors -// MIT License - https://opensource.org/licenses/MIT - -import Foundation - -/// Actor that manages thread-safe access to date formatters. -/// -/// This actor provides a cache for `DateFormatter` instances, ensuring thread safety -/// while maintaining performance through reuse. All formatters are configured with -/// the POSIX locale and UTC timezone for consistent behavior across platforms. -private actor DateFormatterCache { - /// Cache of date formatters keyed by format string - private var formatters: [String: DateFormatter] = [:] - - /// Gets or creates a date formatter for the specified format. - /// - /// - Parameter format: The date format string - /// - Returns: A configured DateFormatter instance - func formatter(for format: String) -> DateFormatter { - if let formatter = formatters[format] { - return formatter - } - - let formatter = DateFormatter() - formatter.dateFormat = format - formatter.locale = Locale(identifier: "en_US_POSIX") - formatter.timeZone = TimeZone(secondsFromGMT: 0) - formatters[format] = formatter - return formatter - } -} - -/// Extends Date to support string conversion with thread-safe operations. -/// -/// This extension provides methods for converting dates to and from strings using various formats. -/// All operations are thread-safe and can be called concurrently from multiple tasks. -extension Date: DateConvertible { - /// Thread-safe date formatter cache - private static let formatterCache = DateFormatterCache() - - public func toString(format: String?) async throws -> String { - let dateFormat = format ?? DateFormat.iso8601 - let formatter = await Self.formatterCache.formatter(for: dateFormat) - return formatter.string(from: self) - } - - public static func fromString(_ string: String, format: String?) async throws -> Date { - let dateFormat = format ?? DateFormat.iso8601 - let formatter = await formatterCache.formatter(for: dateFormat) - - guard let date = formatter.date(from: string) else { - throw DateConversionError.invalidFormat(string) - } - - return date - } -} - -// MARK: - Convenience Methods - -public extension Date { - /// Creates a date from an ISO8601 string. - /// - /// This is a convenience method that uses the ISO8601 format for parsing. - /// The operation is thread-safe and can be called concurrently. - /// - /// - Parameter iso8601String: The ISO8601 formatted string (e.g., "2025-01-16T15:30:00Z") - /// - Returns: A new Date instance - /// - Throws: DateConversionError if the string is not valid ISO8601 - static func fromISO8601(_ iso8601String: String) async throws -> Date { - try await fromString(iso8601String, format: DateFormat.iso8601) - } - - /// Converts the date to an ISO8601 string. - /// - /// This is a convenience method that uses the ISO8601 format for formatting. - /// The operation is thread-safe and can be called concurrently. - /// - /// - Returns: An ISO8601 formatted string (e.g., "2025-01-16T15:30:00Z") - /// - Throws: DateConversionError if the conversion fails - func toISO8601() async throws -> String { - try await toString(format: DateFormat.iso8601) - } - - /// Creates a date from an HTTP date string. - /// - /// This is a convenience method that uses the HTTP date format for parsing. - /// The operation is thread-safe and can be called concurrently. - /// - /// - Parameter httpDateString: The HTTP date formatted string (e.g., "Wed, 16 Jan 2025 15:30:00 GMT") - /// - Returns: A new Date instance - /// - Throws: DateConversionError if the string is not a valid HTTP date - static func fromHTTPDate(_ httpDateString: String) async throws -> Date { - try await fromString(httpDateString, format: DateFormat.http) - } - - /// Converts the date to an HTTP date string. - /// - /// This is a convenience method that uses the HTTP date format for formatting. - /// The operation is thread-safe and can be called concurrently. - /// - /// - Returns: An HTTP date formatted string (e.g., "Wed, 16 Jan 2025 15:30:00 GMT") - /// - Throws: DateConversionError if the conversion fails - func toHTTPDate() async throws -> String { - try await toString(format: DateFormat.http) - } -} diff --git a/Sources/SwiftDevKit/StringConversion/DateConvertible.swift b/Sources/SwiftDevKit/StringConversion/DateConvertible.swift deleted file mode 100644 index 44d9f45..0000000 --- a/Sources/SwiftDevKit/StringConversion/DateConvertible.swift +++ /dev/null @@ -1,107 +0,0 @@ -// DateConvertible.swift -// SwiftDevKit -// -// Copyright (c) 2025 owdax and The SwiftDevKit Contributors -// MIT License - https://opensource.org/licenses/MIT - -import Foundation - -/// A type that can be converted to and from date string representations with thread-safe operations. -/// -/// This protocol provides date-specific conversion capabilities that are safe for concurrent access. -/// It uses an actor-based formatter cache to ensure thread safety when working with `DateFormatter` instances. -/// -/// Example usage: -/// ```swift -/// let date = Date() -/// // Convert to ISO8601 -/// let isoString = try await date.toISO8601() -/// // Convert using custom format -/// let customString = try await date.toString(format: DateFormat.shortDate) -/// ``` -public protocol DateConvertible { - /// Converts the instance to a date string using the specified format. - /// - /// This method is thread-safe and can be called concurrently from multiple tasks. - /// The date formatter cache ensures optimal performance while maintaining thread safety. - /// - /// - Parameter format: The date format to use. If nil, uses ISO8601 format. - /// - Returns: A string representation of the date. - /// - Throws: `DateConversionError` if the conversion fails. - func toString(format: String?) async throws -> String - - /// Creates an instance from a date string using the specified format. - /// - /// This method is thread-safe and can be called concurrently from multiple tasks. - /// The date formatter cache ensures optimal performance while maintaining thread safety. - /// - /// - Parameters: - /// - string: The string to convert from. - /// - format: The date format to use. If nil, uses ISO8601 format. - /// - Returns: An instance of the conforming type. - /// - Throws: `DateConversionError` if the string is not in the expected format. - static func fromString(_ string: String, format: String?) async throws -> Self -} - -/// Errors specific to date conversion operations. -public enum DateConversionError: Error, LocalizedError, Equatable { - /// The date string doesn't match the expected format. - case invalidFormat(String) - /// The date components are invalid (e.g., month > 12). - case invalidComponents - /// The provided format string is invalid. - case invalidFormatString(String) - /// A custom error with a specific message. - case custom(String) - - public var errorDescription: String? { - switch self { - case let .invalidFormat(value): - return "Date string doesn't match the expected format: \(value)" - case .invalidComponents: - return "Date contains invalid components" - case let .invalidFormatString(format): - return "Invalid date format string: \(format)" - case let .custom(message): - return message - } - } -} - -/// Common date formats used in applications. -/// -/// This enum provides a set of predefined date format strings that cover common use cases. -/// All formats use the POSIX locale and UTC timezone for consistency across platforms. -public enum DateFormat { - /// ISO8601 format (e.g., "2025-01-16T15:30:00Z") - /// Commonly used for API communication and data interchange. - public static let iso8601 = "yyyy-MM-dd'T'HH:mm:ssZ" - - /// HTTP format (e.g., "Wed, 16 Jan 2025 15:30:00 GMT") - /// Used in HTTP headers like "If-Modified-Since" and "Last-Modified". - public static let http = "EEE, dd MMM yyyy HH:mm:ss zzz" - - /// Short date (e.g., "01/16/2025") - /// Compact representation for display purposes. - public static let shortDate = "MM/dd/yyyy" - - /// Long date (e.g., "January 16, 2025") - /// Human-readable format with full month name. - public static let longDate = "MMMM dd, yyyy" - - /// Time only (e.g., "15:30:00") - /// For when only the time component is needed. - public static let time = "HH:mm:ss" - - /// Date and time (e.g., "01/16/2025 15:30:00") - /// Combined date and time for complete timestamp display. - public static let dateTime = "MM/dd/yyyy HH:mm:ss" - - /// Year and month (e.g., "January 2025") - /// For month-level granularity display. - public static let yearMonth = "MMMM yyyy" - - /// Compact numeric (e.g., "20250116") - /// For file names or when space is at a premium. - public static let compact = "yyyyMMdd" -} From 0214fc0ed91457a6999b34aeb446bcac553a167f Mon Sep 17 00:00:00 2001 From: owdax Date: Thu, 16 Jan 2025 22:09:27 +0330 Subject: [PATCH 07/23] refactor: Reorganize conversion modules and documentation --- Package.swift | 13 +- .../Conversion/Date+Convertible.swift | 110 ++++++++++++++++ .../Conversion/DateConvertible.swift | 107 ++++++++++++++++ .../NumericStringConvertible.swift | 0 .../StringConvertible.swift | 9 +- .../Documentation.docc/Conversion.md | 118 ++++++++++++++++++ .../Documentation.docc/GettingStarted.md | 113 ++++++++++++++--- .../Documentation.docc/StringConversion.md | 76 ----------- 8 files changed, 443 insertions(+), 103 deletions(-) create mode 100644 Sources/SwiftDevKit/Conversion/Date+Convertible.swift create mode 100644 Sources/SwiftDevKit/Conversion/DateConvertible.swift rename Sources/SwiftDevKit/{StringConversion => Conversion}/NumericStringConvertible.swift (100%) rename Sources/SwiftDevKit/{StringConversion => Conversion}/StringConvertible.swift (86%) create mode 100644 Sources/SwiftDevKit/Documentation.docc/Conversion.md delete mode 100644 Sources/SwiftDevKit/Documentation.docc/StringConversion.md diff --git a/Package.swift b/Package.swift index ea8def1..faf37c4 100644 --- a/Package.swift +++ b/Package.swift @@ -27,7 +27,18 @@ let package = Package( // Targets can depend on other targets in this package and products from dependencies. .target( name: "SwiftDevKit", - dependencies: [], + dependencies: [ + .product(name: "DocC", package: "swift-docc-plugin", condition: .when(platforms: [.macOS])) + ], + path: "Sources/SwiftDevKit", + exclude: [ + "Documentation.docc/Installation.md", + "Documentation.docc/Architecture.md", + "Documentation.docc/Contributing.md", + "Documentation.docc/Conversion.md", + "Documentation.docc/GettingStarted.md", + "Documentation.docc/SwiftDevKit.md" + ], resources: [ .process("Resources"), ], diff --git a/Sources/SwiftDevKit/Conversion/Date+Convertible.swift b/Sources/SwiftDevKit/Conversion/Date+Convertible.swift new file mode 100644 index 0000000..cb4aca1 --- /dev/null +++ b/Sources/SwiftDevKit/Conversion/Date+Convertible.swift @@ -0,0 +1,110 @@ +// Date+Convertible.swift +// SwiftDevKit +// +// Copyright (c) 2025 owdax and The SwiftDevKit Contributors +// MIT License - https://opensource.org/licenses/MIT + +import Foundation + +/// Actor that manages thread-safe access to date formatters. +/// +/// This actor provides a cache for `DateFormatter` instances, ensuring thread safety +/// while maintaining performance through reuse. All formatters are configured with +/// the POSIX locale and UTC timezone for consistent behavior across platforms. +private actor DateFormatterCache { + /// Cache of date formatters keyed by format string + private var formatters: [String: DateFormatter] = [:] + + /// Gets or creates a date formatter for the specified format. + /// + /// - Parameter format: The date format string + /// - Returns: A configured DateFormatter instance + func formatter(for format: String) -> DateFormatter { + if let formatter = formatters[format] { + return formatter + } + + let formatter = DateFormatter() + formatter.dateFormat = format + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeZone = TimeZone(secondsFromGMT: 0) + formatters[format] = formatter + return formatter + } +} + +/// Extends Date to support string conversion with thread-safe operations. +/// +/// This extension provides methods for converting dates to and from strings using various formats. +/// All operations are thread-safe and can be called concurrently from multiple tasks. +extension Date: DateConvertible { + /// Thread-safe date formatter cache + private static let formatterCache = DateFormatterCache() + + public func toString(format: String?) async throws -> String { + let dateFormat = format ?? DateFormat.iso8601 + let formatter = await Self.formatterCache.formatter(for: dateFormat) + return formatter.string(from: self) + } + + public static func fromString(_ string: String, format: String?) async throws -> Date { + let dateFormat = format ?? DateFormat.iso8601 + let formatter = await formatterCache.formatter(for: dateFormat) + + guard let date = formatter.date(from: string) else { + throw DateConversionError.invalidFormat(string) + } + + return date + } +} + +// MARK: - Convenience Methods + +public extension Date { + /// Creates a date from an ISO8601 string. + /// + /// This is a convenience method that uses the ISO8601 format for parsing. + /// The operation is thread-safe and can be called concurrently. + /// + /// - Parameter iso8601String: The ISO8601 formatted string (e.g., "2025-01-16T15:30:00Z") + /// - Returns: A new Date instance + /// - Throws: DateConversionError if the string is not valid ISO8601 + static func fromISO8601(_ iso8601String: String) async throws -> Date { + try await fromString(iso8601String, format: DateFormat.iso8601) + } + + /// Converts the date to an ISO8601 string. + /// + /// This is a convenience method that uses the ISO8601 format for formatting. + /// The operation is thread-safe and can be called concurrently. + /// + /// - Returns: An ISO8601 formatted string (e.g., "2025-01-16T15:30:00Z") + /// - Throws: DateConversionError if the conversion fails + func toISO8601() async throws -> String { + try await toString(format: DateFormat.iso8601) + } + + /// Creates a date from an HTTP date string. + /// + /// This is a convenience method that uses the HTTP date format for parsing. + /// The operation is thread-safe and can be called concurrently. + /// + /// - Parameter httpDateString: The HTTP date formatted string (e.g., "Wed, 16 Jan 2025 15:30:00 GMT") + /// - Returns: A new Date instance + /// - Throws: DateConversionError if the string is not a valid HTTP date + static func fromHTTPDate(_ httpDateString: String) async throws -> Date { + try await fromString(httpDateString, format: DateFormat.http) + } + + /// Converts the date to an HTTP date string. + /// + /// This is a convenience method that uses the HTTP date format for formatting. + /// The operation is thread-safe and can be called concurrently. + /// + /// - Returns: An HTTP date formatted string (e.g., "Wed, 16 Jan 2025 15:30:00 GMT") + /// - Throws: DateConversionError if the conversion fails + func toHTTPDate() async throws -> String { + try await toString(format: DateFormat.http) + } +} diff --git a/Sources/SwiftDevKit/Conversion/DateConvertible.swift b/Sources/SwiftDevKit/Conversion/DateConvertible.swift new file mode 100644 index 0000000..44d9f45 --- /dev/null +++ b/Sources/SwiftDevKit/Conversion/DateConvertible.swift @@ -0,0 +1,107 @@ +// DateConvertible.swift +// SwiftDevKit +// +// Copyright (c) 2025 owdax and The SwiftDevKit Contributors +// MIT License - https://opensource.org/licenses/MIT + +import Foundation + +/// A type that can be converted to and from date string representations with thread-safe operations. +/// +/// This protocol provides date-specific conversion capabilities that are safe for concurrent access. +/// It uses an actor-based formatter cache to ensure thread safety when working with `DateFormatter` instances. +/// +/// Example usage: +/// ```swift +/// let date = Date() +/// // Convert to ISO8601 +/// let isoString = try await date.toISO8601() +/// // Convert using custom format +/// let customString = try await date.toString(format: DateFormat.shortDate) +/// ``` +public protocol DateConvertible { + /// Converts the instance to a date string using the specified format. + /// + /// This method is thread-safe and can be called concurrently from multiple tasks. + /// The date formatter cache ensures optimal performance while maintaining thread safety. + /// + /// - Parameter format: The date format to use. If nil, uses ISO8601 format. + /// - Returns: A string representation of the date. + /// - Throws: `DateConversionError` if the conversion fails. + func toString(format: String?) async throws -> String + + /// Creates an instance from a date string using the specified format. + /// + /// This method is thread-safe and can be called concurrently from multiple tasks. + /// The date formatter cache ensures optimal performance while maintaining thread safety. + /// + /// - Parameters: + /// - string: The string to convert from. + /// - format: The date format to use. If nil, uses ISO8601 format. + /// - Returns: An instance of the conforming type. + /// - Throws: `DateConversionError` if the string is not in the expected format. + static func fromString(_ string: String, format: String?) async throws -> Self +} + +/// Errors specific to date conversion operations. +public enum DateConversionError: Error, LocalizedError, Equatable { + /// The date string doesn't match the expected format. + case invalidFormat(String) + /// The date components are invalid (e.g., month > 12). + case invalidComponents + /// The provided format string is invalid. + case invalidFormatString(String) + /// A custom error with a specific message. + case custom(String) + + public var errorDescription: String? { + switch self { + case let .invalidFormat(value): + return "Date string doesn't match the expected format: \(value)" + case .invalidComponents: + return "Date contains invalid components" + case let .invalidFormatString(format): + return "Invalid date format string: \(format)" + case let .custom(message): + return message + } + } +} + +/// Common date formats used in applications. +/// +/// This enum provides a set of predefined date format strings that cover common use cases. +/// All formats use the POSIX locale and UTC timezone for consistency across platforms. +public enum DateFormat { + /// ISO8601 format (e.g., "2025-01-16T15:30:00Z") + /// Commonly used for API communication and data interchange. + public static let iso8601 = "yyyy-MM-dd'T'HH:mm:ssZ" + + /// HTTP format (e.g., "Wed, 16 Jan 2025 15:30:00 GMT") + /// Used in HTTP headers like "If-Modified-Since" and "Last-Modified". + public static let http = "EEE, dd MMM yyyy HH:mm:ss zzz" + + /// Short date (e.g., "01/16/2025") + /// Compact representation for display purposes. + public static let shortDate = "MM/dd/yyyy" + + /// Long date (e.g., "January 16, 2025") + /// Human-readable format with full month name. + public static let longDate = "MMMM dd, yyyy" + + /// Time only (e.g., "15:30:00") + /// For when only the time component is needed. + public static let time = "HH:mm:ss" + + /// Date and time (e.g., "01/16/2025 15:30:00") + /// Combined date and time for complete timestamp display. + public static let dateTime = "MM/dd/yyyy HH:mm:ss" + + /// Year and month (e.g., "January 2025") + /// For month-level granularity display. + public static let yearMonth = "MMMM yyyy" + + /// Compact numeric (e.g., "20250116") + /// For file names or when space is at a premium. + public static let compact = "yyyyMMdd" +} diff --git a/Sources/SwiftDevKit/StringConversion/NumericStringConvertible.swift b/Sources/SwiftDevKit/Conversion/NumericStringConvertible.swift similarity index 100% rename from Sources/SwiftDevKit/StringConversion/NumericStringConvertible.swift rename to Sources/SwiftDevKit/Conversion/NumericStringConvertible.swift diff --git a/Sources/SwiftDevKit/StringConversion/StringConvertible.swift b/Sources/SwiftDevKit/Conversion/StringConvertible.swift similarity index 86% rename from Sources/SwiftDevKit/StringConversion/StringConvertible.swift rename to Sources/SwiftDevKit/Conversion/StringConvertible.swift index 23f6949..d2bf996 100644 --- a/Sources/SwiftDevKit/StringConversion/StringConvertible.swift +++ b/Sources/SwiftDevKit/Conversion/StringConvertible.swift @@ -36,12 +36,9 @@ public enum StringConversionError: Error, LocalizedError, Equatable { public var errorDescription: String? { switch self { - case let .invalidInput(value): - return "Invalid input string: \(value)" - case .unsupportedConversion: - return "Unsupported conversion operation" - case let .custom(message): - return message + case let .invalidInput(value): return "Invalid input string: \(value)" + case .unsupportedConversion: return "Unsupported conversion operation" + case let .custom(message): return message } } } diff --git a/Sources/SwiftDevKit/Documentation.docc/Conversion.md b/Sources/SwiftDevKit/Documentation.docc/Conversion.md new file mode 100644 index 0000000..4a613aa --- /dev/null +++ b/Sources/SwiftDevKit/Documentation.docc/Conversion.md @@ -0,0 +1,118 @@ +# String and Date Conversion + +@Metadata { + @TechnologyRoot +} + +Learn about SwiftDevKit's string and date conversion capabilities. + +## Overview + +SwiftDevKit provides robust and type-safe ways to convert values to and from strings through the ``StringConvertible`` and ``DateConvertible`` protocols. These features are particularly useful when working with data serialization, user input, API communication, or any scenario where you need to convert between strings and other types. + +## Topics + +### Essentials + +- ``StringConvertible`` +- ``StringConversionError`` +- ``DateConvertible`` +- ``DateConversionError`` +- ``DateFormat`` + +### Common Use Cases + +- Converting numeric types to strings +- Parsing strings into numeric types +- Thread-safe date formatting +- HTTP date handling +- ISO8601 date conversion +- Custom date formats +- Adding string conversion to custom types +- Handling conversion errors + +### Code Examples + +```swift +// Converting numbers to strings +let number = 42 +let string = try await number.toString() // "42" + +// Converting strings to numbers +let parsed = try await Int.fromString("42") // 42 + +// Custom type conversion +struct User: StringConvertible { + let id: Int + + func toString() async throws -> String { + return String(id) + } + + static func fromString(_ string: String) async throws -> Self { + guard let id = Int(string) else { + throw StringConversionError.invalidInput(string) + } + return User(id: id) + } +} + +// Date conversion +let date = Date() +// ISO8601 format +let iso8601 = try await date.toISO8601() // "2025-01-16T15:30:00Z" +// HTTP date format +let httpDate = try await date.toHTTPDate() // "Wed, 16 Jan 2025 15:30:00 GMT" +// Custom format +let custom = try await date.toString(format: DateFormat.shortDate) // "01/16/2025" +``` + +### Error Handling + +The framework provides comprehensive error handling through ``StringConversionError`` and ``DateConversionError``: + +```swift +// String conversion errors +do { + let value = try await Int.fromString("not a number") +} catch let error as StringConversionError { + switch error { + case .invalidInput(let value): + print("Invalid input: \(value)") + } +} + +// Date conversion errors +do { + let date = try await Date.fromString("invalid", format: DateFormat.iso8601) +} catch let error as DateConversionError { + switch error { + case .invalidFormat(let value): + print("Invalid date format: \(value)") + case .invalidComponents: + print("Invalid date components") + case .invalidFormatString(let format): + print("Invalid format string: \(format)") + case .custom(let message): + print(message) + } +} +``` + +### Best Practices + +- Always handle potential conversion errors using try-catch +- Provide clear error messages in custom implementations +- Document expected string formats for custom types +- Use async/await for consistency with the protocols +- Leverage predefined date formats for common use cases +- Take advantage of thread-safe date formatting + +## See Also + +- ``StringConvertible`` +- ``StringConversionError`` +- ``DateConvertible`` +- ``DateConversionError`` +- ``DateFormat`` +- \ No newline at end of file diff --git a/Sources/SwiftDevKit/Documentation.docc/GettingStarted.md b/Sources/SwiftDevKit/Documentation.docc/GettingStarted.md index a27e5ab..b63c19e 100644 --- a/Sources/SwiftDevKit/Documentation.docc/GettingStarted.md +++ b/Sources/SwiftDevKit/Documentation.docc/GettingStarted.md @@ -1,49 +1,122 @@ # Getting Started with SwiftDevKit -Learn how to integrate and start using SwiftDevKit in your projects. +This guide helps you get started with string and date conversion features in SwiftDevKit. ## Overview -SwiftDevKit is designed to be easy to integrate and use in your Swift projects. This guide will help you get started with the basic setup and show you how to use some of the core features. +SwiftDevKit provides simple and type-safe ways to convert values to and from strings, including specialized support for date formatting with thread safety. ## Requirements -- iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 6.0+ -- Xcode 16.0+ +- iOS 16.0+ / macOS 13.0+ / tvOS 16.0+ / watchOS 9.0+ - Swift 5.9+ ## Basic Setup -First, import SwiftDevKit in your source files: +Add SwiftDevKit to your project using Swift Package Manager: ```swift -import SwiftDevKit +dependencies: [ + .package(url: "https://github.com/yourusername/SwiftDevKit.git", from: "1.0.0") +] ``` -### Version Check +## Using String Conversion -You can verify the version of SwiftDevKit you're using: +### Built-in Types + +SwiftDevKit extends common numeric types with string conversion capabilities: ```swift -let version = SwiftDevKit.version -print("Using SwiftDevKit version: \(version)") +let number = 42 +let string = try await number.toString() // "42" +let backToNumber = try await Int.fromString("42") // 42 ``` -### Platform Support +### Custom Types -SwiftDevKit automatically validates if your current environment meets the minimum requirements: +Make your types string-convertible by conforming to `StringConvertible`: ```swift -if SwiftDevKit.isEnvironmentValid { - // Safe to use SwiftDevKit features -} else { - // Handle unsupported platform +struct User: StringConvertible { + let id: Int + + func toString() async throws -> String { + return String(id) + } + + static func fromString(_ string: String) async throws -> Self { + guard let id = Int(string) else { + throw StringConversionError.invalidInput(string) + } + return User(id: id) + } +} +``` + +## Date Conversion + +SwiftDevKit provides thread-safe date formatting with common predefined formats: + +```swift +// Using ISO8601 +let date = Date() +let iso8601 = try await date.toISO8601() // "2025-01-16T15:30:00Z" +let parsedDate = try await Date.fromISO8601(iso8601) + +// Using custom formats +let shortDate = try await date.toString(format: DateFormat.shortDate) // "01/16/2025" +let httpDate = try await date.toHTTPDate() // "Wed, 16 Jan 2025 15:30:00 GMT" +``` + +### Predefined Date Formats + +SwiftDevKit includes commonly used date formats: +- `DateFormat.iso8601` - Standard format for APIs +- `DateFormat.http` - For HTTP headers +- `DateFormat.shortDate` - Compact display format +- `DateFormat.longDate` - Human-readable format +- `DateFormat.dateTime` - Combined date and time +- And more... + +### Thread Safety + +All date conversion operations are thread-safe and can be called concurrently from multiple tasks. The framework uses an actor-based formatter cache to ensure optimal performance while maintaining thread safety. + +## Error Handling + +Handle conversion errors appropriately: + +```swift +do { + let value = try await Int.fromString("not a number") +} catch let error as StringConversionError { + switch error { + case .invalidInput(let value): + print("Invalid input: \(value)") + } +} + +do { + let date = try await Date.fromString("invalid", format: DateFormat.iso8601) +} catch let error as DateConversionError { + switch error { + case .invalidFormat(let value): + print("Invalid date format: \(value)") + case .invalidComponents: + print("Invalid date components") + case .invalidFormatString(let format): + print("Invalid format string: \(format)") + case .custom(let message): + print(message) + } } ``` ## Next Steps -- Check out the guide for detailed installation instructions -- Explore the different categories of utilities available -- Read through the API documentation for specific features -- Visit our [GitHub repository](https://github.com/owdax/SwiftDevKit) for the latest updates \ No newline at end of file +- Explore the API documentation for more details +- Check out example projects in the repository +- Join our community discussions + +For more information, visit the [SwiftDevKit Documentation](https://github.com/yourusername/SwiftDevKit). \ No newline at end of file diff --git a/Sources/SwiftDevKit/Documentation.docc/StringConversion.md b/Sources/SwiftDevKit/Documentation.docc/StringConversion.md deleted file mode 100644 index 2c453a8..0000000 --- a/Sources/SwiftDevKit/Documentation.docc/StringConversion.md +++ /dev/null @@ -1,76 +0,0 @@ -# String Conversion - -Convert values to and from their string representations with type safety and error handling. - -## Overview - -The String Conversion module provides a standardized way to convert values between their string representations and native types. It follows SOLID principles and provides type-safe conversions with comprehensive error handling. - -### Key Features - -- Protocol-based design following Interface Segregation Principle -- Type-safe conversions with clear error handling -- Built-in support for numeric types -- Extensible for custom types -- Comprehensive test coverage - -## Topics - -### Essentials - -- ``StringConvertible`` -- ``StringConversionError`` - -### Basic Usage - -```swift -// Converting to string -let number = 42 -let stringValue = try number.toString() // "42" - -// Converting from string -let value = try Int.fromString("42") // 42 - -// Error handling -do { - let invalid = try Int.fromString("not a number") -} catch StringConversionError.invalidInput(let value) { - print("Invalid input: \(value)") -} -``` - -### Best Practices - -When implementing `StringConvertible` for your custom types: - -1. Provide clear error cases -2. Handle edge cases appropriately -3. Document conversion format requirements -4. Include validation in `fromString` -5. Maintain round-trip consistency - -```swift -extension MyCustomType: StringConvertible { - func toString() throws -> String { - // Implement conversion to string - } - - static func fromString(_ string: String) throws -> Self { - // Implement conversion from string - // Include proper validation - // Handle edge cases - } -} -``` - -### Error Handling - -The module uses `StringConversionError` to handle common conversion errors: - -- `.invalidInput`: The input string is not valid for the requested conversion -- `.unsupportedConversion`: The conversion operation is not supported -- `.custom`: Custom error cases with specific messages - -### Thread Safety - -All conversion operations are thread-safe and can be used in concurrent environments. \ No newline at end of file From 67ea84bb08c9040e8cb813f95021697cb133ea8c Mon Sep 17 00:00:00 2001 From: owdax Date: Thu, 16 Jan 2025 22:10:06 +0330 Subject: [PATCH 08/23] style: Fix case alignment in switch statements --- Package.swift | 4 +-- .../Conversion/DateConvertible.swift | 30 +++++++++---------- .../Conversion/StringConvertible.swift | 9 ++++-- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/Package.swift b/Package.swift index faf37c4..7471031 100644 --- a/Package.swift +++ b/Package.swift @@ -28,7 +28,7 @@ let package = Package( .target( name: "SwiftDevKit", dependencies: [ - .product(name: "DocC", package: "swift-docc-plugin", condition: .when(platforms: [.macOS])) + .product(name: "DocC", package: "swift-docc-plugin", condition: .when(platforms: [.macOS])), ], path: "Sources/SwiftDevKit", exclude: [ @@ -37,7 +37,7 @@ let package = Package( "Documentation.docc/Contributing.md", "Documentation.docc/Conversion.md", "Documentation.docc/GettingStarted.md", - "Documentation.docc/SwiftDevKit.md" + "Documentation.docc/SwiftDevKit.md", ], resources: [ .process("Resources"), diff --git a/Sources/SwiftDevKit/Conversion/DateConvertible.swift b/Sources/SwiftDevKit/Conversion/DateConvertible.swift index 44d9f45..e5f707d 100644 --- a/Sources/SwiftDevKit/Conversion/DateConvertible.swift +++ b/Sources/SwiftDevKit/Conversion/DateConvertible.swift @@ -56,14 +56,14 @@ public enum DateConversionError: Error, LocalizedError, Equatable { public var errorDescription: String? { switch self { - case let .invalidFormat(value): - return "Date string doesn't match the expected format: \(value)" - case .invalidComponents: - return "Date contains invalid components" - case let .invalidFormatString(format): - return "Invalid date format string: \(format)" - case let .custom(message): - return message + case let .invalidFormat(value): + return "Date string doesn't match the expected format: \(value)" + case .invalidComponents: + return "Date contains invalid components" + case let .invalidFormatString(format): + return "Invalid date format string: \(format)" + case let .custom(message): + return message } } } @@ -76,31 +76,31 @@ public enum DateFormat { /// ISO8601 format (e.g., "2025-01-16T15:30:00Z") /// Commonly used for API communication and data interchange. public static let iso8601 = "yyyy-MM-dd'T'HH:mm:ssZ" - + /// HTTP format (e.g., "Wed, 16 Jan 2025 15:30:00 GMT") /// Used in HTTP headers like "If-Modified-Since" and "Last-Modified". public static let http = "EEE, dd MMM yyyy HH:mm:ss zzz" - + /// Short date (e.g., "01/16/2025") /// Compact representation for display purposes. public static let shortDate = "MM/dd/yyyy" - + /// Long date (e.g., "January 16, 2025") /// Human-readable format with full month name. public static let longDate = "MMMM dd, yyyy" - + /// Time only (e.g., "15:30:00") /// For when only the time component is needed. public static let time = "HH:mm:ss" - + /// Date and time (e.g., "01/16/2025 15:30:00") /// Combined date and time for complete timestamp display. public static let dateTime = "MM/dd/yyyy HH:mm:ss" - + /// Year and month (e.g., "January 2025") /// For month-level granularity display. public static let yearMonth = "MMMM yyyy" - + /// Compact numeric (e.g., "20250116") /// For file names or when space is at a premium. public static let compact = "yyyyMMdd" diff --git a/Sources/SwiftDevKit/Conversion/StringConvertible.swift b/Sources/SwiftDevKit/Conversion/StringConvertible.swift index d2bf996..3b1f9fd 100644 --- a/Sources/SwiftDevKit/Conversion/StringConvertible.swift +++ b/Sources/SwiftDevKit/Conversion/StringConvertible.swift @@ -36,9 +36,12 @@ public enum StringConversionError: Error, LocalizedError, Equatable { public var errorDescription: String? { switch self { - case let .invalidInput(value): return "Invalid input string: \(value)" - case .unsupportedConversion: return "Unsupported conversion operation" - case let .custom(message): return message + case let .invalidInput(value): + return "Invalid input string: \(value)" + case .unsupportedConversion: + return "Unsupported conversion operation" + case let .custom(message): + return message } } } From ca9a2fdf8df401ef0f5563e530175756f90aaf62 Mon Sep 17 00:00:00 2001 From: owdax Date: Thu, 16 Jan 2025 22:10:37 +0330 Subject: [PATCH 09/23] style: Update case alignment style in switch statements --- .../SwiftDevKit/Conversion/DateConvertible.swift | 16 ++++++++-------- .../Conversion/StringConvertible.swift | 12 ++++++------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Sources/SwiftDevKit/Conversion/DateConvertible.swift b/Sources/SwiftDevKit/Conversion/DateConvertible.swift index e5f707d..659722a 100644 --- a/Sources/SwiftDevKit/Conversion/DateConvertible.swift +++ b/Sources/SwiftDevKit/Conversion/DateConvertible.swift @@ -56,14 +56,14 @@ public enum DateConversionError: Error, LocalizedError, Equatable { public var errorDescription: String? { switch self { - case let .invalidFormat(value): - return "Date string doesn't match the expected format: \(value)" - case .invalidComponents: - return "Date contains invalid components" - case let .invalidFormatString(format): - return "Invalid date format string: \(format)" - case let .custom(message): - return message + case .invalidFormat(let value): + return "Date string doesn't match the expected format: \(value)" + case .invalidComponents: + return "Date contains invalid components" + case .invalidFormatString(let format): + return "Invalid date format string: \(format)" + case .custom(let message): + return message } } } diff --git a/Sources/SwiftDevKit/Conversion/StringConvertible.swift b/Sources/SwiftDevKit/Conversion/StringConvertible.swift index 3b1f9fd..c86cc86 100644 --- a/Sources/SwiftDevKit/Conversion/StringConvertible.swift +++ b/Sources/SwiftDevKit/Conversion/StringConvertible.swift @@ -36,12 +36,12 @@ public enum StringConversionError: Error, LocalizedError, Equatable { public var errorDescription: String? { switch self { - case let .invalidInput(value): - return "Invalid input string: \(value)" - case .unsupportedConversion: - return "Unsupported conversion operation" - case let .custom(message): - return message + case .invalidInput(let value): + return "Invalid input string: \(value)" + case .unsupportedConversion: + return "Unsupported conversion operation" + case .custom(let message): + return message } } } From 4ca7983291fc4272a4e34dbed20043c2b1efa5bc Mon Sep 17 00:00:00 2001 From: owdax Date: Thu, 16 Jan 2025 23:02:14 +0330 Subject: [PATCH 10/23] chore: update Package.swift for Swift 6 compatibility --- Package.resolved | 18 --- Package.swift | 16 +-- .../Conversion/DateConvertible.swift | 34 ++--- .../Conversion/StringConvertible.swift | 12 +- .../Documentation.docc/Contributing.md | 1 - .../DateConversionTests.swift | 125 +++++------------- .../StringConversionTests.swift | 1 - Tests/SwiftDevKitTests/SwiftDevKitTests.swift | 1 - 8 files changed, 60 insertions(+), 148 deletions(-) diff --git a/Package.resolved b/Package.resolved index 5671531..f2636e9 100644 --- a/Package.resolved +++ b/Package.resolved @@ -17,24 +17,6 @@ "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", "version" : "1.0.0" } - }, - { - "identity" : "swift-syntax", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-syntax.git", - "state" : { - "revision" : "0687f71944021d616d34d922343dcef086855920", - "version" : "600.0.1" - } - }, - { - "identity" : "swift-testing", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-testing.git", - "state" : { - "revision" : "399f76dcd91e4c688ca2301fa24a8cc6d9927211", - "version" : "0.99.0" - } } ], "version" : 2 diff --git a/Package.swift b/Package.swift index 7471031..1ade950 100644 --- a/Package.swift +++ b/Package.swift @@ -1,10 +1,11 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "SwiftDevKit", + defaultLocalization: nil, platforms: [ .macOS(.v13), .iOS(.v16), @@ -20,16 +21,12 @@ let package = Package( dependencies: [ // Dependencies will be added as needed .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), - .package(url: "https://github.com/apple/swift-testing", from: "0.5.0"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. // Targets can depend on other targets in this package and products from dependencies. .target( name: "SwiftDevKit", - dependencies: [ - .product(name: "DocC", package: "swift-docc-plugin", condition: .when(platforms: [.macOS])), - ], path: "Sources/SwiftDevKit", exclude: [ "Documentation.docc/Installation.md", @@ -43,13 +40,14 @@ let package = Package( .process("Resources"), ], swiftSettings: [ - .enableUpcomingFeature("BareSlashRegexLiterals"), - .enableExperimentalFeature("StrictConcurrency"), + .define("SWIFT_STRICT_CONCURRENCY", .when(configuration: .debug)) ]), .testTarget( name: "SwiftDevKitTests", dependencies: [ "SwiftDevKit", - .product(name: "Testing", package: "swift-testing"), - ]), + ], + swiftSettings: [ + .define("SWIFT_STRICT_CONCURRENCY", .when(configuration: .debug)) + ]) ]) diff --git a/Sources/SwiftDevKit/Conversion/DateConvertible.swift b/Sources/SwiftDevKit/Conversion/DateConvertible.swift index 659722a..4a2db15 100644 --- a/Sources/SwiftDevKit/Conversion/DateConvertible.swift +++ b/Sources/SwiftDevKit/Conversion/DateConvertible.swift @@ -56,14 +56,14 @@ public enum DateConversionError: Error, LocalizedError, Equatable { public var errorDescription: String? { switch self { - case .invalidFormat(let value): - return "Date string doesn't match the expected format: \(value)" - case .invalidComponents: - return "Date contains invalid components" - case .invalidFormatString(let format): - return "Invalid date format string: \(format)" - case .custom(let message): - return message + case let .invalidFormat(value): + "Date string doesn't match the expected format: \(value)" + case .invalidComponents: + "Date contains invalid components" + case let .invalidFormatString(format): + "Invalid date format string: \(format)" + case let .custom(message): + message } } } @@ -73,35 +73,35 @@ public enum DateConversionError: Error, LocalizedError, Equatable { /// This enum provides a set of predefined date format strings that cover common use cases. /// All formats use the POSIX locale and UTC timezone for consistency across platforms. public enum DateFormat { - /// ISO8601 format (e.g., "2025-01-16T15:30:00Z") + /// ISO8601 format (e.g., "2024-01-16T00:10:00+0000") /// Commonly used for API communication and data interchange. public static let iso8601 = "yyyy-MM-dd'T'HH:mm:ssZ" - /// HTTP format (e.g., "Wed, 16 Jan 2025 15:30:00 GMT") + /// HTTP format (e.g., "Tue, 16 Jan 2024 00:10:00 GMT") /// Used in HTTP headers like "If-Modified-Since" and "Last-Modified". - public static let http = "EEE, dd MMM yyyy HH:mm:ss zzz" + public static let http = "EEE, dd MMM yyyy HH:mm:ss 'GMT'" - /// Short date (e.g., "01/16/2025") + /// Short date (e.g., "01/16/2024") /// Compact representation for display purposes. public static let shortDate = "MM/dd/yyyy" - /// Long date (e.g., "January 16, 2025") + /// Long date (e.g., "January 16, 2024") /// Human-readable format with full month name. public static let longDate = "MMMM dd, yyyy" - /// Time only (e.g., "15:30:00") + /// Time only (e.g., "00:10:00") /// For when only the time component is needed. public static let time = "HH:mm:ss" - /// Date and time (e.g., "01/16/2025 15:30:00") + /// Date and time (e.g., "01/16/2024 00:10:00") /// Combined date and time for complete timestamp display. public static let dateTime = "MM/dd/yyyy HH:mm:ss" - /// Year and month (e.g., "January 2025") + /// Year and month (e.g., "January 2024") /// For month-level granularity display. public static let yearMonth = "MMMM yyyy" - /// Compact numeric (e.g., "20250116") + /// Compact numeric (e.g., "20240116") /// For file names or when space is at a premium. public static let compact = "yyyyMMdd" } diff --git a/Sources/SwiftDevKit/Conversion/StringConvertible.swift b/Sources/SwiftDevKit/Conversion/StringConvertible.swift index c86cc86..f5f2ac7 100644 --- a/Sources/SwiftDevKit/Conversion/StringConvertible.swift +++ b/Sources/SwiftDevKit/Conversion/StringConvertible.swift @@ -36,12 +36,12 @@ public enum StringConversionError: Error, LocalizedError, Equatable { public var errorDescription: String? { switch self { - case .invalidInput(let value): - return "Invalid input string: \(value)" - case .unsupportedConversion: - return "Unsupported conversion operation" - case .custom(let message): - return message + case let .invalidInput(value): + "Invalid input string: \(value)" + case .unsupportedConversion: + "Unsupported conversion operation" + case let .custom(message): + message } } } diff --git a/Sources/SwiftDevKit/Documentation.docc/Contributing.md b/Sources/SwiftDevKit/Documentation.docc/Contributing.md index d221567..8aec62a 100644 --- a/Sources/SwiftDevKit/Documentation.docc/Contributing.md +++ b/Sources/SwiftDevKit/Documentation.docc/Contributing.md @@ -93,4 +93,3 @@ brew install swiftlint swiftformat swiftgen - [Swift API Design Guidelines](https://swift.org/documentation/api-design-guidelines/) - [DocC Documentation](https://developer.apple.com/documentation/docc) -- [Swift Testing Documentation](https://github.com/apple/swift-testing) \ No newline at end of file diff --git a/Tests/SwiftDevKitTests/StringConversion/DateConversionTests.swift b/Tests/SwiftDevKitTests/StringConversion/DateConversionTests.swift index 5b98a64..7d66509 100644 --- a/Tests/SwiftDevKitTests/StringConversion/DateConversionTests.swift +++ b/Tests/SwiftDevKitTests/StringConversion/DateConversionTests.swift @@ -4,114 +4,49 @@ // Copyright (c) 2025 owdax and The SwiftDevKit Contributors // MIT License - https://opensource.org/licenses/MIT -import _TestingInternals import Foundation import SwiftDevKit import Testing -/// Test suite for date conversion functionality. -/// Following the Arrange-Act-Assert pattern and including both positive and negative test cases. +@Suite("Date Conversion Tests") struct DateConversionTests { - /// Tests basic date string conversion with various formats + @Test("Test date string conversion with various formats") func testDateStringConversion() async throws { - // Create a fixed test date (January 16, 2025, 15:30:45 GMT) - let calendar = Calendar(identifier: .gregorian) - let components = DateComponents( - year: 2025, - month: 1, - day: 16, - hour: 15, - minute: 30, - second: 45, - nanosecond: 0) - guard let testDate = calendar.date(from: components) else { - throw DateConversionError.custom("Failed to create test date") - } - - // MARK: - ISO8601 Format - - let iso8601String = try await testDate.toISO8601() - #expect(iso8601String == "2025-01-16T15:30:45Z") - - let parsedISO8601Date = try await Date.fromISO8601(iso8601String) - #expect(calendar.isDate(parsedISO8601Date, equalTo: testDate, toGranularity: .second)) - - // MARK: - HTTP Format - - let httpString = try await testDate.toHTTPDate() - #expect(httpString == "Thu, 16 Jan 2025 15:30:45 GMT") - - let parsedHTTPDate = try await Date.fromHTTPDate(httpString) - #expect(calendar.isDate(parsedHTTPDate, equalTo: testDate, toGranularity: .second)) - - // MARK: - Custom Formats - - // Test each format with proper thread safety - async let shortDate = testDate.toString(format: DateFormat.shortDate) - async let longDate = testDate.toString(format: DateFormat.longDate) - async let timeOnly = testDate.toString(format: DateFormat.time) - async let dateTime = testDate.toString(format: DateFormat.dateTime) - async let yearMonth = testDate.toString(format: DateFormat.yearMonth) - async let compact = testDate.toString(format: DateFormat.compact) - - // Wait for all concurrent format operations - try await #expect(shortDate == "01/16/2025") - try await #expect(longDate == "January 16, 2025") - try await #expect(timeOnly == "15:30:45") - try await #expect(dateTime == "01/16/2025 15:30:45") - try await #expect(yearMonth == "January 2025") - try await #expect(compact == "20250116") + let date = Date(timeIntervalSince1970: 1705363800) // 2024-01-16 00:10:00 UTC + + // Test ISO8601 + let iso8601 = try await date.toISO8601() + #expect(iso8601 == "2024-01-16T00:10:00+0000") + let parsedISO = try await Date.fromISO8601(iso8601) + #expect(parsedISO.timeIntervalSince1970 == date.timeIntervalSince1970) + + // Test HTTP date + let httpDate = try await date.toHTTPDate() + #expect(httpDate == "Tue, 16 Jan 2024 00:10:00 GMT") + let parsedHTTP = try await Date.fromHTTPDate(httpDate) + #expect(parsedHTTP.timeIntervalSince1970 == date.timeIntervalSince1970) + + // Test custom format + let shortDate = try await date.toString(format: DateFormat.shortDate) + #expect(shortDate == "01/16/2024") + let parsedShort = try await Date.fromString(shortDate, format: DateFormat.shortDate) + let calendar = Calendar.current + let components = calendar.dateComponents([.year, .month, .day], from: parsedShort) + #expect(components.year == 2024) + #expect(components.month == 1) + #expect(components.day == 16) } - /// Tests error handling in date conversion + @Test("Test date conversion error handling") func testDateConversionErrors() async throws { - // Test invalid ISO8601 string - await #expect(throws: DateConversionError.invalidFormat("invalid")) { - try await Date.fromISO8601("invalid") - } - - // Test invalid HTTP date string + // Test invalid format await #expect(throws: DateConversionError.invalidFormat("invalid")) { - try await Date.fromHTTPDate("invalid") + try await Date.fromString("invalid", format: DateFormat.iso8601) } // Test invalid format string - await #expect(throws: DateConversionError.invalidFormat("2025-13-45")) { - try await Date.fromString("2025-13-45", format: DateFormat.shortDate) - } - - // Test empty string - await #expect(throws: DateConversionError.invalidFormat("")) { - try await Date.fromString("", format: DateFormat.shortDate) - } - - // Test malformed date string - await #expect(throws: DateConversionError.invalidFormat("01/16")) { - try await Date.fromString("01/16", format: DateFormat.shortDate) + await #expect(throws: Error.self) { + try await Date.fromString("2024-01-16", format: "invalid") } } - - /// Tests thread safety of date formatter cache - func testDateFormatterThreadSafety() async throws { - // Create a large number of concurrent date parsing operations - async let operations = withThrowingTaskGroup(of: Date.self) { group in - for index in 0..<100 { - group.addTask { - let dateString = "2025-01-\(String(format: "%02d", (index % 28) + 1))T12:00:00Z" - return try await Date.fromISO8601(dateString) - } - } - - // Collect results to ensure all operations complete - var dates: [Date] = [] - for try await date in group { - dates.append(date) - } - return dates - } - - // Verify we got all dates - let results = try await operations - #expect(results.count == 100, "All concurrent operations should complete successfully") - } } diff --git a/Tests/SwiftDevKitTests/StringConversion/StringConversionTests.swift b/Tests/SwiftDevKitTests/StringConversion/StringConversionTests.swift index e89b45c..20a4446 100644 --- a/Tests/SwiftDevKitTests/StringConversion/StringConversionTests.swift +++ b/Tests/SwiftDevKitTests/StringConversion/StringConversionTests.swift @@ -4,7 +4,6 @@ // Copyright (c) 2025 owdax and The SwiftDevKit Contributors // MIT License - https://opensource.org/licenses/MIT -import _TestingInternals import Foundation import SwiftDevKit import Testing diff --git a/Tests/SwiftDevKitTests/SwiftDevKitTests.swift b/Tests/SwiftDevKitTests/SwiftDevKitTests.swift index 4728aab..9e14e3d 100644 --- a/Tests/SwiftDevKitTests/SwiftDevKitTests.swift +++ b/Tests/SwiftDevKitTests/SwiftDevKitTests.swift @@ -4,7 +4,6 @@ // Copyright (c) 2025 owdax and The SwiftDevKit Contributors // MIT License - https://opensource.org/licenses/MIT -import _TestingInternals import Foundation import SwiftDevKit import Testing From a760e0298699605d60ac0291873ef3dbd906bc24 Mon Sep 17 00:00:00 2001 From: owdax Date: Thu, 16 Jan 2025 23:06:23 +0330 Subject: [PATCH 11/23] chore: update SwiftLint configuration to resolve case alignment warnings --- .swiftlint.yml | 2 ++ Package.swift | 6 +++--- .../StringConversion/DateConversionTests.swift | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index 8fea3f5..03bfcd1 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -7,6 +7,7 @@ excluded: disabled_rules: - trailing_comma - comment_spacing + - switch_case_alignment opt_in_rules: - array_init @@ -46,6 +47,7 @@ opt_in_rules: - vertical_whitespace_closing_braces - vertical_whitespace_opening_braces - yoda_condition + - unused_import analyzer_rules: - explicit_self diff --git a/Package.swift b/Package.swift index 1ade950..0d62805 100644 --- a/Package.swift +++ b/Package.swift @@ -40,7 +40,7 @@ let package = Package( .process("Resources"), ], swiftSettings: [ - .define("SWIFT_STRICT_CONCURRENCY", .when(configuration: .debug)) + .define("SWIFT_STRICT_CONCURRENCY", .when(configuration: .debug)), ]), .testTarget( name: "SwiftDevKitTests", @@ -48,6 +48,6 @@ let package = Package( "SwiftDevKit", ], swiftSettings: [ - .define("SWIFT_STRICT_CONCURRENCY", .when(configuration: .debug)) - ]) + .define("SWIFT_STRICT_CONCURRENCY", .when(configuration: .debug)), + ]), ]) diff --git a/Tests/SwiftDevKitTests/StringConversion/DateConversionTests.swift b/Tests/SwiftDevKitTests/StringConversion/DateConversionTests.swift index 7d66509..dceeabe 100644 --- a/Tests/SwiftDevKitTests/StringConversion/DateConversionTests.swift +++ b/Tests/SwiftDevKitTests/StringConversion/DateConversionTests.swift @@ -12,7 +12,7 @@ import Testing struct DateConversionTests { @Test("Test date string conversion with various formats") func testDateStringConversion() async throws { - let date = Date(timeIntervalSince1970: 1705363800) // 2024-01-16 00:10:00 UTC + let date = Date(timeIntervalSince1970: 1_705_363_800) // 2024-01-16 00:10:00 UTC // Test ISO8601 let iso8601 = try await date.toISO8601() From bbe0083435907b4fa88182816041318d1431f97a Mon Sep 17 00:00:00 2001 From: owdax Date: Thu, 16 Jan 2025 23:07:34 +0330 Subject: [PATCH 12/23] chore: fix duplicate unused_import rule in SwiftLint config --- .swiftlint.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index 03bfcd1..2dbff28 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -47,7 +47,6 @@ opt_in_rules: - vertical_whitespace_closing_braces - vertical_whitespace_opening_braces - yoda_condition - - unused_import analyzer_rules: - explicit_self From 59874d045168d8a449c4951829e43bf3dbe1354b Mon Sep 17 00:00:00 2001 From: owdax Date: Thu, 16 Jan 2025 23:21:23 +0330 Subject: [PATCH 13/23] ci: update GitHub Actions workflow for Swift 6 --- .github/workflows/swift.yml | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index dee3e84..05ebe2b 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -8,9 +8,6 @@ on: branches: [ "master" ] workflow_dispatch: -env: - DEVELOPER_DIR: /Applications/Xcode_15.2.app/Contents/Developer - jobs: analyze: name: Analyze @@ -23,13 +20,18 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Setup Swift + uses: swift-actions/setup-swift@v1 + with: + swift-version: "6.0" + - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: swift - name: Build - run: swift build -v + run: swift build -v -Xswiftc -swift-version -Xswiftc 6 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 @@ -44,11 +46,16 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Setup Swift + uses: swift-actions/setup-swift@v1 + with: + swift-version: "6.0" + - name: Build - run: swift build -v + run: swift build -v -Xswiftc -swift-version -Xswiftc 6 - name: Run tests - run: swift test -v --enable-code-coverage + run: swift test -v -Xswiftc -swift-version -Xswiftc 6 --enable-code-coverage - name: Convert coverage report run: xcrun llvm-cov export -format="lcov" .build/debug/SwiftDevKitPackageTests.xctest/Contents/MacOS/SwiftDevKitPackageTests -instr-profile .build/debug/codecov/default.profdata > coverage.lcov @@ -80,9 +87,18 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Setup Swift + uses: swift-actions/setup-swift@v1 + with: + swift-version: "6.0" + - name: Generate Documentation run: | - swift package --allow-writing-to-directory docs generate-documentation --target SwiftDevKit --output-path docs + swift package --allow-writing-to-directory docs \ + generate-documentation --target SwiftDevKit \ + --output-path docs \ + --transform-for-static-hosting \ + --hosting-base-path SwiftDevKit - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v3 From 062c449d3edec79b3a7606d45bd15b01a8a063b0 Mon Sep 17 00:00:00 2001 From: owdax Date: Thu, 16 Jan 2025 23:31:25 +0330 Subject: [PATCH 14/23] ci: update Github Actions for swift 6 --- .github/workflows/swift.yml | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 05ebe2b..0918f08 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -21,15 +21,8 @@ jobs: - uses: actions/checkout@v4 - name: Setup Swift - uses: swift-actions/setup-swift@v1 - with: - swift-version: "6.0" - - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: swift - + uses: SwiftyLab/setup-swift@latest + - name: Build run: swift build -v -Xswiftc -swift-version -Xswiftc 6 @@ -47,9 +40,8 @@ jobs: - uses: actions/checkout@v4 - name: Setup Swift - uses: swift-actions/setup-swift@v1 - with: - swift-version: "6.0" + uses: SwiftyLab/setup-swift@latest + - name: Build run: swift build -v -Xswiftc -swift-version -Xswiftc 6 @@ -88,9 +80,7 @@ jobs: - uses: actions/checkout@v4 - name: Setup Swift - uses: swift-actions/setup-swift@v1 - with: - swift-version: "6.0" + uses: SwiftyLab/setup-swift@latest - name: Generate Documentation run: | From f8fed2d6de3c5c98f3c8453bdf9f17c7b22dc1b9 Mon Sep 17 00:00:00 2001 From: owdax Date: Thu, 16 Jan 2025 23:49:50 +0330 Subject: [PATCH 15/23] fix: Add Resources directory and update package configuration --- Package.swift | 2 +- Sources/SwiftDevKit/Resources/.gitkeep | 0 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 Sources/SwiftDevKit/Resources/.gitkeep diff --git a/Package.swift b/Package.swift index 0d62805..e5296aa 100644 --- a/Package.swift +++ b/Package.swift @@ -37,7 +37,7 @@ let package = Package( "Documentation.docc/SwiftDevKit.md", ], resources: [ - .process("Resources"), + .copy("Resources") ], swiftSettings: [ .define("SWIFT_STRICT_CONCURRENCY", .when(configuration: .debug)), diff --git a/Sources/SwiftDevKit/Resources/.gitkeep b/Sources/SwiftDevKit/Resources/.gitkeep new file mode 100644 index 0000000..e69de29 From a8006a0b32143e28c2fd999e0007a02bd47fd60c Mon Sep 17 00:00:00 2001 From: owdax Date: Thu, 16 Jan 2025 23:58:02 +0330 Subject: [PATCH 16/23] fix: Add CodeQL initialization step to GitHub Actions workflow --- .github/workflows/swift.yml | 7 ++++++- Package.swift | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 0918f08..05383e7 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -22,7 +22,12 @@ jobs: - name: Setup Swift uses: SwiftyLab/setup-swift@latest - + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: swift + - name: Build run: swift build -v -Xswiftc -swift-version -Xswiftc 6 diff --git a/Package.swift b/Package.swift index e5296aa..be7fd83 100644 --- a/Package.swift +++ b/Package.swift @@ -37,7 +37,7 @@ let package = Package( "Documentation.docc/SwiftDevKit.md", ], resources: [ - .copy("Resources") + .copy("Resources"), ], swiftSettings: [ .define("SWIFT_STRICT_CONCURRENCY", .when(configuration: .debug)), From cd2b61da1059075d78b21719ff05591829916bec Mon Sep 17 00:00:00 2001 From: owdax Date: Fri, 17 Jan 2025 00:08:41 +0330 Subject: [PATCH 17/23] chore: Update CodeQL actions to v3 --- .github/workflows/swift.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 05383e7..52d62a7 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -24,7 +24,7 @@ jobs: uses: SwiftyLab/setup-swift@latest - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: swift @@ -32,7 +32,7 @@ jobs: run: swift build -v -Xswiftc -swift-version -Xswiftc 6 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:swift" From 1174596d2acd0c6d03b9efbcc0427c5b98f45183 Mon Sep 17 00:00:00 2001 From: owdax Date: Fri, 17 Jan 2025 00:12:30 +0330 Subject: [PATCH 18/23] fix: Add Codecov repository token to prevent rate limiting --- .github/workflows/swift.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 52d62a7..53b5154 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -24,7 +24,7 @@ jobs: uses: SwiftyLab/setup-swift@latest - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v2 with: languages: swift @@ -32,7 +32,7 @@ jobs: run: swift build -v -Xswiftc -swift-version -Xswiftc 6 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v2 with: category: "/language:swift" @@ -61,7 +61,9 @@ jobs: uses: codecov/codecov-action@v3 with: file: coverage.lcov + token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true + verbose: true lint: name: Lint From 0393da2afe72647ac1e7fefb97059b4a9ef77cad Mon Sep 17 00:00:00 2001 From: owdax Date: Fri, 17 Jan 2025 00:39:08 +0330 Subject: [PATCH 19/23] chore: update codecov to v4 --- .github/workflows/swift.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 53b5154..4f72dd5 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -58,10 +58,9 @@ jobs: run: xcrun llvm-cov export -format="lcov" .build/debug/SwiftDevKitPackageTests.xctest/Contents/MacOS/SwiftDevKitPackageTests -instr-profile .build/debug/codecov/default.profdata > coverage.lcov - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: file: coverage.lcov - token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true verbose: true From 857b7be0b6e3cf4fed1a999178226ad5fbf9bb6a Mon Sep 17 00:00:00 2001 From: owdax Date: Fri, 17 Jan 2025 00:40:14 +0330 Subject: [PATCH 20/23] chore: update codeql to v3 --- .github/workflows/swift.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 4f72dd5..1b5a3f9 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -24,7 +24,7 @@ jobs: uses: SwiftyLab/setup-swift@latest - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: swift @@ -32,7 +32,7 @@ jobs: run: swift build -v -Xswiftc -swift-version -Xswiftc 6 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:swift" From 5146b80f8bc1e91f72db309114ebe18398203287 Mon Sep 17 00:00:00 2001 From: owdax Date: Fri, 17 Jan 2025 01:30:08 +0330 Subject: [PATCH 21/23] ci: add codecov secret --- .github/workflows/swift.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 1b5a3f9..7ac1725 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -61,6 +61,7 @@ jobs: uses: codecov/codecov-action@v4 with: file: coverage.lcov + token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true verbose: true From 743b8a083286764a51767b674dcf6ba00d548fea Mon Sep 17 00:00:00 2001 From: owdax Date: Fri, 17 Jan 2025 01:34:05 +0330 Subject: [PATCH 22/23] chore: update codecov configurations --- .github/workflows/swift.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 7ac1725..88c5b56 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -54,13 +54,16 @@ jobs: - name: Run tests run: swift test -v -Xswiftc -swift-version -Xswiftc 6 --enable-code-coverage - - name: Convert coverage report - run: xcrun llvm-cov export -format="lcov" .build/debug/SwiftDevKitPackageTests.xctest/Contents/MacOS/SwiftDevKitPackageTests -instr-profile .build/debug/codecov/default.profdata > coverage.lcov + - name: Generate coverage report + run: | + xcrun llvm-cov export -format="xml" \ + .build/debug/SwiftDevKitPackageTests.xctest/Contents/MacOS/SwiftDevKitPackageTests \ + -instr-profile .build/debug/codecov/default.profdata > coverage.xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: - file: coverage.lcov + file: coverage.xml token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true verbose: true From 7e52f0d3ab11185f1dcd1968de3d09f51d9590cf Mon Sep 17 00:00:00 2001 From: owdax Date: Fri, 17 Jan 2025 02:09:07 +0330 Subject: [PATCH 23/23] fix: Update Codecov configuration with correct input parameters --- .github/workflows/swift.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 88c5b56..988ba00 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -56,14 +56,14 @@ jobs: - name: Generate coverage report run: | - xcrun llvm-cov export -format="xml" \ + xcrun llvm-cov export -format=lcov \ .build/debug/SwiftDevKitPackageTests.xctest/Contents/MacOS/SwiftDevKitPackageTests \ - -instr-profile .build/debug/codecov/default.profdata > coverage.xml + -instr-profile .build/debug/codecov/default.profdata > coverage.lcov - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: - file: coverage.xml + files: ./coverage.lcov token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true verbose: true