Skip to content

Commit 2db28e8

Browse files
authored
Merge pull request #1 from owdax/feature/string-and-date-conversion
feat: Add string and date conversion with thread-safe operations
2 parents 07f5456 + 7e52f0d commit 2db28e8

File tree

15 files changed

+706
-68
lines changed

15 files changed

+706
-68
lines changed

.github/workflows/swift.yml

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,6 @@ on:
88
branches: [ "master" ]
99
workflow_dispatch:
1010

11-
env:
12-
DEVELOPER_DIR: /Applications/Xcode_15.2.app/Contents/Developer
13-
1411
jobs:
1512
analyze:
1613
name: Analyze
@@ -23,16 +20,19 @@ jobs:
2320
steps:
2421
- uses: actions/checkout@v4
2522

23+
- name: Setup Swift
24+
uses: SwiftyLab/setup-swift@latest
25+
2626
- name: Initialize CodeQL
27-
uses: github/codeql-action/init@v2
27+
uses: github/codeql-action/init@v3
2828
with:
2929
languages: swift
30-
30+
3131
- name: Build
32-
run: swift build -v
32+
run: swift build -v -Xswiftc -swift-version -Xswiftc 6
3333

3434
- name: Perform CodeQL Analysis
35-
uses: github/codeql-action/analyze@v2
35+
uses: github/codeql-action/analyze@v3
3636
with:
3737
category: "/language:swift"
3838

@@ -44,20 +44,29 @@ jobs:
4444
steps:
4545
- uses: actions/checkout@v4
4646

47+
- name: Setup Swift
48+
uses: SwiftyLab/setup-swift@latest
49+
50+
4751
- name: Build
48-
run: swift build -v
52+
run: swift build -v -Xswiftc -swift-version -Xswiftc 6
4953

5054
- name: Run tests
51-
run: swift test -v --enable-code-coverage
55+
run: swift test -v -Xswiftc -swift-version -Xswiftc 6 --enable-code-coverage
5256

53-
- name: Convert coverage report
54-
run: xcrun llvm-cov export -format="lcov" .build/debug/SwiftDevKitPackageTests.xctest/Contents/MacOS/SwiftDevKitPackageTests -instr-profile .build/debug/codecov/default.profdata > coverage.lcov
57+
- name: Generate coverage report
58+
run: |
59+
xcrun llvm-cov export -format=lcov \
60+
.build/debug/SwiftDevKitPackageTests.xctest/Contents/MacOS/SwiftDevKitPackageTests \
61+
-instr-profile .build/debug/codecov/default.profdata > coverage.lcov
5562
5663
- name: Upload coverage to Codecov
57-
uses: codecov/codecov-action@v3
64+
uses: codecov/codecov-action@v5
5865
with:
59-
file: coverage.lcov
66+
files: ./coverage.lcov
67+
token: ${{ secrets.CODECOV_TOKEN }}
6068
fail_ci_if_error: true
69+
verbose: true
6170

6271
lint:
6372
name: Lint
@@ -80,9 +89,16 @@ jobs:
8089
steps:
8190
- uses: actions/checkout@v4
8291

92+
- name: Setup Swift
93+
uses: SwiftyLab/setup-swift@latest
94+
8395
- name: Generate Documentation
8496
run: |
85-
swift package --allow-writing-to-directory docs generate-documentation --target SwiftDevKit --output-path docs
97+
swift package --allow-writing-to-directory docs \
98+
generate-documentation --target SwiftDevKit \
99+
--output-path docs \
100+
--transform-for-static-hosting \
101+
--hosting-base-path SwiftDevKit
86102
87103
- name: Deploy to GitHub Pages
88104
uses: peaceiris/actions-gh-pages@v3

.swiftlint.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ excluded:
77
disabled_rules:
88
- trailing_comma
99
- comment_spacing
10+
- switch_case_alignment
1011

1112
opt_in_rules:
1213
- array_init

Package.resolved

Lines changed: 0 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
// swift-tools-version: 5.9
1+
// swift-tools-version: 6.0
22
// The swift-tools-version declares the minimum version of Swift required to build this package.
33

44
import PackageDescription
55

66
let package = Package(
77
name: "SwiftDevKit",
8+
defaultLocalization: nil,
89
platforms: [
9-
.iOS(.v13),
10-
.macOS(.v10_15),
11-
.tvOS(.v13),
12-
.watchOS(.v6),
10+
.macOS(.v13),
11+
.iOS(.v16),
12+
.tvOS(.v16),
13+
.watchOS(.v9),
1314
],
1415
products: [
1516
// Products define the executables and libraries a package produces, making them visible to other packages.
@@ -19,27 +20,34 @@ let package = Package(
1920
],
2021
dependencies: [
2122
// Dependencies will be added as needed
22-
.package(url: "https://github.com/apple/swift-testing.git", from: "0.5.0"),
2323
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
2424
],
2525
targets: [
2626
// Targets are the basic building blocks of a package, defining a module or a test suite.
2727
// Targets can depend on other targets in this package and products from dependencies.
2828
.target(
2929
name: "SwiftDevKit",
30-
dependencies: [],
31-
swiftSettings: [
32-
.define("DEBUG", .when(configuration: .debug)),
33-
.enableUpcomingFeature("BareSlashRegexLiterals"),
34-
.enableExperimentalFeature("StrictConcurrency"),
30+
path: "Sources/SwiftDevKit",
31+
exclude: [
32+
"Documentation.docc/Installation.md",
33+
"Documentation.docc/Architecture.md",
34+
"Documentation.docc/Contributing.md",
35+
"Documentation.docc/Conversion.md",
36+
"Documentation.docc/GettingStarted.md",
37+
"Documentation.docc/SwiftDevKit.md",
38+
],
39+
resources: [
40+
.copy("Resources"),
3541
],
36-
plugins: [
37-
.plugin(name: "Swift-DocC", package: "swift-docc-plugin"),
42+
swiftSettings: [
43+
.define("SWIFT_STRICT_CONCURRENCY", .when(configuration: .debug)),
3844
]),
3945
.testTarget(
4046
name: "SwiftDevKitTests",
4147
dependencies: [
4248
"SwiftDevKit",
43-
.product(name: "Testing", package: "swift-testing"),
49+
],
50+
swiftSettings: [
51+
.define("SWIFT_STRICT_CONCURRENCY", .when(configuration: .debug)),
4452
]),
4553
])
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Date+Convertible.swift
2+
// SwiftDevKit
3+
//
4+
// Copyright (c) 2025 owdax and The SwiftDevKit Contributors
5+
// MIT License - https://opensource.org/licenses/MIT
6+
7+
import Foundation
8+
9+
/// Actor that manages thread-safe access to date formatters.
10+
///
11+
/// This actor provides a cache for `DateFormatter` instances, ensuring thread safety
12+
/// while maintaining performance through reuse. All formatters are configured with
13+
/// the POSIX locale and UTC timezone for consistent behavior across platforms.
14+
private actor DateFormatterCache {
15+
/// Cache of date formatters keyed by format string
16+
private var formatters: [String: DateFormatter] = [:]
17+
18+
/// Gets or creates a date formatter for the specified format.
19+
///
20+
/// - Parameter format: The date format string
21+
/// - Returns: A configured DateFormatter instance
22+
func formatter(for format: String) -> DateFormatter {
23+
if let formatter = formatters[format] {
24+
return formatter
25+
}
26+
27+
let formatter = DateFormatter()
28+
formatter.dateFormat = format
29+
formatter.locale = Locale(identifier: "en_US_POSIX")
30+
formatter.timeZone = TimeZone(secondsFromGMT: 0)
31+
formatters[format] = formatter
32+
return formatter
33+
}
34+
}
35+
36+
/// Extends Date to support string conversion with thread-safe operations.
37+
///
38+
/// This extension provides methods for converting dates to and from strings using various formats.
39+
/// All operations are thread-safe and can be called concurrently from multiple tasks.
40+
extension Date: DateConvertible {
41+
/// Thread-safe date formatter cache
42+
private static let formatterCache = DateFormatterCache()
43+
44+
public func toString(format: String?) async throws -> String {
45+
let dateFormat = format ?? DateFormat.iso8601
46+
let formatter = await Self.formatterCache.formatter(for: dateFormat)
47+
return formatter.string(from: self)
48+
}
49+
50+
public static func fromString(_ string: String, format: String?) async throws -> Date {
51+
let dateFormat = format ?? DateFormat.iso8601
52+
let formatter = await formatterCache.formatter(for: dateFormat)
53+
54+
guard let date = formatter.date(from: string) else {
55+
throw DateConversionError.invalidFormat(string)
56+
}
57+
58+
return date
59+
}
60+
}
61+
62+
// MARK: - Convenience Methods
63+
64+
public extension Date {
65+
/// Creates a date from an ISO8601 string.
66+
///
67+
/// This is a convenience method that uses the ISO8601 format for parsing.
68+
/// The operation is thread-safe and can be called concurrently.
69+
///
70+
/// - Parameter iso8601String: The ISO8601 formatted string (e.g., "2025-01-16T15:30:00Z")
71+
/// - Returns: A new Date instance
72+
/// - Throws: DateConversionError if the string is not valid ISO8601
73+
static func fromISO8601(_ iso8601String: String) async throws -> Date {
74+
try await fromString(iso8601String, format: DateFormat.iso8601)
75+
}
76+
77+
/// Converts the date to an ISO8601 string.
78+
///
79+
/// This is a convenience method that uses the ISO8601 format for formatting.
80+
/// The operation is thread-safe and can be called concurrently.
81+
///
82+
/// - Returns: An ISO8601 formatted string (e.g., "2025-01-16T15:30:00Z")
83+
/// - Throws: DateConversionError if the conversion fails
84+
func toISO8601() async throws -> String {
85+
try await toString(format: DateFormat.iso8601)
86+
}
87+
88+
/// Creates a date from an HTTP date string.
89+
///
90+
/// This is a convenience method that uses the HTTP date format for parsing.
91+
/// The operation is thread-safe and can be called concurrently.
92+
///
93+
/// - Parameter httpDateString: The HTTP date formatted string (e.g., "Wed, 16 Jan 2025 15:30:00 GMT")
94+
/// - Returns: A new Date instance
95+
/// - Throws: DateConversionError if the string is not a valid HTTP date
96+
static func fromHTTPDate(_ httpDateString: String) async throws -> Date {
97+
try await fromString(httpDateString, format: DateFormat.http)
98+
}
99+
100+
/// Converts the date to an HTTP date string.
101+
///
102+
/// This is a convenience method that uses the HTTP date format for formatting.
103+
/// The operation is thread-safe and can be called concurrently.
104+
///
105+
/// - Returns: An HTTP date formatted string (e.g., "Wed, 16 Jan 2025 15:30:00 GMT")
106+
/// - Throws: DateConversionError if the conversion fails
107+
func toHTTPDate() async throws -> String {
108+
try await toString(format: DateFormat.http)
109+
}
110+
}

0 commit comments

Comments
 (0)