├── .github └── workflows │ └── test.yml ├── .gitignore ├── .swiftpm └── xcode │ └── xcshareddata │ └── xcschemes │ ├── TypedDate.xcscheme │ ├── TypedDateCore.xcscheme │ ├── TypedDateCoreTests.xcscheme │ ├── TypedDateMacrosTests.xcscheme │ ├── TypedDateTests.xcscheme │ └── swift-composable-date.xcscheme ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── TypedDate │ ├── Date+scope.swift │ ├── Macro.swift │ ├── TypedDate+Codable.swift │ ├── TypedDate+Comparable.swift │ ├── TypedDate+Hashable.swift │ ├── TypedDate+components.swift │ ├── TypedDate+erase.swift │ ├── TypedDate+fill.swift │ ├── TypedDate+init.swift │ ├── TypedDate+modify.swift │ ├── TypedDate.swift │ ├── TypedDateOf.swift │ ├── TypedDateProtocol.swift │ └── exported.swift ├── TypedDateCore │ ├── TypedDateUnits+BinaryInteger.swift │ └── TypedDateUnits.swift └── TypedDateMacros │ ├── EraseContextMacro.swift │ ├── FillInContextMacro.swift │ ├── MacroPlugins.swift │ └── ModifyContextMacro.swift └── Tests ├── TypedDateMacrosTests ├── EraseContextMacroTests.swift ├── FillInContextMacroTests.swift └── ModifyContextMacroTests.swift └── TypedDateTests ├── Scaffolding.swift ├── TypedDateAddTests.swift ├── TypedDateCodableTests.swift ├── TypedDateComparableTests.swift ├── TypedDateComponentsTests.swift ├── TypedDateEquatableTests.swift ├── TypedDateEraseTests.swift ├── TypedDateFillTests.swift ├── TypedDateInitTests.swift ├── TypedDateModifyTests.swift ├── TypedDateScopeTests.swift └── TypedDateTestSupport.swift /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | branches: [ "main" ] 6 | paths-ignore: 7 | - README.md 8 | push: 9 | branches: [ "main" ] 10 | paths-ignore: 11 | - README.md 12 | workflow_dispatch: 13 | 14 | concurrency: 15 | group: format-${{ github.ref }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | build: 20 | name: Test 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | os: [macos-13] 25 | runs-on: ${{ matrix.os }} 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | 30 | - name: Select Xcode 15 31 | run: sudo xcode-select -s /Applications/Xcode_15.0.app 32 | 33 | - name: Test 34 | run: swift test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/TypedDate.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 43 | 49 | 50 | 56 | 57 | 58 | 59 | 61 | 62 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/TypedDateCore.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 43 | 49 | 50 | 56 | 57 | 58 | 59 | 61 | 62 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/TypedDateCoreTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 16 | 18 | 24 | 25 | 26 | 27 | 28 | 38 | 39 | 45 | 46 | 48 | 49 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/TypedDateMacrosTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 16 | 18 | 24 | 25 | 26 | 27 | 28 | 38 | 39 | 45 | 46 | 48 | 49 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/TypedDateTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 16 | 18 | 24 | 25 | 26 | 27 | 28 | 38 | 39 | 45 | 46 | 48 | 49 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/swift-composable-date.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 48 | 54 | 55 | 56 | 58 | 64 | 65 | 66 | 68 | 74 | 75 | 76 | 78 | 84 | 85 | 86 | 87 | 88 | 98 | 99 | 105 | 106 | 112 | 113 | 114 | 115 | 117 | 118 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ryu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-macro-testing", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/pointfreeco/swift-macro-testing", 7 | "state" : { 8 | "revision" : "35acd9468d40ae87e75991a18af6271e8124c261", 9 | "version" : "0.2.1" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-snapshot-testing", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/pointfreeco/swift-snapshot-testing", 16 | "state" : { 17 | "revision" : "bb0ea08db8e73324fe6c3727f755ca41a23ff2f4", 18 | "version" : "1.14.2" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-syntax", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/apple/swift-syntax.git", 25 | "state" : { 26 | "revision" : "6ad4ea24b01559dde0773e3d091f1b9e36175036", 27 | "version" : "509.0.2" 28 | } 29 | }, 30 | { 31 | "identity" : "swift-testing", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/apple/swift-testing.git", 34 | "state" : { 35 | "revision" : "395288f1d154d0e8b92823313957546ddb88106c", 36 | "version" : "0.1.0" 37 | } 38 | } 39 | ], 40 | "version" : 2 41 | } 42 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | import CompilerPluginSupport 6 | 7 | let package = Package( 8 | name: "swift-typed-date", 9 | platforms: [ 10 | .macOS(.v10_15), 11 | .iOS(.v13), 12 | .tvOS(.v13), 13 | .watchOS(.v6), 14 | .macCatalyst(.v13) 15 | ], 16 | products: [ 17 | // Products define the executables and libraries a package produces, making them visible to other packages. 18 | .library( 19 | name: "TypedDate", 20 | targets: ["TypedDate"] 21 | ) 22 | ], 23 | dependencies: [ 24 | // Depend on the Swift 5.9 release of SwiftSyntax 25 | .package(url: "https://github.com/apple/swift-syntax.git", "509.0.0"..<"510.0.0"), 26 | .package(url: "https://github.com/apple/swift-testing.git", from: "0.1.0"), 27 | .package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.2.1") 28 | ], 29 | targets: [ 30 | // Targets are the basic building blocks of a package, defining a module or a test suite. 31 | // Targets can depend on other targets in this package and products from dependencies. 32 | // Macro implementation that performs the source transformation of a macro. 33 | .macro( 34 | name: "TypedDateMacros", 35 | dependencies: [ 36 | "TypedDateCore", 37 | .product(name: "SwiftSyntax", package: "swift-syntax"), 38 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 39 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax") 40 | ] 41 | ), 42 | 43 | // Library that exposes a macro as part of its API, which is used in client programs. 44 | .target( 45 | name: "TypedDate", 46 | dependencies: [ 47 | "TypedDateCore", 48 | "TypedDateMacros" 49 | ] 50 | ), 51 | .target( 52 | name: "TypedDateCore", 53 | dependencies: [ 54 | ] 55 | ), 56 | 57 | .testTarget( 58 | name: "TypedDateMacrosTests", 59 | dependencies: [ 60 | "TypedDateMacros", 61 | "TypedDateCore", 62 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 63 | .product(name: "MacroTesting", package: "swift-macro-testing") 64 | ] 65 | ), 66 | .testTarget( 67 | name: "TypedDateTests", 68 | dependencies: [ 69 | "TypedDate", 70 | .product(name: "Testing", package: "swift-testing"), 71 | ] 72 | ) 73 | ] 74 | ) 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TypedDate 2 | 3 | ![Language:Swift](https://img.shields.io/static/v1?label=Language&message=Swift&color=orange&style=flat-square) 4 | ![License:MIT](https://img.shields.io/static/v1?label=License&message=MIT&color=blue&style=flat-square) 5 | [![Latest Release](https://img.shields.io/github/v/release/Ryu0118/swift-typed-date?style=flat-square)](https://github.com/Ryu0118/swift-typed-date/releases/latest) 6 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FRyu0118%2Fswift-typed-date%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/Ryu0118/swift-typed-date) 7 | [![Twitter](https://img.shields.io/twitter/follow/ryu_hu03?style=social)](https://twitter.com/ryu_hu03) 8 | 9 | TypedDate is a wrapper for Swift's Date that enhances date handling at the type level. It enables the construction of custom date structures, ranging from single components like year to combinations of year, month, day, time, etc., tailored to specific development requirements. 10 | 11 | Key features of TypedDate include: 12 | 13 | * **Explicit Clarity in Date Components** 14 |
Allows developers to specify precisely which date components (year, month, day, etc.) they are working with, leading to a better understanding and less likelihood of inconsistencies. 15 | * **Flexible Customization** 16 |
Enables the creation of date objects with different levels of detail, from a simple year-only structure to more comprehensive ones including month, day, second, and nanosecond. 17 | * **Modifiable Date Components** 18 |
Provides methods for modifying individual components such as year, month or day. 19 | * **Seamless Conversion** 20 |
Enables effortless conversion between 'TypedDate' and Swift's standard Date object, adapting to different scenarios by filling in any missing components as needed. 21 | 22 | ## Usage 23 | ### Initialization 24 | To initialize a TypedDate, you can use the following syntax: 25 | 26 | ```Swift 27 | import TypedDate 28 | 29 | TypedDate(Year(2023), Month(11), Day(12)) 30 | TypedDate(Year(2023), Month(11), Day(12), calendar: Calendar(identifier: .gregorian)) 31 | TypedDate(Year(2023), Month(11), Day(12), Hour(11), Minute(12), Second(1), Nanosecond(10000000)) 32 | TypedDate(Year(2023), Month(11), Day(12), Hour(11), Minute(12), FractionalSecond(5.12)) 33 | ``` 34 | This will create a TypedDate instance representing the date 2023/11/12. 35 |
Date has the following components available: Year, Month, Day, Hour, Minute, Second, and Nanosecond. 36 | 37 | ### Date to TypedDate conversion 38 | To create a TypedDate from a Date, use `Date.scope(to:calendar:)`. 39 | ```Swift 40 | let typedDate1: TypedDate<(Year, Month)> = Date().scope(to: \.month) 41 | let typedDate2: TypedDate<(Year, Month, Day, Hour)> = Date().scope(to: \.hour) 42 | let typedDate3: TypedDate<(Year, Month, Day, Hour, Minute)> = Date().scope(to: \.minute, calendar: Calendar(identifier: .gregorian)) 43 | let typedDate4: TypedDateOfYear = Date().scope(to: \.year) 44 | let typedDate5: TypedDateOfDay = Date().scope(to: \.day) 45 | ``` 46 | 47 | ### TypedDate to Date conversion 48 | To convert from TypedDate to Date, use date property. 49 | ```Swift 50 | typedDate.date // Date 51 | ``` 52 | 53 | ### Fill 54 | The fill method allows you to fill in a specific part of a date. For example: 55 | ```Swift 56 | let typedDate = TypedDate(Year(2023), Month(11), Day(12)) 57 | // typedDate: TypedDate<(Year, Month, Day)> 58 | 59 | typedDate.fill( 60 | to: \.second, 61 | arguments: (Hour(11), Minute(12), Second(10)) 62 | ) 63 | // TypedDate<(Year, Month, Day, Hour, Minute, Second)> 64 | 65 | typedDate.fill( 66 | to: \.hour, 67 | arguments: (Hour(11)), 68 | calendar: Calendar(identifier: .gregorian) 69 | ) 70 | // TypedDate<(Year, Month, Day, Hour)> 71 | ``` 72 | In this example, filledDate will represent the date 2023/11/12 11:12 73 | 74 | ### Erase 75 | The erase method allows you to erase a specific part of a date. For example: 76 | 77 | ```Swift 78 | let date = TypedDate(Year(2023), Month(11), Day(12), Hour(11), Minute(12)) 79 | let erasedDate1: TypedDate<(Year, Month, Day)> = date.erase(to: \.day) 80 | let erasedDate2: TypedDate<(Year, Month)> = date.erase(to: \.month) 81 | let erasedDate2: TypedDate<(Year)> = date.erase(to: \.year, calendar: Calendar(identifier: .gregorian) 82 | ``` 83 | In this example, erasedDate will be erased up to date specified in keyPath. 84 | 85 | ### Modify 86 | The modify method allows you to modify a specific part of a date. For example: 87 | ```Swift 88 | let typedDate = TypedDate(Year(2023), Month(11), Day(12), Hour(11), Minute(12)) 89 | // typedDate: 2023/11/12 11:12 90 | 91 | let modifiedDate = typedDate.modifying(\.year) { $0 += 1 } 92 | .modifying(\.month) { $0 -= 2 } 93 | .modifying(\.day) { $0 += 3 } 94 | .modifying(\.hour) { $0 += 4 } 95 | .modifying(\.minute) { $0 += 5 } 96 | .add(\.year, 1) 97 | .add(\.month, -2) 98 | .add(\.day, 3) 99 | .add(\.hour, -2) 100 | .add(\.minute, -3) 101 | // modifiedDate: 2025/07/18 13:14 102 | ``` 103 | or use `TypedDate.modify(_:calendar:modify:)` method 104 | ```Swift 105 | var typedDate = TypedDate(Year(2023), Month(11)) 106 | 107 | typedDate.modify(\.year) { $0 += 1 } 108 | // typedDate: 2024/11 109 | ``` 110 | 111 | ### Access to each date component 112 | ```Swift 113 | let typedDate = TypedDate(Year(2023), Month(11), Day(12), Hour(11), Minute(12), Second(1), Nanosecond(10000000)) 114 | typedDate.year() // 2023 115 | typedDate.month(calendar: Calendar(identifier: .gregorian)) // 11 116 | typedDate.day() // 12 117 | typedDate.hour(calendar: Calendar(identifier: .gregorian)) // 11 118 | typedDate.minute() // 12 119 | typedDate.second(calendar: Calendar(identifier: .gregorian) // 1 120 | typedDate.nanosecond() // 10000000 121 | ``` 122 | 123 | ### Simplifying TypedDate with TypedDateOf〇〇 Aliases 124 | This library includes the following typealias: 125 | ```Swift 126 | public typealias TypedDateOfYear = TypedDate 127 | public typealias TypedDateOfMonth = TypedDate<(Year, Month)> 128 | public typealias TypedDateOfDay = TypedDate<(Year, Month, Day)> 129 | public typealias TypedDateOfHour = TypedDate<(Year, Month, Day, Hour)> 130 | public typealias TypedDateOfMinute = TypedDate<(Year, Month, Day, Hour, Minute)> 131 | public typealias TypedDateOfSecond = TypedDate<(Year, Month, Day, Hour, Minute, Second)> 132 | public typealias TypedDateOfNanosecond = TypedDate<(Year, Month, Day, Hour, Minute, Second, Nanosecond)> 133 | ``` 134 | 135 | ### Conformance to Standard Protocols 136 | TypedDate conforms to the Comparable, Equatable, Hashable, and Codable protocols, which makes it even more powerful and convenient compared to traditional date handling: 137 | 138 | #### **Comparable and Equatable** 139 | These protocols allow for easy comparison of TypedDate instances. You can check if one date is equal to, less than, or greater than another date using standard comparison operators (==, <, >, etc.). This is much more intuitive and less error-prone than comparing individual date components. 140 | ```Swift 141 | let date1: Date // 2023/11/12 142 | let date2: Date // 2023/11/11 143 | 144 | // To check the date have the same year 145 | date1.scope(to: \.year) == date2.scope(to: \.year) // -> true 146 | 147 | // To check the date have the same year, month, and day 148 | date1.scope(to: \.day) == date2.scope(to: \.day) // -> false 149 | 150 | // Compare days 151 | date1.scope(to: \.day) > date2.scope(to: \.day) // -> true 152 | ``` 153 | #### **Codable** 154 | The Codable conformance means that TypedDate instances can be easily encoded to and decoded from various formats such as JSON. This is particularly useful when working with APIs or saving dates to persistent storage. 155 | 156 | ## Installation 157 | ```Swift 158 | .package(url: "https://github.com/Ryu0118/swift-typed-date", exact: "0.6.0") 159 | ``` 160 | -------------------------------------------------------------------------------- /Sources/TypedDate/Date+scope.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Date { 4 | /// Returns a `TypedDate` representing the specified date components. 5 | /// 6 | /// - Parameters: 7 | /// - keyPath: A key path to the specific date component. 8 | /// - calendar: The calendar used for date calculations. Defaults to the current calendar. 9 | /// - Returns: A `TypedDate` representing the specified date components. 10 | @_disfavoredOverload 11 | public func scope( 12 | to keyPath: KeyPath<_TypedDateContext, TypedDate?>, 13 | calendar: Calendar = .current 14 | ) -> TypedDate { 15 | scope(to: keyPath, calendar: calendar)! 16 | } 17 | 18 | // To test that this method does not become nil, a @_disfavoredOverload attribute is given to the public method. 19 | func scope( 20 | to keyPath: KeyPath<_TypedDateContext, TypedDate?>, 21 | calendar: Calendar = .current 22 | ) -> TypedDate? { 23 | _TypedDateContext(date: self, calendar: calendar)[keyPath: keyPath] 24 | } 25 | } 26 | 27 | public final class _TypedDateContext { 28 | private let date: Date 29 | private let calendar: Calendar 30 | 31 | public lazy var year: TypedDate<(Year)>? = .init(date: date, calendar: calendar) 32 | public lazy var month: TypedDate<(Year, Month)>? = .init(date: date, calendar: calendar) 33 | public lazy var day: TypedDate<(Year, Month, Day)>? = .init(date: date, calendar: calendar) 34 | public lazy var hour: TypedDate<(Year, Month, Day, Hour)>? = .init(date: date, calendar: calendar) 35 | public lazy var minute: TypedDate<(Year, Month, Day, Hour, Minute)>? = .init(date: date, calendar: calendar) 36 | public lazy var second: TypedDate<(Year, Month, Day, Hour, Minute, Second)>? = .init(date: date, calendar: calendar) 37 | public lazy var nanosecond: TypedDate<(Year, Month, Day, Hour, Minute, Second, Nanosecond)>? = .init(date: date, calendar: calendar) 38 | 39 | public init(date: Date, calendar: Calendar) { 40 | self.date = date 41 | self.calendar = calendar 42 | } 43 | } 44 | 45 | extension TypedDate { 46 | init?( 47 | date: Date, 48 | calendar: Calendar 49 | ) where Components == Year { 50 | let dc = calendar.dateComponents([.year], from: date) 51 | guard let date = calendar.date(from: dc), 52 | let year = dc.year 53 | else { 54 | return nil 55 | } 56 | self.date = date 57 | self.components = Year(year) 58 | } 59 | 60 | init?( 61 | date: Date, 62 | calendar: Calendar 63 | ) where Components == (Year, Month) { 64 | let dc = calendar.dateComponents([.year, .month], from: date) 65 | guard let date = calendar.date(from: dc), 66 | let year = dc.year, 67 | let month = dc.month 68 | else { 69 | return nil 70 | } 71 | self.date = date 72 | self.components = (Year(year), Month(month)) 73 | } 74 | 75 | init?( 76 | date: Date, 77 | calendar: Calendar 78 | ) where Components == (Year, Month, Day) { 79 | let dc = calendar.dateComponents([.year, .month, .day], from: date) 80 | guard let date = calendar.date(from: dc), 81 | let year = dc.year, 82 | let month = dc.month, 83 | let day = dc.day 84 | else { 85 | return nil 86 | } 87 | self.date = date 88 | self.components = (Year(year), Month(month), Day(day)) 89 | } 90 | 91 | init?( 92 | date: Date, 93 | calendar: Calendar 94 | ) where Components == (Year, Month, Day, Hour) { 95 | let dc = calendar.dateComponents([.year, .month, .day, .hour], from: date) 96 | guard let date = calendar.date(from: dc), 97 | let year = dc.year, 98 | let month = dc.month, 99 | let day = dc.day, 100 | let hour = dc.hour 101 | else { 102 | return nil 103 | } 104 | self.date = date 105 | self.components = (Year(year), Month(month), Day(day), Hour(hour)) 106 | } 107 | 108 | init?( 109 | date: Date, 110 | calendar: Calendar 111 | ) where Components == (Year, Month, Day, Hour, Minute) { 112 | let dc = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: date) 113 | guard let date = calendar.date(from: dc), 114 | let year = dc.year, 115 | let month = dc.month, 116 | let day = dc.day, 117 | let hour = dc.hour, 118 | let minute = dc.minute 119 | else { 120 | return nil 121 | } 122 | self.date = date 123 | self.components = (Year(year), Month(month), Day(day), Hour(hour), Minute(minute)) 124 | } 125 | 126 | init?( 127 | date: Date, 128 | calendar: Calendar 129 | ) where Components == (Year, Month, Day, Hour, Minute, Second) { 130 | let dc = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: date) 131 | guard let date = calendar.date(from: dc), 132 | let year = dc.year, 133 | let month = dc.month, 134 | let day = dc.day, 135 | let hour = dc.hour, 136 | let minute = dc.minute, 137 | let second = dc.second 138 | else { 139 | return nil 140 | } 141 | self.date = date 142 | self.components = (Year(year), Month(month), Day(day), Hour(hour), Minute(minute), Second(second)) 143 | } 144 | 145 | init?( 146 | date: Date, 147 | calendar: Calendar 148 | ) where Components == (Year, Month, Day, Hour, Minute, Second, Nanosecond) { 149 | let dc = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second, .nanosecond], from: date) 150 | guard let date = calendar.date(from: dc), 151 | let year = dc.year, 152 | let month = dc.month, 153 | let day = dc.day, 154 | let hour = dc.hour, 155 | let minute = dc.minute, 156 | let second = dc.second 157 | else { 158 | return nil 159 | } 160 | let nanosecond = dc.nanosecond ?? 0 161 | self.date = date 162 | self.components = (Year(year), Month(month), Day(day), Hour(hour), Minute(minute), Second(second), Nanosecond(nanosecond)) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /Sources/TypedDate/Macro.swift: -------------------------------------------------------------------------------- 1 | @attached(member, names: arbitrary) 2 | internal macro FillInContext() = #externalMacro(module: "TypedDateMacros", type: "FillInContextMacro") 3 | 4 | @attached(member, names: arbitrary) 5 | internal macro EraseContext() = #externalMacro(module: "TypedDateMacros", type: "EraseContextMacro") 6 | 7 | @attached(member, names: arbitrary) 8 | internal macro ModifyContext() = #externalMacro(module: "TypedDateMacros", type: "ModifyContextMacro") 9 | -------------------------------------------------------------------------------- /Sources/TypedDate/TypedDate+Codable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension TypedDate: Codable { 4 | public init(from decoder: Decoder) throws { 5 | let container = try decoder.container(keyedBy: CodingKeys.self) 6 | let date = try container.decode(Date.self, forKey: .date) 7 | let calendar = Calendar.current 8 | 9 | switch Components.self { 10 | case is (Year).Type: 11 | let dc = calendar.dateComponents([.year], from: date) 12 | guard let date = calendar.date(from: dc), 13 | let year = dc.year 14 | else { 15 | throw TypedDateDecodingError.invalidDate 16 | } 17 | self.date = date 18 | self.components = Year(year) as! Components 19 | 20 | case is (Year, Month).Type: 21 | let dc = calendar.dateComponents([.year, .month], from: date) 22 | guard let date = calendar.date(from: dc), 23 | let year = dc.year, 24 | let month = dc.month 25 | else { 26 | throw TypedDateDecodingError.invalidDate 27 | } 28 | self.date = date 29 | self.components = (Year(year), Month(month)) as! Components 30 | 31 | case is (Year, Month, Day).Type: 32 | let dc = calendar.dateComponents([.year, .month, .day], from: date) 33 | guard let date = calendar.date(from: dc), 34 | let year = dc.year, 35 | let month = dc.month, 36 | let day = dc.day 37 | else { 38 | throw TypedDateDecodingError.invalidDate 39 | } 40 | self.date = date 41 | self.components = (Year(year), Month(month), Day(day)) as! Components 42 | 43 | case is (Year, Month, Day, Hour).Type: 44 | let dc = calendar.dateComponents([.year, .month, .day, .hour], from: date) 45 | guard let date = calendar.date(from: dc), 46 | let year = dc.year, 47 | let month = dc.month, 48 | let day = dc.day, 49 | let hour = dc.hour 50 | else { 51 | throw TypedDateDecodingError.invalidDate 52 | } 53 | self.date = date 54 | self.components = (Year(year), Month(month), Day(day), Hour(hour)) as! Components 55 | 56 | case is (Year, Month, Day, Hour, Minute).Type: 57 | let dc = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: date) 58 | guard let date = calendar.date(from: dc), 59 | let year = dc.year, 60 | let month = dc.month, 61 | let day = dc.day, 62 | let hour = dc.hour, 63 | let minute = dc.minute 64 | else { 65 | throw TypedDateDecodingError.invalidDate 66 | } 67 | self.date = date 68 | self.components = (Year(year), Month(month), Day(day), Hour(hour), Minute(minute)) as! Components 69 | 70 | case is (Year, Month, Day, Hour, Minute, Second).Type: 71 | let dc = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: date) 72 | guard let date = calendar.date(from: dc), 73 | let year = dc.year, 74 | let month = dc.month, 75 | let day = dc.day, 76 | let hour = dc.hour, 77 | let minute = dc.minute, 78 | let second = dc.second 79 | else { 80 | throw TypedDateDecodingError.invalidDate 81 | } 82 | self.date = date 83 | self.components = (Year(year), Month(month), Day(day), Hour(hour), Minute(minute), Second(second)) as! Components 84 | 85 | case is (Year, Month, Day, Hour, Minute, Second, Nanosecond).Type: 86 | let dc = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second, .nanosecond], from: date) 87 | guard let date = calendar.date(from: dc), 88 | let year = dc.year, 89 | let month = dc.month, 90 | let day = dc.day, 91 | let hour = dc.hour, 92 | let minute = dc.minute, 93 | let second = dc.second 94 | else { 95 | throw TypedDateDecodingError.invalidDate 96 | } 97 | let nanosecond = dc.nanosecond ?? 0 98 | self.date = date 99 | self.components = (Year(year), Month(month), Day(day), Hour(hour), Minute(minute), Second(second), Nanosecond(nanosecond)) as! Components 100 | 101 | default: throw TypedDateDecodingError.invalidComponents 102 | } 103 | } 104 | 105 | public func encode(to encoder: Encoder) throws { 106 | var container = encoder.container(keyedBy: CodingKeys.self) 107 | try container.encode(date, forKey: .date) 108 | } 109 | 110 | public enum CodingKeys: CodingKey { 111 | case date 112 | } 113 | } 114 | 115 | public enum TypedDateDecodingError: Error { 116 | case invalidDate 117 | case invalidComponents 118 | } 119 | -------------------------------------------------------------------------------- /Sources/TypedDate/TypedDate+Comparable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension TypedDate: Comparable { 4 | public static func == (lhs: TypedDate, rhs: TypedDate) -> Bool { 5 | lhs.date == rhs.date 6 | } 7 | 8 | public static func < (lhs: TypedDate, rhs: TypedDate) -> Bool { 9 | lhs.date < rhs.date 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/TypedDate/TypedDate+Hashable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension TypedDate: Hashable { 4 | public func hash(into hasher: inout Hasher) { 5 | hasher.combine(date) 6 | switch Components.self { 7 | case is (Year).Type: 8 | hasher.combine(1) 9 | 10 | case is (Year, Month).Type: 11 | hasher.combine(2) 12 | 13 | case is (Year, Month, Day).Type: 14 | hasher.combine(3) 15 | 16 | case is (Year, Month, Day, Hour).Type: 17 | hasher.combine(4) 18 | 19 | case is (Year, Month, Day, Hour, Minute).Type: 20 | hasher.combine(5) 21 | 22 | case is (Year, Month, Day, Hour, Minute, Second).Type: 23 | hasher.combine(6) 24 | 25 | case is (Year, Month, Day, Hour, Minute, Second, Nanosecond).Type: 26 | hasher.combine(6) 27 | 28 | default: hasher.combine(7) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/TypedDate/TypedDate+components.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// `TypedDate` extension. This allows you to get the year component of a date. 4 | public extension TypedDate { 5 | /// Returns the year component of `TypedDate`. 6 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 7 | func year(calendar: Calendar = .current) -> Int { 8 | calendar.dateComponents([.year], from: date).year ?? components.value 9 | } 10 | } 11 | 12 | /// `TypedDate<(Year, Month)>` extension. This allows you to get the year and month components of a date. 13 | public extension TypedDate<(Year, Month)> { 14 | /// Returns the year component of `TypedDate<(Year, Month)>`. 15 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 16 | func year(calendar: Calendar = .current) -> Int { 17 | calendar.dateComponents([.year], from: date).year ?? components.0.value 18 | } 19 | 20 | /// Returns the month component of `TypedDate<(Year, Month)>`. 21 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 22 | func month(calendar: Calendar = .current) -> Int { 23 | calendar.dateComponents([.month], from: date).month ?? components.1.value 24 | } 25 | } 26 | 27 | /// `TypedDate<(Year, Month, Day)>` extension. This allows you to get the year, month, and day components of a date. 28 | public extension TypedDate<(Year, Month, Day)> { 29 | /// Returns the year component of `TypedDate<(Year, Month, Day)>`. 30 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 31 | func year(calendar: Calendar = .current) -> Int { 32 | calendar.dateComponents([.year], from: date).year ?? components.0.value 33 | } 34 | 35 | /// Returns the month component of `TypedDate<(Year, Month, Day)>`. 36 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 37 | func month(calendar: Calendar = .current) -> Int { 38 | calendar.dateComponents([.month], from: date).month ?? components.1.value 39 | } 40 | 41 | /// Returns the day component of `TypedDate<(Year, Month, Day)>`. 42 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 43 | func day(calendar: Calendar = .current) -> Int { 44 | calendar.dateComponents([.day], from: date).day ?? components.2.value 45 | } 46 | } 47 | 48 | /// `TypedDate<(Year, Month, Day, Hour)>` extension. This allows you to get the year, month, day, and hour components of a date. 49 | public extension TypedDate<(Year, Month, Day, Hour)> { 50 | /// Returns the year component of `TypedDate<(Year, Month, Day, Hour)>`. 51 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 52 | func year(calendar: Calendar = .current) -> Int { 53 | calendar.dateComponents([.year], from: date).year ?? components.0.value 54 | } 55 | 56 | /// Returns the month component of `TypedDate<(Year, Month, Day, Hour)>`. 57 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 58 | func month(calendar: Calendar = .current) -> Int { 59 | calendar.dateComponents([.month], from: date).month ?? components.1.value 60 | } 61 | 62 | /// Returns the day component of `TypedDate<(Year, Month, Day, Hour)>`. 63 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 64 | func day(calendar: Calendar = .current) -> Int { 65 | calendar.dateComponents([.day], from: date).day ?? components.2.value 66 | } 67 | 68 | /// Returns the hour component of `TypedDate<(Year, Month, Day, Hour)>`. 69 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 70 | func hour(calendar: Calendar = .current) -> Int { 71 | calendar.dateComponents([.hour], from: date).hour ?? components.3.value 72 | } 73 | } 74 | 75 | /// `TypedDate<(Year, Month, Day, Hour, Minute)>` extension. This allows you to get the year, month, day, hour, and minute components of a date. 76 | public extension TypedDate<(Year, Month, Day, Hour, Minute)> { 77 | /// Returns the year component of `TypedDate<(Year, Month, Day, Hour, Minute)>`. 78 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 79 | func year(calendar: Calendar = .current) -> Int { 80 | calendar.dateComponents([.year], from: date).year ?? components.0.value 81 | } 82 | 83 | /// Returns the month component of `TypedDate<(Year, Month, Day, Hour, Minute)>`. 84 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 85 | func month(calendar: Calendar = .current) -> Int { 86 | calendar.dateComponents([.month], from: date).month ?? components.1.value 87 | } 88 | 89 | /// Returns the day component of `TypedDate<(Year, Month, Day, Hour, Minute)>`. 90 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 91 | func day(calendar: Calendar = .current) -> Int { 92 | calendar.dateComponents([.day], from: date).day ?? components.2.value 93 | } 94 | 95 | /// Returns the hour component of `TypedDate<(Year, Month, Day, Hour, Minute)>`. 96 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 97 | func hour(calendar: Calendar = .current) -> Int { 98 | calendar.dateComponents([.hour], from: date).hour ?? components.3.value 99 | } 100 | 101 | /// Returns the minute component of `TypedDate<(Year, Month, Day, Hour, Minute)>`. 102 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 103 | func minute(calendar: Calendar = .current) -> Int { 104 | calendar.dateComponents([.minute], from: date).minute ?? components.4.value 105 | } 106 | } 107 | 108 | /// `TypedDate<(Year, Month, Day, Hour, Minute, Second)>` extension. This allows you to get the year, month, day, hour, minute, and second components of a date. 109 | public extension TypedDate<(Year, Month, Day, Hour, Minute, Second)> { 110 | /// Returns the year component of `TypedDate<(Year, Month, Day, Hour, Minute, Second)>`. 111 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 112 | func year(calendar: Calendar = .current) -> Int { 113 | calendar.dateComponents([.year], from: date).year ?? components.0.value 114 | } 115 | 116 | /// Returns the month component of `TypedDate<(Year, Month, Day, Hour, Minute, Second)>`. 117 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 118 | func month(calendar: Calendar = .current) -> Int { 119 | calendar.dateComponents([.month], from: date).month ?? components.1.value 120 | } 121 | 122 | /// Returns the day component of `TypedDate<(Year, Month, Day, Hour, Minute, Second)>`. 123 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 124 | func day(calendar: Calendar = .current) -> Int { 125 | calendar.dateComponents([.day], from: date).day ?? components.2.value 126 | } 127 | 128 | /// Returns the hour component of `TypedDate<(Year, Month, Day, Hour, Minute, Second)>`. 129 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 130 | func hour(calendar: Calendar = .current) -> Int { 131 | calendar.dateComponents([.hour], from: date).hour ?? components.3.value 132 | } 133 | 134 | /// Returns the minute component of `TypedDate<(Year, Month, Day, Hour, Minute, Second)>`. 135 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 136 | func minute(calendar: Calendar = .current) -> Int { 137 | calendar.dateComponents([.minute], from: date).minute ?? components.4.value 138 | } 139 | 140 | /// Returns the second component of `TypedDate<(Year, Month, Day, Hour, Minute, Second)>`. 141 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 142 | /// - Returns: The second component as an Int. 143 | func second(calendar: Calendar = .current) -> Int { 144 | calendar.dateComponents([.second], from: date).second ?? components.5.value 145 | } 146 | } 147 | 148 | public extension TypedDate<(Year, Month, Day, Hour, Minute, Second, Nanosecond)> { 149 | /// Returns the year component of `TypedDate<(Year, Month, Day, Hour, Minute, Second)>`. 150 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 151 | /// - Returns: Returns the year component of the date as an integer value. 152 | func year(calendar: Calendar = .current) -> Int { 153 | calendar.dateComponents([.year], from: date).year ?? components.0.value 154 | } 155 | 156 | /// Returns the month component of `TypedDate<(Year, Month, Day, Hour, Minute, Second)>`. 157 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 158 | /// - Returns: Returns the month component of the date as an integer value. 159 | func month(calendar: Calendar = .current) -> Int { 160 | calendar.dateComponents([.month], from: date).month ?? components.1.value 161 | } 162 | 163 | /// Returns the day component of `TypedDate<(Year, Month, Day, Hour, Minute, Second)>`. 164 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 165 | /// - Returns: Returns the day component of the date as an integer value. 166 | func day(calendar: Calendar = .current) -> Int { 167 | calendar.dateComponents([.day], from: date).day ?? components.2.value 168 | } 169 | 170 | /// Returns the hour component of `TypedDate<(Year, Month, Day, Hour, Minute, Second)>`. 171 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 172 | /// - Returns: Returns the hour component of the date as an integer value. 173 | func hour(calendar: Calendar = .current) -> Int { 174 | calendar.dateComponents([.hour], from: date).hour ?? components.3.value 175 | } 176 | 177 | /// Returns the minute component of `TypedDate<(Year, Month, Day, Hour, Minute, Second)>`. 178 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 179 | /// - Returns: Returns the minute component of the date as an integer value. 180 | func minute(calendar: Calendar = .current) -> Int { 181 | calendar.dateComponents([.minute], from: date).minute ?? components.4.value 182 | } 183 | 184 | /// Returns the second component of `TypedDate<(Year, Month, Day, Hour, Minute, Second)>`. 185 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 186 | /// - Returns: Returns the second component of the date as an integer value. 187 | func second(calendar: Calendar = .current) -> Int { 188 | calendar.dateComponents([.second], from: date).second ?? components.5.value 189 | } 190 | 191 | /// Returns the nanosecond component of `TypedDate<(Year, Month, Day, Hour, Minute, Second)>`. 192 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 193 | /// - Returns: Returns the nanosecond component of the date as an integer value. 194 | func nanosecond(calendar: Calendar = .current) -> Int { 195 | calendar.dateComponents([.nanosecond], from: date).nanosecond ?? components.6.value 196 | } 197 | } 198 | 199 | extension Date { 200 | /// Returns the year component of `TypedDate<(Year, Month, Day, Hour, Minute, Second)>`. 201 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 202 | /// - Returns: Returns the year component of the date as an integer value. 203 | func year(calendar: Calendar) -> Int? { 204 | calendar.dateComponents([.year], from: self).year 205 | } 206 | 207 | /// Returns the month component of `TypedDate<(Year, Month, Day, Hour, Minute, Second)>`. 208 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 209 | /// - Returns: Returns the month component of the date as an integer value. 210 | func month(calendar: Calendar) -> Int? { 211 | calendar.dateComponents([.month], from: self).month 212 | } 213 | 214 | /// Returns the day component of `TypedDate<(Year, Month, Day, Hour, Minute, Second)>`. 215 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 216 | /// - Returns: Returns the day component of the date as an integer value. 217 | func day(calendar: Calendar ) -> Int? { 218 | calendar.dateComponents([.day], from: self).day 219 | } 220 | 221 | /// Returns the hour component of `TypedDate<(Year, Month, Day, Hour, Minute, Second)>`. 222 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 223 | /// - Returns: Returns the hour component of the date as an integer value. 224 | func hour(calendar: Calendar = .current) -> Int? { 225 | calendar.dateComponents([.hour], from: self).hour 226 | } 227 | 228 | /// Returns the minute component of `TypedDate<(Year, Month, Day, Hour, Minute, Second)>`. 229 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 230 | /// - Returns: Returns the minute component of the date as an integer value. 231 | func minute(calendar: Calendar = .current) -> Int? { 232 | calendar.dateComponents([.minute], from: self).minute 233 | } 234 | 235 | /// Returns the second component of `TypedDate<(Year, Month, Day, Hour, Minute, Second)>`. 236 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 237 | /// - Returns: Returns the second component of the date as an integer value. 238 | func second(calendar: Calendar = .current) -> Int? { 239 | calendar.dateComponents([.second], from: self).second 240 | } 241 | 242 | /// Returns the nanosecond component of `TypedDate<(Year, Month, Day, Hour, Minute, Second)>`. 243 | /// - Parameter calendar: Calendar used for date calculations, defaults to the current calendar. 244 | /// - Returns: Returns the nanosecond component of the date as an integer value. 245 | func nanosecond(calendar: Calendar = .current) -> Int? { 246 | calendar.dateComponents([.nanosecond], from: self).nanosecond 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /Sources/TypedDate/TypedDate+erase.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @EraseContext 4 | public extension TypedDate<(Year, Month)> { 5 | /// Type erase TypedDate<(Year, Month)> to the specified type. 6 | /// - Parameters: 7 | /// - keyPath: Type erase up to the component of Date specified by KeyPath 8 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 9 | /// - Returns: type-erased instance 10 | func erase( 11 | to keyPath: KeyPath<_MonthEraseContext, (TypedDate.Type, T)>, 12 | calendar: Calendar = .current 13 | ) -> TypedDate { 14 | let context = _MonthEraseContext(base: components) [keyPath: keyPath] 15 | return context.0.init(context.1, calendar: calendar) 16 | } 17 | } 18 | 19 | @EraseContext 20 | public extension TypedDate<(Year, Month, Day)> { 21 | /// Type erase TypedDate<(Year, Month, Day)> to the specified type. 22 | /// - Parameters: 23 | /// - keyPath: Type erase up to the component of Date specified by KeyPath 24 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 25 | /// - Returns: type-erased instance 26 | func erase( 27 | to keyPath: KeyPath<_DayEraseContext, (TypedDate.Type, T)>, 28 | calendar: Calendar = .current 29 | ) -> TypedDate { 30 | let context = _DayEraseContext(base: components) [keyPath: keyPath] 31 | return context.0.init(context.1, calendar: calendar) 32 | } 33 | } 34 | 35 | @EraseContext 36 | public extension TypedDate<(Year, Month, Day, Hour)> { 37 | /// Type erase TypedDate<(Year, Month, Day, Hour)> to the specified type. 38 | /// - Parameters: 39 | /// - keyPath: Type erase up to the component of Date specified by KeyPath 40 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 41 | /// - Returns: type-erased instance 42 | func erase( 43 | to keyPath: KeyPath<_HourEraseContext, (TypedDate.Type, T)>, 44 | calendar: Calendar = .current 45 | ) -> TypedDate { 46 | let context = _HourEraseContext(base: components) [keyPath: keyPath] 47 | return context.0.init(context.1, calendar: calendar) 48 | } 49 | } 50 | 51 | @EraseContext 52 | public extension TypedDate<(Year, Month, Day, Hour, Minute)> { 53 | /// Type erase TypedDate<(Year, Month, Day, Hour, Minute)> to the specified type. 54 | /// - Parameters: 55 | /// - keyPath: Type erase up to the component of Date specified by KeyPath 56 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 57 | /// - Returns: type-erased instance 58 | func erase( 59 | to keyPath: KeyPath<_MinuteEraseContext, (TypedDate.Type, T)>, 60 | calendar: Calendar = .current 61 | ) -> TypedDate { 62 | let context = _MinuteEraseContext(base: components) [keyPath: keyPath] 63 | return context.0.init(context.1, calendar: calendar) 64 | } 65 | } 66 | 67 | @EraseContext 68 | public extension TypedDate<(Year, Month, Day, Hour, Minute, Second)> { 69 | /// Type erase TypedDate<(Year, Month, Day, Hour, Minute, Second)> to the specified type. 70 | /// - Parameters: 71 | /// - keyPath: Type erase up to the component of Date specified by KeyPath 72 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 73 | /// - Returns: type-erased instance 74 | func erase( 75 | to keyPath: KeyPath<_SecondEraseContext, (TypedDate.Type, T)>, 76 | calendar: Calendar = .current 77 | ) -> TypedDate { 78 | let context = _SecondEraseContext(base: components) [keyPath: keyPath] 79 | return context.0.init(context.1, calendar: calendar) 80 | } 81 | } 82 | 83 | @EraseContext 84 | public extension TypedDate<(Year, Month, Day, Hour, Minute, Second, Nanosecond)> { 85 | /// Type erase TypedDate<(Year, Month, Day, Hour, Minute, Second, Nanosecond)> to the specified type. 86 | /// - Parameters: 87 | /// - keyPath: Type erase up to the component of Date specified by KeyPath 88 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 89 | /// - Returns: type-erased instance 90 | func erase( 91 | to keyPath: KeyPath<_NanosecondEraseContext, (TypedDate.Type, T)>, 92 | calendar: Calendar = .current 93 | ) -> TypedDate { 94 | let context = _NanosecondEraseContext(base: components) [keyPath: keyPath] 95 | return context.0.init(context.1, calendar: calendar) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Sources/TypedDate/TypedDate+fill.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @FillInContext 4 | public extension TypedDate { 5 | /// Fill in missing Date components 6 | /// 7 | /// - Parameters: 8 | /// - to: KeyPath for specifying components of a Date. 9 | /// - arguments: Tuple of Components of Date to be filled. 10 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 11 | /// - Returns: Instance of TypedDate with more Date Components. 12 | func fill( 13 | to keyPath: KeyPath<_YearFillInContext,(U) -> T>, 14 | arguments: U, 15 | calendar: Calendar = .current 16 | ) -> TypedDate { 17 | let context = _YearFillInContext(base: components) 18 | let transform = context[keyPath: keyPath] 19 | return .init(transform(arguments), calendar: calendar) 20 | } 21 | } 22 | 23 | @FillInContext 24 | public extension TypedDate<(Year, Month)> { 25 | /// Fill in missing Date components 26 | /// 27 | /// - Parameters: 28 | /// - to: KeyPath for specifying components of a Date. 29 | /// - arguments: Tuple of Components of Date to be filled. 30 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 31 | /// - Returns: Instance of TypedDate with more Date Components. 32 | func fill( 33 | to keyPath: KeyPath<_MonthFillInContext,(U) -> T>, 34 | arguments: U, 35 | calendar: Calendar = .current 36 | ) -> TypedDate { 37 | let context = _MonthFillInContext(base: components) 38 | let transform = context[keyPath: keyPath] 39 | return .init(transform(arguments), calendar: calendar) 40 | } 41 | } 42 | 43 | @FillInContext 44 | public extension TypedDate<(Year, Month, Day)> { 45 | /// Fill in missing Date components 46 | /// 47 | /// - Parameters: 48 | /// - to: KeyPath for specifying components of a Date. 49 | /// - arguments: Tuple of Components of Date to be filled. 50 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 51 | /// - Returns: Instance of TypedDate with more Date Components. 52 | func fill( 53 | to keyPath: KeyPath<_DayFillInContext, (U) -> T>, 54 | arguments: U, 55 | calendar: Calendar = .current 56 | ) -> TypedDate { 57 | let context = _DayFillInContext(base: components) 58 | let transform = context[keyPath: keyPath] 59 | return .init(transform(arguments), calendar: calendar) 60 | } 61 | } 62 | 63 | @FillInContext 64 | public extension TypedDate<(Year, Month, Day, Hour)> { 65 | /// Fill in missing Date components 66 | /// 67 | /// - Parameters: 68 | /// - to: KeyPath for specifying components of a Date. 69 | /// - arguments: Tuple of Components of Date to be filled. 70 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 71 | /// - Returns: Instance of TypedDate with more Date Components. 72 | func fill( 73 | to keyPath: KeyPath<_HourFillInContext, (U) -> T>, 74 | arguments: U, 75 | calendar: Calendar = .current 76 | ) -> TypedDate { 77 | let context = _HourFillInContext(base: components) 78 | let transform = context[keyPath: keyPath] 79 | return .init(transform(arguments), calendar: calendar) 80 | } 81 | } 82 | 83 | @FillInContext 84 | public extension TypedDate<(Year, Month, Day, Hour, Minute)> { 85 | /// Fill in missing Date components 86 | /// 87 | /// - Parameters: 88 | /// - to: KeyPath for specifying components of a Date. 89 | /// - arguments: Tuple of Components of Date to be filled. 90 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 91 | /// - Returns: Instance of TypedDate with more Date Components. 92 | func fill( 93 | to keyPath: KeyPath<_MinuteFillInContext, (U) -> T>, 94 | arguments: U, 95 | calendar: Calendar = .current 96 | ) -> TypedDate { 97 | let context = _MinuteFillInContext(base: components) 98 | let transform = context[keyPath: keyPath] 99 | return .init(transform(arguments), calendar: calendar) 100 | } 101 | } 102 | 103 | @FillInContext 104 | public extension TypedDate<(Year, Month, Day, Hour, Minute, Second)> { 105 | /// Fill in missing Date components 106 | /// 107 | /// - Parameters: 108 | /// - to: KeyPath for specifying components of a Date. 109 | /// - arguments: Tuple of Components of Date to be filled. 110 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 111 | /// - Returns: Instance of TypedDate with more Date Components. 112 | func fill( 113 | to keyPath: KeyPath<_SecondFillInContext, (U) -> T>, 114 | arguments: U, 115 | calendar: Calendar = .current 116 | ) -> TypedDate { 117 | let context = _SecondFillInContext(base: components) 118 | let transform = context[keyPath: keyPath] 119 | return .init(transform(arguments), calendar: calendar) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Sources/TypedDate/TypedDate+init.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// `TypedDate` is a structure for handling dates in a type-safe manner. 4 | public extension TypedDate { 5 | /// Initializes a `TypedDate` by specifying the year. 6 | /// - Parameters: 7 | /// - year: The year component of the date. 8 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 9 | /// - Returns: Instance of `TypedDate` with the year component. 10 | init(_ year: Year, calendar: Calendar = .current) where Components == Year { 11 | self.components = year 12 | self.date = DateComponents( 13 | calendar: calendar, 14 | year: year.value 15 | ).date! 16 | } 17 | 18 | /// Initializes a `TypedDate` by specifying the year and month. 19 | /// - Parameters: 20 | /// - year: The year component of the date. 21 | /// - month: The month component of the date. 22 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 23 | /// - Returns: Instance of `TypedDate` with the year and month components. 24 | init( 25 | _ year: Year, 26 | _ month: Month, 27 | calendar: Calendar = .current 28 | ) where Components == (Year, Month) { 29 | self.components = (year, month) 30 | self.date = DateComponents( 31 | calendar: calendar, 32 | year: year.value, 33 | month: month.value 34 | ).date! 35 | } 36 | 37 | /// Initializes a `TypedDate` by specifying the year, month, and day. 38 | /// - Parameters: 39 | /// - year: The year component of the date. 40 | /// - month: The month component of the date. 41 | /// - day: The day component of the date. 42 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 43 | /// - Returns: Instance of `TypedDate` with the year, month, and day components. 44 | init( 45 | _ year: Year, 46 | _ month: Month, 47 | _ day: Day, 48 | calendar: Calendar = .current 49 | ) where Components == (Year, Month, Day) { 50 | self.components = (year, month, day) 51 | self.date = DateComponents( 52 | calendar: calendar, 53 | year: year.value, 54 | month: month.value, 55 | day: day.value 56 | ).date! 57 | } 58 | 59 | /// Initializes a `TypedDate` by specifying the year, month, day, and hour. 60 | /// - Parameters: 61 | /// - year: The year component of the date. 62 | /// - month: The month component of the date. 63 | /// - day: The day component of the date. 64 | /// - hour: The hour component of the date. 65 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 66 | /// - Returns: Instance of `TypedDate` with the year, month, day, and hour components. 67 | init( 68 | _ year: Year, 69 | _ month: Month, 70 | _ day: Day, 71 | _ hour: Hour, 72 | calendar: Calendar = .current 73 | ) where Components == (Year, Month, Day, Hour) { 74 | self.components = (year, month, day, hour) 75 | self.date = DateComponents( 76 | calendar: calendar, 77 | year: year.value, 78 | month: month.value, 79 | day: day.value, 80 | hour: hour.value 81 | ).date! 82 | } 83 | 84 | /// Initializes a `TypedDate` by specifying the year, month, day, hour, and minute. 85 | /// - Parameters: 86 | /// - year: The year component of the date. 87 | /// - month: The month component of the date. 88 | /// - day: The day component of the date. 89 | /// - hour: The hour component of the date. 90 | /// - minute: The minute component of the date. 91 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 92 | /// - Returns: Instance of `TypedDate` with the year, month, day, hour, and minute components. 93 | init( 94 | _ year: Year, 95 | _ month: Month, 96 | _ day: Day, 97 | _ hour: Hour, 98 | _ minute: Minute, 99 | calendar: Calendar = .current 100 | ) where Components == (Year, Month, Day, Hour, Minute) { 101 | self.components = (year, month, day, hour, minute) 102 | self.date = DateComponents( 103 | calendar: calendar, 104 | year: year.value, 105 | month: month.value, 106 | day: day.value, 107 | hour: hour.value, 108 | minute: minute.value 109 | ).date! 110 | } 111 | 112 | /// Initializes a `TypedDate` by specifying the year, month, day, hour, minute, and second. 113 | /// - Parameters: 114 | /// - year: The year component of the date. 115 | /// - month: The month component of the date. 116 | /// - day: The day component of the date. 117 | /// - hour: The hour component of the date. 118 | /// - minute: The minute component of the date. 119 | /// - second: The second component of the date. 120 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 121 | /// - Returns: Instance of `TypedDate` with the year, month, day, hour, minute, and second components. 122 | init( 123 | _ year: Year, 124 | _ month: Month, 125 | _ day: Day, 126 | _ hour: Hour, 127 | _ minute: Minute, 128 | _ second: Second, 129 | calendar: Calendar = .current 130 | ) where Components == (Year, Month, Day, Hour, Minute, Second) { 131 | self.components = (year, month, day, hour, minute, second) 132 | self.date = DateComponents( 133 | calendar: calendar, 134 | year: year.value, 135 | month: month.value, 136 | day: day.value, 137 | hour: hour.value, 138 | minute: minute.value, 139 | second: second.value 140 | ).date! 141 | } 142 | 143 | /// Initializes a `TypedDate` by specifying the year, month, day, hour, minute, second, and nanosecond. 144 | /// - Parameters: 145 | /// - year: The year component of the date. 146 | /// - month: The month component of the date. 147 | /// - day: The day component of the date. 148 | /// - hour: The hour component of the date. 149 | /// - minute: The minute component of the date. 150 | /// - second: The second component of the date. 151 | /// - nanosecond: The nanosecond component of the date. 152 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 153 | /// - Returns: Instance of `TypedDate` with the year, month, day, hour, minute, second, and nanosecond components. 154 | init( 155 | _ year: Year, 156 | _ month: Month, 157 | _ day: Day, 158 | _ hour: Hour, 159 | _ minute: Minute, 160 | _ second: Second, 161 | _ nanosecond: Nanosecond, 162 | calendar: Calendar = .current 163 | ) where Components == (Year, Month, Day, Hour, Minute, Second, Nanosecond) { 164 | self.components = (year, month, day, hour, minute, second, nanosecond) 165 | self.date = DateComponents( 166 | calendar: calendar, 167 | year: year.value, 168 | month: month.value, 169 | day: day.value, 170 | hour: hour.value, 171 | minute: minute.value, 172 | second: second.value, 173 | nanosecond: nanosecond.value 174 | ).date! 175 | } 176 | 177 | /// Initializes a `TypedDate` by specifying the year, month, day, hour, minute, and fractional second. 178 | /// - Parameters: 179 | /// - year: The year component of the date. 180 | /// - month: The month component of the date. 181 | /// - day: The day component of the date. 182 | /// - hour: The hour component of the date. 183 | /// - minute: The minute component of the date. 184 | /// - fractionalSecond: The fractional second component of the date. 185 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 186 | /// - Returns: Instance of `TypedDate` with the year, month, day, hour, minute, and fractional second components. 187 | init( 188 | _ year: Year, 189 | _ month: Month, 190 | _ day: Day, 191 | _ hour: Hour, 192 | _ minute: Minute, 193 | _ fractionalSecond: FractionalSecond, 194 | calendar: Calendar = .current 195 | ) where Components == (Year, Month, Day, Hour, Minute, Second, Nanosecond) { 196 | self.components = (year, month, day, hour, minute, Second(fractionalSecond.seconds), Nanosecond(fractionalSecond.nanoseconds)) 197 | self.date = DateComponents( 198 | calendar: calendar, 199 | year: year.value, 200 | month: month.value, 201 | day: day.value, 202 | hour: hour.value, 203 | minute: minute.value, 204 | second: components.5.value, 205 | nanosecond: components.6.value 206 | ).date! 207 | } 208 | } 209 | 210 | extension TypedDate { 211 | init(_ components: Components, calendar: Calendar = .current) { 212 | switch components { 213 | case let components as Year: 214 | self.date = DateComponents( 215 | calendar: calendar, 216 | year: components.value 217 | ).date! 218 | self.components = Year(date.year(calendar: calendar) ?? components.value) as! Components 219 | case let components as (Year, Month): 220 | self.date = DateComponents( 221 | calendar: calendar, 222 | year: components.0.value, 223 | month: components.1.value 224 | ).date! 225 | self.components = ( 226 | Year(date.year(calendar: calendar) ?? components.0.value), 227 | Month(date.month(calendar: calendar) ?? components.1.value) 228 | ) as! Components 229 | case let components as (Year, Month, Day): 230 | self.date = DateComponents( 231 | calendar: calendar, 232 | year: components.0.value, 233 | month: components.1.value, 234 | day: components.2.value 235 | ).date! 236 | self.components = ( 237 | Year(date.year(calendar: calendar) ?? components.0.value), 238 | Month(date.month(calendar: calendar) ?? components.1.value), 239 | Day(date.day(calendar: calendar) ?? components.2.value) 240 | ) as! Components 241 | case let components as (Year, Month, Day, Hour): 242 | self.date = DateComponents( 243 | calendar: calendar, 244 | year: components.0.value, 245 | month: components.1.value, 246 | day: components.2.value, 247 | hour: components.3.value 248 | ).date! 249 | self.components = ( 250 | Year(date.year(calendar: calendar) ?? components.0.value), 251 | Month(date.month(calendar: calendar) ?? components.1.value), 252 | Day(date.day(calendar: calendar) ?? components.2.value), 253 | Hour(date.hour(calendar: calendar) ?? components.3.value) 254 | ) as! Components 255 | case let components as (Year, Month, Day, Hour, Minute): 256 | self.date = DateComponents( 257 | calendar: calendar, 258 | year: components.0.value, 259 | month: components.1.value, 260 | day: components.2.value, 261 | hour: components.3.value, 262 | minute: components.4.value 263 | ).date! 264 | self.components = ( 265 | Year(date.year(calendar: calendar) ?? components.0.value), 266 | Month(date.month(calendar: calendar) ?? components.1.value), 267 | Day(date.day(calendar: calendar) ?? components.2.value), 268 | Hour(date.hour(calendar: calendar) ?? components.3.value), 269 | Minute(date.minute(calendar: calendar) ?? components.4.value) 270 | ) as! Components 271 | case let components as (Year, Month, Day, Hour, Minute, Second): 272 | self.date = DateComponents( 273 | calendar: calendar, 274 | year: components.0.value, 275 | month: components.1.value, 276 | day: components.2.value, 277 | hour: components.3.value, 278 | minute: components.4.value, 279 | second: components.5.value 280 | ).date! 281 | self.components = ( 282 | Year(date.year(calendar: calendar) ?? components.0.value), 283 | Month(date.month(calendar: calendar) ?? components.1.value), 284 | Day(date.day(calendar: calendar) ?? components.2.value), 285 | Hour(date.hour(calendar: calendar) ?? components.3.value), 286 | Minute(date.minute(calendar: calendar) ?? components.4.value), 287 | Second(date.second(calendar: calendar) ?? components.5.value) 288 | ) as! Components 289 | case let components as (Year, Month, Day, Hour, Minute, Second, Nanosecond): 290 | self.date = DateComponents( 291 | calendar: calendar, 292 | year: components.0.value, 293 | month: components.1.value, 294 | day: components.2.value, 295 | hour: components.3.value, 296 | minute: components.4.value, 297 | second: components.5.value, 298 | nanosecond: components.6.value 299 | ).date! 300 | self.components = ( 301 | Year(date.year(calendar: calendar) ?? components.0.value), 302 | Month(date.month(calendar: calendar) ?? components.1.value), 303 | Day(date.day(calendar: calendar) ?? components.2.value), 304 | Hour(date.hour(calendar: calendar) ?? components.3.value), 305 | Minute(date.minute(calendar: calendar) ?? components.4.value), 306 | Second(date.second(calendar: calendar) ?? components.5.value), 307 | Nanosecond(date.nanosecond(calendar: calendar) ?? components.6.value) 308 | ) as! Components 309 | 310 | default: 311 | fatalError( 312 | """ 313 | Unexpected components: \(components) 314 | """ 315 | ) 316 | } 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /Sources/TypedDate/TypedDate+modify.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @ModifyContext 4 | public extension TypedDate { 5 | /// Modifies a specific date component with a provided modification closure. 6 | /// - Parameters: 7 | /// - keyPath: Specify the Components of the Date you want to change by keyPath. 8 | /// - modify: Closure to change the value. 9 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 10 | /// - Returns: A new `TypedDate` instance with the modified component. 11 | func modifying( 12 | _ keyPath: KeyPath<_YearModifyContext, (T, (T) -> Components)>, 13 | calendar: Calendar = .current, 14 | modify: (inout T) -> Void 15 | ) -> TypedDate { 16 | let context = _YearModifyContext(base: components) 17 | var (target, transform) = context[keyPath: keyPath] 18 | modify(&target) 19 | return .init(transform(target), calendar: calendar) 20 | } 21 | 22 | /// Mutates a specific date component of with a provided modification closure. 23 | /// - Parameters: 24 | /// - keyPath: Specify the Components of the Date you want to change by keyPath. 25 | /// - modify: Closure to change the value. 26 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 27 | mutating func modify( 28 | _ keyPath: KeyPath<_YearModifyContext, (T, (T) -> Components)>, 29 | calendar: Calendar = .current, 30 | modify: (inout T) -> Void 31 | ) { 32 | self = modifying(keyPath, calendar: calendar, modify: modify) 33 | } 34 | 35 | /// Adds a specific value to a date component. 36 | /// - Parameters: 37 | /// - keyPath: A KeyPath specifying the date component to be modified. 38 | /// - value: The value to be added. 39 | /// - calendar: The calendar used for date calculations, defaults to the current calendar. 40 | /// - Returns: A new `TypedDate` instance with the modified component. 41 | func add( 42 | _ keyPath: KeyPath<_YearModifyContext, (T, (T) -> Components)>, 43 | _ value: T, 44 | calendar: Calendar = .current 45 | ) -> TypedDate { 46 | modifying(keyPath, calendar: calendar, modify: { $0 += value }) 47 | } 48 | } 49 | 50 | @ModifyContext 51 | public extension TypedDate<(Year, Month)> { 52 | /// Modifies a specific date component with a provided modification closure. 53 | /// - Parameters: 54 | /// - keyPath: Specify the Components of the Date you want to change by keyPath. 55 | /// - modify: Closure to change the value. 56 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 57 | /// - Returns: A new `TypedDate` instance with the modified component. 58 | func modifying( 59 | _ keyPath: KeyPath<_MonthModifyContext, (T, (T) -> Components)>, 60 | calendar: Calendar = .current, 61 | modify: (inout T) -> Void 62 | ) -> TypedDate { 63 | let context = _MonthModifyContext(base: components) 64 | var (target, transform) = context[keyPath: keyPath] 65 | modify(&target) 66 | return .init(transform(target), calendar: calendar) 67 | } 68 | 69 | /// Mutates a specific date component with a provided modification closure. 70 | /// - Parameters: 71 | /// - keyPath: Specify the Components of the Date you want to change by keyPath. 72 | /// - modify: Closure to change the value. 73 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 74 | mutating func modify( 75 | _ keyPath: KeyPath<_MonthModifyContext, (T, (T) -> Components)>, 76 | calendar: Calendar = .current, 77 | modify: (inout T) -> Void 78 | ) { 79 | self = modifying(keyPath, calendar: calendar, modify: modify) 80 | } 81 | 82 | /// Adds a specific value to a date component. 83 | /// - Parameters: 84 | /// - keyPath: A KeyPath specifying the date component to be modified. 85 | /// - value: The value to be added. 86 | /// - calendar: The calendar used for date calculations, defaults to the current calendar. 87 | /// - Returns: A new `TypedDate` instance with the modified component. 88 | func add( 89 | _ keyPath: KeyPath<_MonthModifyContext, (T, (T) -> Components)>, 90 | _ value: T, 91 | calendar: Calendar = .current 92 | ) -> TypedDate { 93 | modifying(keyPath, calendar: calendar, modify: { $0 += value }) 94 | } 95 | } 96 | 97 | @ModifyContext 98 | public extension TypedDate<(Year, Month, Day)> { 99 | /// Modifies a specific date component with a provided modification closure. 100 | /// - Parameters: 101 | /// - keyPath: Specify the Components of the Date you want to change by keyPath. 102 | /// - modify: Closure to change the value. 103 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 104 | /// - Returns: A new `TypedDate` instance with the modified component. 105 | func modifying( 106 | _ keyPath: KeyPath<_DayModifyContext, (T, (T) -> Components)>, 107 | calendar: Calendar = .current, 108 | modify: (inout T) -> Void 109 | ) -> TypedDate { 110 | let context = _DayModifyContext(base: components) 111 | var (target, transform) = context[keyPath: keyPath] 112 | modify(&target) 113 | return .init(transform(target), calendar: calendar) 114 | } 115 | 116 | /// Mutates a specific date component with a provided modification closure. 117 | /// - Parameters: 118 | /// - keyPath: Specify the Components of the Date you want to change by keyPath. 119 | /// - modify: Closure to change the value. 120 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 121 | mutating func modify( 122 | _ keyPath: KeyPath<_DayModifyContext, (T, (T) -> Components)>, 123 | calendar: Calendar = .current, 124 | modify: (inout T) -> Void 125 | ) { 126 | self = modifying(keyPath, calendar: calendar, modify: modify) 127 | } 128 | 129 | /// Adds a specific value to a date component. 130 | /// - Parameters: 131 | /// - keyPath: A KeyPath specifying the date component to be modified. 132 | /// - value: The value to be added. 133 | /// - calendar: The calendar used for date calculations, defaults to the current calendar. 134 | /// - Returns: A new `TypedDate` instance with the modified component. 135 | func add( 136 | _ keyPath: KeyPath<_DayModifyContext, (T, (T) -> Components)>, 137 | _ value: T, 138 | calendar: Calendar = .current 139 | ) -> TypedDate { 140 | modifying(keyPath, calendar: calendar, modify: { $0 += value }) 141 | } 142 | } 143 | 144 | @ModifyContext 145 | public extension TypedDate<(Year, Month, Day, Hour)> { 146 | /// Modifies a specific date component with a provided modification closure. 147 | /// - Parameters: 148 | /// - keyPath: Specify the Components of the Date you want to change by keyPath. 149 | /// - modify: Closure to change the value. 150 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 151 | /// - Returns: A new `TypedDate` instance with the modified component. 152 | func modifying( 153 | _ keyPath: KeyPath<_HourModifyContext, (T, (T) -> Components)>, 154 | calendar: Calendar = .current, 155 | modify: (inout T) -> Void 156 | ) -> TypedDate { 157 | let context = _HourModifyContext(base: components) 158 | var (target, transform) = context[keyPath: keyPath] 159 | modify(&target) 160 | return .init(transform(target), calendar: calendar) 161 | } 162 | 163 | /// Mutates a specific date component with a provided modification closure. 164 | /// - Parameters: 165 | /// - keyPath: Specify the Components of the Date you want to change by keyPath. 166 | /// - modify: Closure to change the value. 167 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 168 | mutating func modify( 169 | _ keyPath: KeyPath<_HourModifyContext, (T, (T) -> Components)>, 170 | calendar: Calendar = .current, 171 | modify: (inout T) -> Void 172 | ) { 173 | self = modifying(keyPath, calendar: calendar, modify: modify) 174 | } 175 | 176 | /// Adds a specific value to a date component. 177 | /// - Parameters: 178 | /// - keyPath: A KeyPath specifying the date component to be modified. 179 | /// - value: The value to be added. 180 | /// - calendar: The calendar used for date calculations, defaults to the current calendar. 181 | /// - Returns: A new `TypedDate` instance with the modified component. 182 | func add( 183 | _ keyPath: KeyPath<_HourModifyContext, (T, (T) -> Components)>, 184 | _ value: T, 185 | calendar: Calendar = .current 186 | ) -> TypedDate { 187 | modifying(keyPath, calendar: calendar, modify: { $0 += value }) 188 | } 189 | } 190 | 191 | @ModifyContext 192 | public extension TypedDate<(Year, Month, Day, Hour, Minute)> { 193 | /// Modifies a specific date component with a provided modification closure. 194 | /// - Parameters: 195 | /// - keyPath: Specify the Components of the Date you want to change by keyPath. 196 | /// - modify: Closure to change the value. 197 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 198 | /// - Returns: A new `TypedDate` instance with the modified component. 199 | func modifying( 200 | _ keyPath: KeyPath<_MinuteModifyContext, (T, (T) -> Components)>, 201 | calendar: Calendar = .current, 202 | modify: (inout T) -> Void 203 | ) -> TypedDate { 204 | let context = _MinuteModifyContext(base: components) 205 | var (target, transform) = context[keyPath: keyPath] 206 | modify(&target) 207 | return .init(transform(target), calendar: calendar) 208 | } 209 | 210 | /// Mutates a specific date component with a provided modification closure. 211 | /// - Parameters: 212 | /// - keyPath: Specify the Components of the Date you want to change by keyPath. 213 | /// - modify: Closure to change the value. 214 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 215 | mutating func modify( 216 | _ keyPath: KeyPath<_MinuteModifyContext, (T, (T) -> Components)>, 217 | calendar: Calendar = .current, 218 | modify: (inout T) -> Void 219 | ) { 220 | self = modifying(keyPath, calendar: calendar, modify: modify) 221 | } 222 | 223 | /// Adds a specific value to a date component. 224 | /// - Parameters: 225 | /// - keyPath: A KeyPath specifying the date component to be modified. 226 | /// - value: The value to be added. 227 | /// - calendar: The calendar used for date calculations, defaults to the current calendar. 228 | /// - Returns: A new `TypedDate` instance with the modified component. 229 | func add( 230 | _ keyPath: KeyPath<_MinuteModifyContext, (T, (T) -> Components)>, 231 | _ value: T, 232 | calendar: Calendar = .current 233 | ) -> TypedDate { 234 | modifying(keyPath, calendar: calendar, modify: { $0 += value }) 235 | } 236 | } 237 | 238 | @ModifyContext 239 | public extension TypedDate<(Year, Month, Day, Hour, Minute, Second)> { 240 | /// Modifies a specific date component with a provided modification closure. 241 | /// - Parameters: 242 | /// - keyPath: Specify the Components of the Date you want to change by keyPath. 243 | /// - modify: Closure to change the value. 244 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 245 | /// - Returns: A new `TypedDate` instance with the modified component. 246 | func modifying( 247 | _ keyPath: KeyPath<_SecondModifyContext, (T, (T) -> Components)>, 248 | calendar: Calendar = .current, 249 | modify: (inout T) -> Void 250 | ) -> TypedDate { 251 | let context = _SecondModifyContext(base: components) 252 | var (target, transform) = context[keyPath: keyPath] 253 | modify(&target) 254 | return .init(transform(target), calendar: calendar) 255 | } 256 | 257 | /// Mutates a specific date component with a provided modification closure. 258 | /// - Parameters: 259 | /// - keyPath: Specify the Components of the Date you want to change by keyPath. 260 | /// - modify: Closure to change the value. 261 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 262 | mutating func modify( 263 | _ keyPath: KeyPath<_SecondModifyContext, (T, (T) -> Components)>, 264 | calendar: Calendar = .current, 265 | modify: (inout T) -> Void 266 | ) { 267 | self = modifying(keyPath, calendar: calendar, modify: modify) 268 | } 269 | 270 | /// Adds a specific value to a date component. 271 | /// - Parameters: 272 | /// - keyPath: A KeyPath specifying the date component to be modified. 273 | /// - value: The value to be added. 274 | /// - calendar: The calendar used for date calculations, defaults to the current calendar. 275 | /// - Returns: A new `TypedDate` instance with the modified component. 276 | func add( 277 | _ keyPath: KeyPath<_SecondModifyContext, (T, (T) -> Components)>, 278 | _ value: T, 279 | calendar: Calendar = .current 280 | ) -> TypedDate { 281 | modifying(keyPath, calendar: calendar, modify: { $0 += value }) 282 | } 283 | } 284 | 285 | @ModifyContext 286 | public extension TypedDate<(Year, Month, Day, Hour, Minute, Second, Nanosecond)> { 287 | /// Modifies a specific date component with a provided modification closure. 288 | /// - Parameters: 289 | /// - keyPath: Specify the Components of the Date you want to change by keyPath. 290 | /// - modify: Closure to change the value. 291 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 292 | /// - Returns: A new `TypedDate` instance with the modified component. 293 | func modifying( 294 | _ keyPath: KeyPath<_NanosecondModifyContext, (T, (T) -> Components)>, 295 | calendar: Calendar = .current, 296 | modify: (inout T) -> Void 297 | ) -> TypedDate { 298 | let context = _NanosecondModifyContext(base: components) 299 | var (target, transform) = context[keyPath: keyPath] 300 | modify(&target) 301 | return .init(transform(target), calendar: calendar) 302 | } 303 | 304 | /// Mutates a specific date component with a provided modification closure. 305 | /// - Parameters: 306 | /// - keyPath: Specify the Components of the Date you want to change by keyPath. 307 | /// - modify: Closure to change the value. 308 | /// - calendar: Calendar used for date calculations, defaults to the current calendar. 309 | mutating func modify( 310 | _ keyPath: KeyPath<_NanosecondModifyContext, (T, (T) -> Components)>, 311 | calendar: Calendar = .current, 312 | modify: (inout T) -> Void 313 | ) { 314 | self = modifying(keyPath, calendar: calendar, modify: modify) 315 | } 316 | 317 | /// Adds a specific value to a date component. 318 | /// - Parameters: 319 | /// - keyPath: A KeyPath specifying the date component to be modified. 320 | /// - value: The value to be added. 321 | /// - calendar: The calendar used for date calculations, defaults to the current calendar. 322 | /// - Returns: A new `TypedDate` instance with the modified component. 323 | func add( 324 | _ keyPath: KeyPath<_NanosecondModifyContext, (T, (T) -> Components)>, 325 | _ value: T, 326 | calendar: Calendar = .current 327 | ) -> TypedDate { 328 | modifying(keyPath, calendar: calendar, modify: { $0 += value }) 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /Sources/TypedDate/TypedDate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A `TypedDate` struct that encapsulates a `Date` object with associated date components. 4 | /// This struct provides a type-safe way to handle dates with specific components, 5 | /// such as year, month, day, etc. The `Components` type parameter defines the date components 6 | /// associated with this `TypedDate`. 7 | public struct TypedDate: TypedDateProtocol, Sendable { 8 | /// The underlying `Date` object. 9 | /// This property stores the actual date value represented by the `TypedDate` instance. 10 | public let date: Date 11 | 12 | /// The components associated with the `TypedDate`. 13 | /// These components are of the type specified by the `Components` generic parameter, 14 | /// and they represent various aspects of the date, such as year, month, day, etc. 15 | let components: Components 16 | } 17 | -------------------------------------------------------------------------------- /Sources/TypedDate/TypedDateOf.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A convenience type alias that represents a `TypedDate` with only the year component. 4 | public typealias TypedDateOfYear = TypedDate 5 | 6 | /// A convenience type alias that represents a `TypedDate` with the year and month components. 7 | public typealias TypedDateOfMonth = TypedDate<(Year, Month)> 8 | 9 | /// A convenience type alias that represents a `TypedDate` with the year, month, and day components. 10 | public typealias TypedDateOfDay = TypedDate<(Year, Month, Day)> 11 | 12 | /// A convenience type alias that represents a `TypedDate` with the year, month, day, and hour components. 13 | public typealias TypedDateOfHour = TypedDate<(Year, Month, Day, Hour)> 14 | 15 | /// A convenience type alias that represents a `TypedDate` with the year, month, day, hour, and minute components. 16 | public typealias TypedDateOfMinute = TypedDate<(Year, Month, Day, Hour, Minute)> 17 | 18 | /// A convenience type alias that represents a `TypedDate` with the year, month, day, hour, minute, and second components. 19 | public typealias TypedDateOfSecond = TypedDate<(Year, Month, Day, Hour, Minute, Second)> 20 | 21 | /// A convenience type alias that represents a `TypedDate` with the year, month, day, hour, minute, second, and nanosecond components. 22 | public typealias TypedDateOfNanosecond = TypedDate<(Year, Month, Day, Hour, Minute, Second, Nanosecond)> 23 | 24 | public extension TypedDateOfYear { 25 | /// Returns a `TypedDate` instance that represents the current year, at the moment of access. 26 | static var now: Self { 27 | Date().scope(to: \.year) 28 | } 29 | } 30 | 31 | public extension TypedDateOfMonth { 32 | /// Returns a `TypedDate` instance that represents the current year and month, at the moment of access. 33 | static var now: Self { 34 | Date().scope(to: \.month) 35 | } 36 | } 37 | 38 | public extension TypedDateOfDay { 39 | /// Returns a `TypedDate` instance that represents the current year, month, and day, at the moment of access. 40 | static var now: Self { 41 | Date().scope(to: \.day) 42 | } 43 | } 44 | 45 | public extension TypedDateOfHour { 46 | /// Returns a `TypedDate` instance that represents the current year, month, day, and hour, at the moment of access. 47 | static var now: Self { 48 | Date().scope(to: \.hour) 49 | } 50 | } 51 | 52 | public extension TypedDateOfMinute { 53 | /// Returns a `TypedDate` instance that represents the current year, month, day, hour, and minute, at the moment of access. 54 | static var now: Self { 55 | Date().scope(to: \.minute) 56 | } 57 | } 58 | 59 | public extension TypedDateOfSecond { 60 | /// Returns a `TypedDate` instance that represents the current year, month, day, hour, minute, and second, at the moment of access. 61 | static var now: Self { 62 | Date().scope(to: \.second) 63 | } 64 | } 65 | 66 | public extension TypedDateOfNanosecond { 67 | /// Returns a `TypedDate` instance that represents the current year, month, day, hour, minute, second, and nanosecond, at the moment of access. 68 | static var now: Self { 69 | Date().scope(to: \.nanosecond) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/TypedDate/TypedDateProtocol.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// The base protocol for typed date 4 | public protocol TypedDateProtocol: Hashable, Codable, Equatable, Comparable { 5 | var date: Date { get } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/TypedDate/exported.swift: -------------------------------------------------------------------------------- 1 | @_exported import TypedDateCore 2 | -------------------------------------------------------------------------------- /Sources/TypedDateCore/TypedDateUnits+BinaryInteger.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol TypedDateUnit: BinaryInteger where Words == Int.Words, Magnitude == Int.Magnitude, IntegerLiteralType == Int.IntegerLiteralType { 4 | var value: Int { get } 5 | init(_: Int) 6 | } 7 | 8 | public extension TypedDateUnit { 9 | static var zero: Self { 10 | .init(0) 11 | } 12 | 13 | static func + (lhs: Self, rhs: Self) -> Self { 14 | Self(lhs.value + rhs.value) 15 | } 16 | 17 | static func - (lhs: Self, rhs: Self) -> Self { 18 | Self(lhs.value - rhs.value) 19 | } 20 | 21 | static var isSigned: Bool { 22 | Int.isSigned 23 | } 24 | 25 | init(_ source: T) where T : BinaryFloatingPoint { 26 | self.init(Int(source)) 27 | } 28 | 29 | var words: Int.Words { 30 | value.words 31 | } 32 | 33 | var bitWidth: Int { 34 | value.bitWidth 35 | } 36 | 37 | var trailingZeroBitCount: Int { 38 | value.trailingZeroBitCount 39 | } 40 | 41 | static func / (lhs: Self, rhs: Self) -> Self { 42 | Self(lhs.value / rhs.value) 43 | } 44 | 45 | static func % (lhs: Self, rhs: Self) -> Self { 46 | Self(lhs.value % rhs.value) 47 | } 48 | 49 | static func %= (lhs: inout Self, rhs: Self) { 50 | lhs = Self(lhs.value % rhs.value) 51 | } 52 | 53 | static func * (lhs: Self, rhs: Self) -> Self { 54 | Self(lhs.value * rhs.value) 55 | } 56 | 57 | static func *= (lhs: inout Self, rhs: Self) { 58 | lhs = Self(lhs.value * rhs.value) 59 | } 60 | 61 | static func &= (lhs: inout Self, rhs: Self) { 62 | lhs = Self(lhs.value & rhs.value) 63 | } 64 | 65 | static func |= (lhs: inout Self, rhs: Self) { 66 | lhs = Self(lhs.value | rhs.value) 67 | } 68 | 69 | static func ^= (lhs: inout Self, rhs: Self) { 70 | lhs = Self(lhs.value ^ rhs.value) 71 | } 72 | 73 | var magnitude: UInt { 74 | value.magnitude 75 | } 76 | 77 | init(integerLiteral value: Int) { 78 | self.init(value) 79 | } 80 | 81 | static func <<= (lhs: inout Self, rhs: RHS) where RHS : BinaryInteger { 82 | lhs = Self(lhs.value << rhs) 83 | } 84 | 85 | static func >>= (lhs: inout Self, rhs: RHS) where RHS : BinaryInteger { 86 | lhs = Self(lhs.value >> rhs) 87 | } 88 | 89 | static prefix func ~ (x: Self) -> Self { 90 | Self(~x.value) 91 | } 92 | 93 | static func /= (lhs: inout Self, rhs: Self) { 94 | lhs = Self(lhs.value / rhs.value) 95 | } 96 | 97 | init?(exactly source: T) where T : BinaryInteger { 98 | if let integer = Int(exactly: source) { 99 | self.init(integer) 100 | } else { 101 | return nil 102 | } 103 | } 104 | 105 | init?(exactly source: T) where T : BinaryFloatingPoint { 106 | if let integer = Int(exactly: source) { 107 | self.init(integer) 108 | } else { 109 | return nil 110 | } 111 | } 112 | 113 | init(_ source: T) where T : BinaryInteger { 114 | self.init(Int(source)) 115 | } 116 | 117 | init(truncatingIfNeeded source: T) where T : BinaryInteger { 118 | self.init(Int(truncatingIfNeeded: source)) 119 | } 120 | 121 | init(clamping source: T) where T : BinaryInteger { 122 | self.init(Int(clamping: source)) 123 | } 124 | 125 | func add(_ value: Int) -> Self { 126 | Self(self.value + value) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Sources/TypedDateCore/TypedDateUnits.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | package let allUnitNames = [ 4 | Year.typeName, 5 | Month.typeName, 6 | Day.typeName, 7 | Hour.typeName, 8 | Minute.typeName, 9 | Second.typeName, 10 | Nanosecond.typeName 11 | ] 12 | 13 | package extension TypedDateUnit { 14 | static var typeName: String { 15 | String(String(describing: Self.self)) 16 | } 17 | } 18 | 19 | /// Representing a year. 20 | public struct Year: TypedDateUnit, Sendable { 21 | public let value: Int 22 | 23 | public init(_ year: Int) { 24 | self.value = year 25 | } 26 | } 27 | 28 | /// Representing a month. 29 | public struct Month: TypedDateUnit, Sendable { 30 | public let value: Int 31 | 32 | public init(_ month: Int) { 33 | self.value = month 34 | } 35 | } 36 | 37 | /// Representing a day. 38 | public struct Day: TypedDateUnit, Sendable { 39 | public let value: Int 40 | 41 | public init(_ day: Int) { 42 | self.value = day 43 | } 44 | } 45 | 46 | /// Representing a hour. 47 | public struct Hour: TypedDateUnit, Sendable { 48 | public let value: Int 49 | 50 | public init(_ hours: Int) { 51 | self.value = hours 52 | } 53 | } 54 | 55 | /// Representing a month. 56 | public struct Minute: TypedDateUnit, Sendable { 57 | public let value: Int 58 | 59 | public init(_ value: Int) { 60 | self.value = value 61 | } 62 | } 63 | 64 | /// Representing a second. 65 | public struct Second: TypedDateUnit, Sendable { 66 | public let value: Int 67 | 68 | public init(_ second: Int) { 69 | self.value = second 70 | } 71 | } 72 | 73 | /// Representing a nanosecond. 74 | public struct Nanosecond: TypedDateUnit, Sendable { 75 | public let value: Int 76 | 77 | public init(_ nanosecond: Int) { 78 | self.value = nanosecond 79 | } 80 | } 81 | 82 | /// Representing a fractional second. 83 | public struct FractionalSecond: Sendable { 84 | public let value: Double 85 | 86 | public var seconds: Int { 87 | Int(value) 88 | } 89 | public var nanoseconds: Int { 90 | Int((value - floor(value)) * 1_000_000_000) 91 | } 92 | 93 | public init(_ value: Double) { 94 | self.value = value 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Sources/TypedDateMacros/EraseContextMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftCompilerPlugin 2 | import SwiftSyntax 3 | import SwiftSyntaxBuilder 4 | import SwiftSyntaxMacros 5 | import SwiftDiagnostics 6 | import TypedDateCore 7 | import Foundation 8 | 9 | // Note: This macro does not generate a detailed DiagnosticError because it is only used in TypedDate package. 10 | package struct EraseContextMacro: MemberMacro { 11 | package static func expansion( 12 | of node: SwiftSyntax.AttributeSyntax, 13 | providingMembersOf declaration: some SwiftSyntax.DeclGroupSyntax, 14 | in context: some SwiftSyntaxMacros.MacroExpansionContext 15 | ) throws -> [SwiftSyntax.DeclSyntax] { 16 | guard let extensionDecl = declaration.as(ExtensionDeclSyntax.self), 17 | let genericArgument = extensionDecl.extendedType.as(IdentifierTypeSyntax.self)?.genericArgumentClause?.arguments.first?.argument 18 | else { 19 | throw DiagnosticsError.init(diagnostics: []) 20 | } 21 | 22 | let baseComponents = if let identifierType = genericArgument.as(IdentifierTypeSyntax.self) { 23 | [identifierType.name.text] 24 | } else if let tupleType = genericArgument.as(TupleTypeSyntax.self) { 25 | tupleType.argumentTypes 26 | } else { 27 | throw DiagnosticsError.init(diagnostics: []) 28 | } 29 | 30 | return [ 31 | try generateEraseContext(of: baseComponents).cast(DeclSyntax.self), 32 | ] 33 | } 34 | 35 | private static func generateEraseContext(of baseComponents: [String]) throws -> StructDeclSyntax { 36 | StructDeclSyntax( 37 | name: .identifier("_\(baseComponents.last ?? "_")EraseContext"), 38 | memberBlock: try MemberBlockSyntax { 39 | try MemberBlockItemListSyntax { 40 | try MemberBlockItemSyntax( 41 | decl: VariableDeclSyntax("private let base: Components") 42 | ) 43 | let erasedComponents = baseComponents.allSubarrays().dropLast() 44 | for components in erasedComponents { 45 | let type = components.returnType 46 | let typeName = components.last?.lowercased() ?? "" 47 | MemberBlockItemSyntax( 48 | decl: DeclSyntax("public let \(raw: typeName): (TypedDate<\(raw: type)>.Type, \(raw: type))") 49 | ) 50 | } 51 | try MemberBlockItemSyntax( 52 | decl: InitializerDeclSyntax("init(base: Components)") { 53 | """ 54 | self.base = base 55 | \(raw: erasedComponents.map { generateInitializerCodeBlockList(of: $0) }.joined(separator: "\n")) 56 | """ 57 | } 58 | ) 59 | } 60 | } 61 | ) 62 | } 63 | 64 | private static func generateInitializerCodeBlockList(of erasedComponents: [String]) -> String { 65 | let type = erasedComponents.returnType 66 | let arguments = erasedComponents 67 | .enumerated() 68 | .map { i, _ in "base.\(i)" } 69 | .returnType 70 | return "\(erasedComponents.last!.lowercased()) = (TypedDate<\(type)>.self, \(arguments))" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/TypedDateMacros/FillInContextMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftCompilerPlugin 2 | import SwiftSyntax 3 | import SwiftSyntaxBuilder 4 | import SwiftSyntaxMacros 5 | import SwiftDiagnostics 6 | import TypedDateCore 7 | import Foundation 8 | 9 | // Note: This macro does not generate a detailed DiagnosticError because it is only used in TypedDate package. 10 | package struct FillInContextMacro: MemberMacro { 11 | package static func expansion( 12 | of node: SwiftSyntax.AttributeSyntax, 13 | providingMembersOf declaration: some SwiftSyntax.DeclGroupSyntax, 14 | in context: some SwiftSyntaxMacros.MacroExpansionContext 15 | ) throws -> [SwiftSyntax.DeclSyntax] { 16 | guard let extensionDecl = declaration.as(ExtensionDeclSyntax.self), 17 | let genericArgument = extensionDecl.extendedType.as(IdentifierTypeSyntax.self)?.genericArgumentClause?.arguments.first?.argument 18 | else { 19 | throw DiagnosticsError.init(diagnostics: []) 20 | } 21 | 22 | let baseComponents = if let identifierType = genericArgument.as(IdentifierTypeSyntax.self) { 23 | [identifierType.name.text] 24 | } else if let tupleType = genericArgument.as(TupleTypeSyntax.self) { 25 | tupleType.argumentTypes 26 | } else { 27 | throw DiagnosticsError.init(diagnostics: []) 28 | } 29 | 30 | return [ 31 | try generateFillContext(of: baseComponents).cast(DeclSyntax.self), 32 | ] 33 | } 34 | 35 | private static func generateFillContext(of baseComponents: [String]) throws -> StructDeclSyntax { 36 | StructDeclSyntax( 37 | name: .identifier("_\(baseComponents.last ?? "")FillInContext"), 38 | memberBlock: try MemberBlockSyntax { 39 | try MemberBlockItemListSyntax { 40 | let convertComponents = allUnitNames.allSubarrays().filter({ !($0.count == 1 && $0.first == baseComponents.first ?? "") }) 41 | for subarray in convertComponents { 42 | if let property = try generateUnitProperty(baseComponents: baseComponents, to: subarray) { 43 | MemberBlockItemSyntax(decl: property) 44 | } 45 | } 46 | 47 | try MemberBlockItemSyntax( 48 | decl: VariableDeclSyntax("private let base: Components") 49 | ) 50 | 51 | try MemberBlockItemSyntax( 52 | decl: InitializerDeclSyntax("init(base: Components)") { 53 | """ 54 | self.base = base 55 | \(raw: convertComponents.compactMap { generateInitializerCodeBlockList(baseComponents: baseComponents, to: $0) }.joined(separator: "\n")) 56 | """ 57 | } 58 | ) 59 | } 60 | } 61 | ) 62 | } 63 | 64 | private static func generateNecessaryArgumentTypes( 65 | baseComponents: [String], 66 | to convertComponents: [String] 67 | ) -> [String] { 68 | var convertComponents = convertComponents 69 | convertComponents.removeAll { baseComponents.contains($0) } 70 | return convertComponents 71 | } 72 | 73 | private static func generateInitializerCodeBlockList( 74 | baseComponents: [String], 75 | to convertComponents: [String] 76 | ) -> String? { 77 | let bases: String = baseComponents.count == 1 ? "base" : baseComponents.enumerated().map { i, _ in "base.\(i)" }.joined(separator: ", ") 78 | let diff = convertComponents.count - baseComponents.count 79 | 80 | guard diff >= 1 else { 81 | return nil 82 | } 83 | 84 | let passedArguments: String = diff == 1 ? "data" : (0.. VariableDeclSyntax? { 94 | let types = generateNecessaryArgumentTypes(baseComponents: baseComponents, to: convertComponents) 95 | guard types.count >= 1 else { 96 | return nil 97 | } 98 | return try VariableDeclSyntax( 99 | """ 100 | public let \(raw: convertComponents.last!.lowercased()): \(raw: types.closureParameter) -> \(raw: convertComponents.returnType) 101 | """ 102 | ) 103 | } 104 | } 105 | 106 | extension TupleTypeSyntax { 107 | var argumentTypes: [String] { 108 | elements.compactMap { $0.type.as(IdentifierTypeSyntax.self)?.name.text } 109 | } 110 | } 111 | 112 | extension [String] { 113 | var closureParameter: String { 114 | if indices.count == 1 { 115 | "(\(self[0]))" 116 | } else { 117 | "((\(joined(separator: ", "))))" 118 | } 119 | } 120 | 121 | var returnType: String { 122 | if indices.count == 1 { 123 | self[0] 124 | } else { 125 | "(\(self.joined(separator: ", ")))" 126 | } 127 | } 128 | } 129 | 130 | extension Array { 131 | func allSubarrays() -> [[Element]] { 132 | var result: [[Element]] = [] 133 | for end in 1...self.count { 134 | result.append(Array(self.prefix(end))) 135 | } 136 | return result 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Sources/TypedDateMacros/MacroPlugins.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftCompilerPlugin 3 | import SwiftSyntaxMacros 4 | 5 | @main 6 | struct TypedDatePlugin: CompilerPlugin { 7 | let providingMacros: [Macro.Type] = [ 8 | FillInContextMacro.self, 9 | EraseContextMacro.self, 10 | ModifyContextMacro.self 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /Sources/TypedDateMacros/ModifyContextMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftCompilerPlugin 2 | import SwiftSyntax 3 | import SwiftSyntaxBuilder 4 | import SwiftSyntaxMacros 5 | import SwiftDiagnostics 6 | import TypedDateCore 7 | import Foundation 8 | 9 | // Note: This macro does not generate a detailed DiagnosticError because it is only used in TypedDate package. 10 | package struct ModifyContextMacro: MemberMacro { 11 | package static func expansion( 12 | of node: SwiftSyntax.AttributeSyntax, 13 | providingMembersOf declaration: some SwiftSyntax.DeclGroupSyntax, 14 | in context: some SwiftSyntaxMacros.MacroExpansionContext 15 | ) throws -> [SwiftSyntax.DeclSyntax] { 16 | guard let extensionDecl = declaration.as(ExtensionDeclSyntax.self), 17 | let genericArgument = extensionDecl.extendedType.as(IdentifierTypeSyntax.self)?.genericArgumentClause?.arguments.first?.argument 18 | else { 19 | throw DiagnosticsError.init(diagnostics: []) 20 | } 21 | 22 | let baseComponents = if let identifierType = genericArgument.as(IdentifierTypeSyntax.self) { 23 | [identifierType.name.text] 24 | } else if let tupleType = genericArgument.as(TupleTypeSyntax.self) { 25 | tupleType.argumentTypes 26 | } else { 27 | throw DiagnosticsError.init(diagnostics: []) 28 | } 29 | 30 | return [ 31 | try generateModifyContext(of: baseComponents).cast(DeclSyntax.self), 32 | ] 33 | } 34 | 35 | private static func generateModifyContext(of baseComponents: [String]) throws -> StructDeclSyntax { 36 | StructDeclSyntax( 37 | name: .identifier("_\(baseComponents.last ?? "_")ModifyContext"), 38 | memberBlock: try MemberBlockSyntax { 39 | try MemberBlockItemListSyntax { 40 | try MemberBlockItemSyntax( 41 | decl: VariableDeclSyntax("private let base: Components") 42 | ) 43 | for component in baseComponents { 44 | let typeName = component.lowercased() 45 | MemberBlockItemSyntax( 46 | decl: DeclSyntax("public let \(raw: typeName): (\(raw: component), (\(raw: component)) -> Components)") 47 | ) 48 | } 49 | try MemberBlockItemSyntax( 50 | decl: InitializerDeclSyntax("init(base: Components)") { 51 | """ 52 | self.base = base 53 | \(raw: generateInitializerCodeBlockList(of: baseComponents)) 54 | """ 55 | } 56 | ) 57 | } 58 | } 59 | ) 60 | } 61 | 62 | private static func generateInitializerCodeBlockList( 63 | of baseComponents: [String] 64 | ) -> String { 65 | var codeBlock = [String]() 66 | for (rowIndex, component) in baseComponents.enumerated() { 67 | let typeName = component.lowercased() 68 | let argument = baseComponents.count == 1 ? "base" : "base.\(rowIndex)" 69 | let closureReturnValue = baseComponents 70 | .enumerated() 71 | .map { closureIndex, closureComponent in 72 | if rowIndex == closureIndex { 73 | typeName 74 | } else { 75 | "base.\(closureIndex)" 76 | } 77 | } 78 | .joined(separator: ", ") 79 | codeBlock.append( 80 | """ 81 | self.\(typeName) = (\(argument), { \(typeName) in (\(closureReturnValue)) }) 82 | """ 83 | ) 84 | } 85 | return codeBlock.joined(separator: "\n") 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Tests/TypedDateMacrosTests/EraseContextMacroTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | import MacroTesting 4 | import TypedDateCore 5 | @testable import TypedDateMacros 6 | 7 | final class EraseContextMacroTests: XCTestCase { 8 | override func invokeTest() { 9 | withMacroTesting(macros: ["EraseContext": EraseContextMacro.self]) { 10 | super.invokeTest() 11 | } 12 | } 13 | 14 | func testNanosecondExpansion() { 15 | assertMacro { 16 | """ 17 | @EraseContext 18 | public extension TypedDate<(Year, Month, Day, Hour, Minute, Second, Nanosecond)> {} 19 | """ 20 | } expansion: { 21 | """ 22 | public extension TypedDate<(Year, Month, Day, Hour, Minute, Second, Nanosecond)> { 23 | 24 | struct _NanosecondEraseContext { 25 | private let base: Components 26 | public let year: (TypedDate.Type, Year) 27 | public let month: (TypedDate<(Year, Month)>.Type, (Year, Month)) 28 | public let day: (TypedDate<(Year, Month, Day)>.Type, (Year, Month, Day)) 29 | public let hour: (TypedDate<(Year, Month, Day, Hour)>.Type, (Year, Month, Day, Hour)) 30 | public let minute: (TypedDate<(Year, Month, Day, Hour, Minute)>.Type, (Year, Month, Day, Hour, Minute)) 31 | public let second: (TypedDate<(Year, Month, Day, Hour, Minute, Second)>.Type, (Year, Month, Day, Hour, Minute, Second)) 32 | init(base: Components) { 33 | self.base = base 34 | year = (TypedDate.self, base.0) 35 | month = (TypedDate<(Year, Month)>.self, (base.0, base.1)) 36 | day = (TypedDate<(Year, Month, Day)>.self, (base.0, base.1, base.2)) 37 | hour = (TypedDate<(Year, Month, Day, Hour)>.self, (base.0, base.1, base.2, base.3)) 38 | minute = (TypedDate<(Year, Month, Day, Hour, Minute)>.self, (base.0, base.1, base.2, base.3, base.4)) 39 | second = (TypedDate<(Year, Month, Day, Hour, Minute, Second)>.self, (base.0, base.1, base.2, base.3, base.4, base.5)) 40 | } 41 | }} 42 | """ 43 | } 44 | } 45 | 46 | func testSecondExpansion() { 47 | assertMacro { 48 | """ 49 | @EraseContext 50 | public extension TypedDate<(Year, Month, Day, Hour, Minute, Second)> {} 51 | """ 52 | } expansion: { 53 | """ 54 | public extension TypedDate<(Year, Month, Day, Hour, Minute, Second)> { 55 | 56 | struct _SecondEraseContext { 57 | private let base: Components 58 | public let year: (TypedDate.Type, Year) 59 | public let month: (TypedDate<(Year, Month)>.Type, (Year, Month)) 60 | public let day: (TypedDate<(Year, Month, Day)>.Type, (Year, Month, Day)) 61 | public let hour: (TypedDate<(Year, Month, Day, Hour)>.Type, (Year, Month, Day, Hour)) 62 | public let minute: (TypedDate<(Year, Month, Day, Hour, Minute)>.Type, (Year, Month, Day, Hour, Minute)) 63 | init(base: Components) { 64 | self.base = base 65 | year = (TypedDate.self, base.0) 66 | month = (TypedDate<(Year, Month)>.self, (base.0, base.1)) 67 | day = (TypedDate<(Year, Month, Day)>.self, (base.0, base.1, base.2)) 68 | hour = (TypedDate<(Year, Month, Day, Hour)>.self, (base.0, base.1, base.2, base.3)) 69 | minute = (TypedDate<(Year, Month, Day, Hour, Minute)>.self, (base.0, base.1, base.2, base.3, base.4)) 70 | } 71 | }} 72 | """ 73 | } 74 | } 75 | 76 | func testMinuteExpansion() { 77 | assertMacro { 78 | """ 79 | @EraseContext 80 | public extension TypedDate<(Year, Month, Day, Hour, Minute)> {} 81 | """ 82 | } expansion: { 83 | """ 84 | public extension TypedDate<(Year, Month, Day, Hour, Minute)> { 85 | 86 | struct _MinuteEraseContext { 87 | private let base: Components 88 | public let year: (TypedDate.Type, Year) 89 | public let month: (TypedDate<(Year, Month)>.Type, (Year, Month)) 90 | public let day: (TypedDate<(Year, Month, Day)>.Type, (Year, Month, Day)) 91 | public let hour: (TypedDate<(Year, Month, Day, Hour)>.Type, (Year, Month, Day, Hour)) 92 | init(base: Components) { 93 | self.base = base 94 | year = (TypedDate.self, base.0) 95 | month = (TypedDate<(Year, Month)>.self, (base.0, base.1)) 96 | day = (TypedDate<(Year, Month, Day)>.self, (base.0, base.1, base.2)) 97 | hour = (TypedDate<(Year, Month, Day, Hour)>.self, (base.0, base.1, base.2, base.3)) 98 | } 99 | }} 100 | """ 101 | } 102 | } 103 | func testHourExpansion() { 104 | assertMacro { 105 | """ 106 | @EraseContext 107 | public extension TypedDate<(Year, Month, Day, Hour)> {} 108 | """ 109 | } expansion: { 110 | """ 111 | public extension TypedDate<(Year, Month, Day, Hour)> { 112 | 113 | struct _HourEraseContext { 114 | private let base: Components 115 | public let year: (TypedDate.Type, Year) 116 | public let month: (TypedDate<(Year, Month)>.Type, (Year, Month)) 117 | public let day: (TypedDate<(Year, Month, Day)>.Type, (Year, Month, Day)) 118 | init(base: Components) { 119 | self.base = base 120 | year = (TypedDate.self, base.0) 121 | month = (TypedDate<(Year, Month)>.self, (base.0, base.1)) 122 | day = (TypedDate<(Year, Month, Day)>.self, (base.0, base.1, base.2)) 123 | } 124 | }} 125 | """ 126 | } 127 | } 128 | func testDayExpansion() { 129 | assertMacro { 130 | """ 131 | @EraseContext 132 | public extension TypedDate<(Year, Month, Day)> {} 133 | """ 134 | } expansion: { 135 | """ 136 | public extension TypedDate<(Year, Month, Day)> { 137 | 138 | struct _DayEraseContext { 139 | private let base: Components 140 | public let year: (TypedDate.Type, Year) 141 | public let month: (TypedDate<(Year, Month)>.Type, (Year, Month)) 142 | init(base: Components) { 143 | self.base = base 144 | year = (TypedDate.self, base.0) 145 | month = (TypedDate<(Year, Month)>.self, (base.0, base.1)) 146 | } 147 | }} 148 | """ 149 | } 150 | } 151 | func testMonthExpansion() { 152 | assertMacro { 153 | """ 154 | @EraseContext 155 | public extension TypedDate<(Year, Month)> {} 156 | """ 157 | } expansion: { 158 | """ 159 | public extension TypedDate<(Year, Month)> { 160 | 161 | struct _MonthEraseContext { 162 | private let base: Components 163 | public let year: (TypedDate.Type, Year) 164 | init(base: Components) { 165 | self.base = base 166 | year = (TypedDate.self, base.0) 167 | } 168 | }} 169 | """ 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /Tests/TypedDateMacrosTests/FillInContextMacroTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | import MacroTesting 4 | import TypedDateCore 5 | @testable import TypedDateMacros 6 | 7 | final class FillInContextMacroTests: XCTestCase { 8 | override func invokeTest() { 9 | withMacroTesting(macros: ["FillInContext": FillInContextMacro.self]) { 10 | super.invokeTest() 11 | } 12 | } 13 | 14 | func testSecondExpansion() { 15 | assertMacro { 16 | """ 17 | @FillInContext 18 | public extension TypedDate<(Year, Month, Day, Hour, Minute, Second)> {} 19 | """ 20 | } expansion: { 21 | """ 22 | public extension TypedDate<(Year, Month, Day, Hour, Minute, Second)> { 23 | 24 | struct _SecondFillInContext { 25 | public let nanosecond: (Nanosecond) -> (Year, Month, Day, Hour, Minute, Second, Nanosecond) 26 | private let base: Components 27 | init(base: Components) { 28 | self.base = base 29 | nanosecond = { data in (base.0, base.1, base.2, base.3, base.4, base.5, data) } 30 | } 31 | }} 32 | """ 33 | } 34 | } 35 | 36 | func testMinuteExpansion() { 37 | assertMacro { 38 | """ 39 | @FillInContext 40 | public extension TypedDate<(Year, Month, Day, Hour, Minute)> {} 41 | """ 42 | } expansion: { 43 | """ 44 | public extension TypedDate<(Year, Month, Day, Hour, Minute)> { 45 | 46 | struct _MinuteFillInContext { 47 | public let second: (Second) -> (Year, Month, Day, Hour, Minute, Second) 48 | public let nanosecond: ((Second, Nanosecond)) -> (Year, Month, Day, Hour, Minute, Second, Nanosecond) 49 | private let base: Components 50 | init(base: Components) { 51 | self.base = base 52 | second = { data in (base.0, base.1, base.2, base.3, base.4, data) } 53 | nanosecond = { data in (base.0, base.1, base.2, base.3, base.4, data.0, data.1) } 54 | } 55 | }} 56 | """ 57 | } 58 | } 59 | func testHourExpansion() { 60 | assertMacro { 61 | """ 62 | @FillInContext 63 | public extension TypedDate<(Year, Month, Day, Hour)> {} 64 | """ 65 | } expansion: { 66 | """ 67 | public extension TypedDate<(Year, Month, Day, Hour)> { 68 | 69 | struct _HourFillInContext { 70 | public let minute: (Minute) -> (Year, Month, Day, Hour, Minute) 71 | public let second: ((Minute, Second)) -> (Year, Month, Day, Hour, Minute, Second) 72 | public let nanosecond: ((Minute, Second, Nanosecond)) -> (Year, Month, Day, Hour, Minute, Second, Nanosecond) 73 | private let base: Components 74 | init(base: Components) { 75 | self.base = base 76 | minute = { data in (base.0, base.1, base.2, base.3, data) } 77 | second = { data in (base.0, base.1, base.2, base.3, data.0, data.1) } 78 | nanosecond = { data in (base.0, base.1, base.2, base.3, data.0, data.1, data.2) } 79 | } 80 | }} 81 | """ 82 | } 83 | } 84 | func testDayExpansion() { 85 | assertMacro { 86 | """ 87 | @FillInContext 88 | public extension TypedDate<(Year, Month, Day)> {} 89 | """ 90 | } expansion: { 91 | """ 92 | public extension TypedDate<(Year, Month, Day)> { 93 | 94 | struct _DayFillInContext { 95 | public let hour: (Hour) -> (Year, Month, Day, Hour) 96 | public let minute: ((Hour, Minute)) -> (Year, Month, Day, Hour, Minute) 97 | public let second: ((Hour, Minute, Second)) -> (Year, Month, Day, Hour, Minute, Second) 98 | public let nanosecond: ((Hour, Minute, Second, Nanosecond)) -> (Year, Month, Day, Hour, Minute, Second, Nanosecond) 99 | private let base: Components 100 | init(base: Components) { 101 | self.base = base 102 | hour = { data in (base.0, base.1, base.2, data) } 103 | minute = { data in (base.0, base.1, base.2, data.0, data.1) } 104 | second = { data in (base.0, base.1, base.2, data.0, data.1, data.2) } 105 | nanosecond = { data in (base.0, base.1, base.2, data.0, data.1, data.2, data.3) } 106 | } 107 | }} 108 | """ 109 | } 110 | } 111 | func testMonthExpansion() { 112 | assertMacro { 113 | """ 114 | @FillInContext 115 | public extension TypedDate<(Year, Month)> {} 116 | """ 117 | } expansion: { 118 | """ 119 | public extension TypedDate<(Year, Month)> { 120 | 121 | struct _MonthFillInContext { 122 | public let day: (Day) -> (Year, Month, Day) 123 | public let hour: ((Day, Hour)) -> (Year, Month, Day, Hour) 124 | public let minute: ((Day, Hour, Minute)) -> (Year, Month, Day, Hour, Minute) 125 | public let second: ((Day, Hour, Minute, Second)) -> (Year, Month, Day, Hour, Minute, Second) 126 | public let nanosecond: ((Day, Hour, Minute, Second, Nanosecond)) -> (Year, Month, Day, Hour, Minute, Second, Nanosecond) 127 | private let base: Components 128 | init(base: Components) { 129 | self.base = base 130 | day = { data in (base.0, base.1, data) } 131 | hour = { data in (base.0, base.1, data.0, data.1) } 132 | minute = { data in (base.0, base.1, data.0, data.1, data.2) } 133 | second = { data in (base.0, base.1, data.0, data.1, data.2, data.3) } 134 | nanosecond = { data in (base.0, base.1, data.0, data.1, data.2, data.3, data.4) } 135 | } 136 | }} 137 | """ 138 | } 139 | } 140 | 141 | func testYearExpansion() { 142 | assertMacro { 143 | """ 144 | @FillInContext 145 | public extension TypedDate {} 146 | """ 147 | } expansion: { 148 | """ 149 | public extension TypedDate { 150 | 151 | struct _YearFillInContext { 152 | public let month: (Month) -> (Year, Month) 153 | public let day: ((Month, Day)) -> (Year, Month, Day) 154 | public let hour: ((Month, Day, Hour)) -> (Year, Month, Day, Hour) 155 | public let minute: ((Month, Day, Hour, Minute)) -> (Year, Month, Day, Hour, Minute) 156 | public let second: ((Month, Day, Hour, Minute, Second)) -> (Year, Month, Day, Hour, Minute, Second) 157 | public let nanosecond: ((Month, Day, Hour, Minute, Second, Nanosecond)) -> (Year, Month, Day, Hour, Minute, Second, Nanosecond) 158 | private let base: Components 159 | init(base: Components) { 160 | self.base = base 161 | month = { data in (base, data) } 162 | day = { data in (base, data.0, data.1) } 163 | hour = { data in (base, data.0, data.1, data.2) } 164 | minute = { data in (base, data.0, data.1, data.2, data.3) } 165 | second = { data in (base, data.0, data.1, data.2, data.3, data.4) } 166 | nanosecond = { data in (base, data.0, data.1, data.2, data.3, data.4, data.5) } 167 | } 168 | }} 169 | """ 170 | } 171 | } 172 | } 173 | 174 | -------------------------------------------------------------------------------- /Tests/TypedDateMacrosTests/ModifyContextMacroTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | import MacroTesting 4 | import TypedDateCore 5 | @testable import TypedDateMacros 6 | 7 | final class ModifyContextMacroTests: XCTestCase { 8 | override func invokeTest() { 9 | withMacroTesting(macros: ["ModifyContext": ModifyContextMacro.self]) { 10 | super.invokeTest() 11 | } 12 | } 13 | 14 | func testNanosecondExpansion() { 15 | assertMacro { 16 | """ 17 | @ModifyContext 18 | public extension TypedDate<(Year, Month, Day, Hour, Minute, Second, Nanosecond)> {} 19 | """ 20 | } expansion: { 21 | """ 22 | public extension TypedDate<(Year, Month, Day, Hour, Minute, Second, Nanosecond)> { 23 | 24 | struct _NanosecondModifyContext { 25 | private let base: Components 26 | public let year: (Year, (Year) -> Components) 27 | public let month: (Month, (Month) -> Components) 28 | public let day: (Day, (Day) -> Components) 29 | public let hour: (Hour, (Hour) -> Components) 30 | public let minute: (Minute, (Minute) -> Components) 31 | public let second: (Second, (Second) -> Components) 32 | public let nanosecond: (Nanosecond, (Nanosecond) -> Components) 33 | init(base: Components) { 34 | self.base = base 35 | self.year = (base.0, { year in (year, base.1, base.2, base.3, base.4, base.5, base.6) }) 36 | self.month = (base.1, { month in (base.0, month, base.2, base.3, base.4, base.5, base.6) }) 37 | self.day = (base.2, { day in (base.0, base.1, day, base.3, base.4, base.5, base.6) }) 38 | self.hour = (base.3, { hour in (base.0, base.1, base.2, hour, base.4, base.5, base.6) }) 39 | self.minute = (base.4, { minute in (base.0, base.1, base.2, base.3, minute, base.5, base.6) }) 40 | self.second = (base.5, { second in (base.0, base.1, base.2, base.3, base.4, second, base.6) }) 41 | self.nanosecond = (base.6, { nanosecond in (base.0, base.1, base.2, base.3, base.4, base.5, nanosecond) }) 42 | } 43 | }} 44 | """ 45 | } 46 | } 47 | 48 | func testSecondExpansion() { 49 | assertMacro { 50 | """ 51 | @ModifyContext 52 | public extension TypedDate<(Year, Month, Day, Hour, Minute, Second)> {} 53 | """ 54 | } expansion: { 55 | """ 56 | public extension TypedDate<(Year, Month, Day, Hour, Minute, Second)> { 57 | 58 | struct _SecondModifyContext { 59 | private let base: Components 60 | public let year: (Year, (Year) -> Components) 61 | public let month: (Month, (Month) -> Components) 62 | public let day: (Day, (Day) -> Components) 63 | public let hour: (Hour, (Hour) -> Components) 64 | public let minute: (Minute, (Minute) -> Components) 65 | public let second: (Second, (Second) -> Components) 66 | init(base: Components) { 67 | self.base = base 68 | self.year = (base.0, { year in (year, base.1, base.2, base.3, base.4, base.5) }) 69 | self.month = (base.1, { month in (base.0, month, base.2, base.3, base.4, base.5) }) 70 | self.day = (base.2, { day in (base.0, base.1, day, base.3, base.4, base.5) }) 71 | self.hour = (base.3, { hour in (base.0, base.1, base.2, hour, base.4, base.5) }) 72 | self.minute = (base.4, { minute in (base.0, base.1, base.2, base.3, minute, base.5) }) 73 | self.second = (base.5, { second in (base.0, base.1, base.2, base.3, base.4, second) }) 74 | } 75 | }} 76 | """ 77 | } 78 | } 79 | 80 | func testMinuteExpansion() { 81 | assertMacro { 82 | """ 83 | @ModifyContext 84 | public extension TypedDate<(Year, Month, Day, Hour, Minute)> {} 85 | """ 86 | } expansion: { 87 | """ 88 | public extension TypedDate<(Year, Month, Day, Hour, Minute)> { 89 | 90 | struct _MinuteModifyContext { 91 | private let base: Components 92 | public let year: (Year, (Year) -> Components) 93 | public let month: (Month, (Month) -> Components) 94 | public let day: (Day, (Day) -> Components) 95 | public let hour: (Hour, (Hour) -> Components) 96 | public let minute: (Minute, (Minute) -> Components) 97 | init(base: Components) { 98 | self.base = base 99 | self.year = (base.0, { year in (year, base.1, base.2, base.3, base.4) }) 100 | self.month = (base.1, { month in (base.0, month, base.2, base.3, base.4) }) 101 | self.day = (base.2, { day in (base.0, base.1, day, base.3, base.4) }) 102 | self.hour = (base.3, { hour in (base.0, base.1, base.2, hour, base.4) }) 103 | self.minute = (base.4, { minute in (base.0, base.1, base.2, base.3, minute) }) 104 | } 105 | }} 106 | """ 107 | } 108 | } 109 | 110 | func testHourExpansion() { 111 | assertMacro { 112 | """ 113 | @ModifyContext 114 | public extension TypedDate<(Year, Month, Day, Hour)> {} 115 | """ 116 | } expansion: { 117 | """ 118 | public extension TypedDate<(Year, Month, Day, Hour)> { 119 | 120 | struct _HourModifyContext { 121 | private let base: Components 122 | public let year: (Year, (Year) -> Components) 123 | public let month: (Month, (Month) -> Components) 124 | public let day: (Day, (Day) -> Components) 125 | public let hour: (Hour, (Hour) -> Components) 126 | init(base: Components) { 127 | self.base = base 128 | self.year = (base.0, { year in (year, base.1, base.2, base.3) }) 129 | self.month = (base.1, { month in (base.0, month, base.2, base.3) }) 130 | self.day = (base.2, { day in (base.0, base.1, day, base.3) }) 131 | self.hour = (base.3, { hour in (base.0, base.1, base.2, hour) }) 132 | } 133 | }} 134 | """ 135 | } 136 | } 137 | 138 | func testDayExpansion() { 139 | assertMacro { 140 | """ 141 | @ModifyContext 142 | public extension TypedDate<(Year, Month, Day)> {} 143 | """ 144 | } expansion: { 145 | """ 146 | public extension TypedDate<(Year, Month, Day)> { 147 | 148 | struct _DayModifyContext { 149 | private let base: Components 150 | public let year: (Year, (Year) -> Components) 151 | public let month: (Month, (Month) -> Components) 152 | public let day: (Day, (Day) -> Components) 153 | init(base: Components) { 154 | self.base = base 155 | self.year = (base.0, { year in (year, base.1, base.2) }) 156 | self.month = (base.1, { month in (base.0, month, base.2) }) 157 | self.day = (base.2, { day in (base.0, base.1, day) }) 158 | } 159 | }} 160 | """ 161 | } 162 | } 163 | 164 | func testMonthExpansion() { 165 | assertMacro { 166 | """ 167 | @ModifyContext 168 | public extension TypedDate<(Year, Month)> {} 169 | """ 170 | } expansion: { 171 | """ 172 | public extension TypedDate<(Year, Month)> { 173 | 174 | struct _MonthModifyContext { 175 | private let base: Components 176 | public let year: (Year, (Year) -> Components) 177 | public let month: (Month, (Month) -> Components) 178 | init(base: Components) { 179 | self.base = base 180 | self.year = (base.0, { year in (year, base.1) }) 181 | self.month = (base.1, { month in (base.0, month) }) 182 | } 183 | }} 184 | """ 185 | } 186 | } 187 | 188 | func testYearExpansion() { 189 | assertMacro { 190 | """ 191 | @ModifyContext 192 | public extension TypedDate<(Year)> {} 193 | """ 194 | } expansion: { 195 | """ 196 | public extension TypedDate<(Year)> { 197 | 198 | struct _YearModifyContext { 199 | private let base: Components 200 | public let year: (Year, (Year) -> Components) 201 | init(base: Components) { 202 | self.base = base 203 | self.year = (base, { year in (year) }) 204 | } 205 | }} 206 | """ 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /Tests/TypedDateTests/Scaffolding.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | import XCTest 3 | 4 | final class AllTests: XCTestCase { 5 | func testAll() async { 6 | await XCTestScaffold.runAllTests(hostedBy: self) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Tests/TypedDateTests/TypedDateAddTests.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | @testable import TypedDate 3 | import TypedDateCore 4 | import Foundation 5 | 6 | @Suite 7 | struct TypedDateAddTests { 8 | let calendar: Calendar 9 | let testSupport: TypedDateTestSupport 10 | 11 | init() { 12 | calendar = Calendar(identifier: .gregorian) 13 | testSupport = TypedDateTestSupport.init(calendar: calendar) 14 | } 15 | 16 | @Test 17 | func addYear() { 18 | let typedDate = testSupport.generateTypedYearDate().add(\.year, 1) 19 | 20 | #expect(typedDate.year(calendar: calendar) == testSupport.year.add(1)) 21 | } 22 | 23 | @Test 24 | func addMonth() { 25 | let typedDate = testSupport.generateTypedMonthDate().add(\.month, 1) 26 | #expect(typedDate.month(calendar: calendar) == testSupport.month.add(1)) 27 | } 28 | 29 | @Test 30 | func addHour() { 31 | let typedDate = testSupport.generateTypedHourDate().add(\.hour, 1) 32 | #expect(typedDate.hour(calendar: calendar) == testSupport.hour.add(1)) 33 | } 34 | 35 | @Test 36 | func addMinute() { 37 | let typedDate = testSupport.generateTypedMinuteDate().add(\.minute, 1) 38 | #expect(typedDate.minute(calendar: calendar) == testSupport.minute.add(1)) 39 | } 40 | 41 | @Test 42 | func addSecond() { 43 | let typedDate = testSupport.generateTypedSecondDate().add(\.second, 1) 44 | #expect(typedDate.second(calendar: calendar) == testSupport.second.add(1)) 45 | } 46 | 47 | @Test 48 | func subtractSecond() { 49 | let typedDate = testSupport.generateTypedSecondDate() 50 | let yesterdayTypedDate = typedDate.add(\.second, Second(-(60 * 60 * 24))) 51 | #expect(typedDate.erase(to: \.day) != yesterdayTypedDate.erase(to: \.day)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Tests/TypedDateTests/TypedDateCodableTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Testing 3 | @testable import TypedDate 4 | 5 | @Suite 6 | struct TypedDateCodableTests { 7 | let calendar: Calendar 8 | let testSupport: TypedDateTestSupport 9 | 10 | init() { 11 | calendar = Calendar(identifier: .gregorian) 12 | testSupport = TypedDateTestSupport.init(calendar: calendar) 13 | } 14 | 15 | @Test 16 | func testCodable() throws { 17 | let typedDate = testSupport.generateTypedNanosecondDate() 18 | let jsonEncoder = JSONEncoder() 19 | let data = try jsonEncoder.encode(typedDate) 20 | let decoded1 = try JSONDecoder().decode(TypedDate<(Year, Month, Day, Hour, Minute, Second, Nanosecond)>.self, from: data) 21 | testSupport.assertTypedDate(for: decoded1) 22 | let decoded2 = try JSONDecoder().decode(TypedDate<(Year, Month, Day, Hour, Minute, Second)>.self, from: data) 23 | testSupport.assertTypedDate(for: decoded2) 24 | let decoded3 = try JSONDecoder().decode(TypedDate<(Year, Month, Day, Hour, Minute)>.self, from: data) 25 | testSupport.assertTypedDate(for: decoded3) 26 | let decoded4 = try JSONDecoder().decode(TypedDate<(Year, Month, Day, Hour)>.self, from: data) 27 | testSupport.assertTypedDate(for: decoded4) 28 | let decoded5 = try JSONDecoder().decode(TypedDate<(Year, Month, Day)>.self, from: data) 29 | testSupport.assertTypedDate(for: decoded5) 30 | let decoded6 = try JSONDecoder().decode(TypedDate<(Year, Month)>.self, from: data) 31 | testSupport.assertTypedDate(for: decoded6) 32 | let decoded7 = try JSONDecoder().decode(TypedDate<(Year, Month)>.self, from: data) 33 | testSupport.assertTypedDate(for: decoded7) 34 | let decoded8 = try JSONDecoder().decode(TypedDate<(Year)>.self, from: data) 35 | testSupport.assertTypedDate(for: decoded8) 36 | } 37 | 38 | @Test 39 | func testAnotherModelCodable() throws { 40 | struct Model: Codable, Equatable { 41 | let id: Int 42 | let name: String 43 | let date: TypedDate<(Year, Month, Day)> 44 | } 45 | let typedDate = TypedDate(Year(2023), Month(11), Day(10)) 46 | let model = Model(id: 1, name: "John", date: typedDate) 47 | 48 | let data = try JSONEncoder().encode(model) 49 | let decodedModel = try JSONDecoder().decode(Model.self, from: data) 50 | #expect(model == decodedModel) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Tests/TypedDateTests/TypedDateComparableTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Testing 3 | @testable import TypedDate 4 | 5 | @Suite 6 | struct TypedDateComparableTests { 7 | let calendar: Calendar 8 | let testSupport: TypedDateTestSupport 9 | 10 | init() { 11 | calendar = Calendar(identifier: .gregorian) 12 | testSupport = TypedDateTestSupport.init(calendar: calendar) 13 | } 14 | 15 | @Test 16 | func compareYear() { 17 | let lhsDate = TypedDate(testSupport.year, calendar: calendar) 18 | let rhsDate = TypedDate(testSupport.year.add(1), calendar: calendar) 19 | 20 | #expect(lhsDate < rhsDate) 21 | } 22 | 23 | @Test 24 | func compareMonth() { 25 | let lhsDate = TypedDate(testSupport.year, testSupport.month, calendar: calendar) 26 | let rhsDate = TypedDate(testSupport.year, testSupport.month.add(1), calendar: calendar) 27 | 28 | #expect(lhsDate < rhsDate) 29 | } 30 | 31 | @Test 32 | func compareDay() { 33 | let lhsDate = TypedDate(testSupport.year, testSupport.month, testSupport.day, calendar: calendar) 34 | let rhsDate = TypedDate(testSupport.year, testSupport.month, testSupport.day.add(1), calendar: calendar) 35 | 36 | #expect(lhsDate < rhsDate) 37 | } 38 | 39 | @Test 40 | func compareHour() { 41 | let lhsDate = TypedDate(testSupport.year, testSupport.month, testSupport.day, testSupport.hour, calendar: calendar) 42 | let rhsDate = TypedDate(testSupport.year, testSupport.month, testSupport.day, testSupport.hour.add(1), calendar: calendar) 43 | 44 | #expect(lhsDate < rhsDate) 45 | } 46 | 47 | @Test 48 | func compareMinute() { 49 | let lhsDate = TypedDate(testSupport.year, testSupport.month, testSupport.day, testSupport.hour, testSupport.minute, calendar: calendar) 50 | let rhsDate = TypedDate(testSupport.year, testSupport.month, testSupport.day, testSupport.hour, testSupport.minute.add(1), calendar: calendar) 51 | 52 | #expect(lhsDate < rhsDate) 53 | } 54 | 55 | @Test 56 | func compareSecond() { 57 | let lhsDate = TypedDate(testSupport.year, testSupport.month, testSupport.day, testSupport.hour, testSupport.minute, testSupport.second, calendar: calendar) 58 | let rhsDate = TypedDate(testSupport.year, testSupport.month, testSupport.day, testSupport.hour, testSupport.minute, testSupport.second.add(1), calendar: calendar) 59 | 60 | #expect(lhsDate < rhsDate) 61 | } 62 | 63 | @Test 64 | func compareNanosecond() { 65 | let lhsDate = TypedDate(testSupport.year, testSupport.month, testSupport.day, testSupport.hour, testSupport.minute, testSupport.second, testSupport.nanosecond, calendar: calendar) 66 | let rhsDate = TypedDate(testSupport.year, testSupport.month, testSupport.day, testSupport.hour, testSupport.minute, testSupport.second, testSupport.nanosecond.add(-10000), calendar: calendar) 67 | 68 | #expect(lhsDate > rhsDate) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Tests/TypedDateTests/TypedDateComponentsTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Testing 3 | @testable import TypedDate 4 | 5 | @Suite 6 | struct TypedDateComponentsTests { 7 | let calendar: Calendar 8 | let testSupport: TypedDateTestSupport 9 | 10 | init() { 11 | calendar = Calendar(identifier: .gregorian) 12 | testSupport = TypedDateTestSupport(calendar: calendar) 13 | } 14 | 15 | @Test 16 | func year() { 17 | let typedDate = testSupport.generateTypedYearDate() 18 | #expect(typedDate.year(calendar: calendar) == testSupport.year.value) 19 | } 20 | 21 | @Test 22 | func month() { 23 | let typedDate = testSupport.generateTypedMonthDate() 24 | #expect(typedDate.year(calendar: calendar) == testSupport.year.value) 25 | #expect(typedDate.month(calendar: calendar) == testSupport.month.value) 26 | } 27 | 28 | @Test 29 | func day() { 30 | let typedDate = testSupport.generateTypedDayDate() 31 | #expect(typedDate.year(calendar: calendar) == testSupport.year.value) 32 | #expect(typedDate.month(calendar: calendar) == testSupport.month.value) 33 | #expect(typedDate.day(calendar: calendar) == testSupport.day.value) 34 | } 35 | 36 | @Test 37 | func hour() { 38 | let typedDate = testSupport.generateTypedHourDate() 39 | #expect(typedDate.year(calendar: calendar) == testSupport.year.value) 40 | #expect(typedDate.month(calendar: calendar) == testSupport.month.value) 41 | #expect(typedDate.day(calendar: calendar) == testSupport.day.value) 42 | #expect(typedDate.hour(calendar: calendar) == testSupport.hour.value) 43 | } 44 | 45 | @Test 46 | func minute() { 47 | let typedDate = testSupport.generateTypedMinuteDate() 48 | #expect(typedDate.year(calendar: calendar) == testSupport.year.value) 49 | #expect(typedDate.month(calendar: calendar) == testSupport.month.value) 50 | #expect(typedDate.day(calendar: calendar) == testSupport.day.value) 51 | #expect(typedDate.hour(calendar: calendar) == testSupport.hour.value) 52 | #expect(typedDate.minute(calendar: calendar) == testSupport.minute.value) 53 | } 54 | 55 | @Test 56 | func second() { 57 | let typedDate = testSupport.generateTypedSecondDate() 58 | #expect(typedDate.year(calendar: calendar) == testSupport.year.value) 59 | #expect(typedDate.month(calendar: calendar) == testSupport.month.value) 60 | #expect(typedDate.day(calendar: calendar) == testSupport.day.value) 61 | #expect(typedDate.hour(calendar: calendar) == testSupport.hour.value) 62 | #expect(typedDate.minute(calendar: calendar) == testSupport.minute.value) 63 | #expect(typedDate.second(calendar: calendar) == testSupport.second.value) 64 | } 65 | 66 | @Test 67 | func nanosecond() { 68 | let typedDate = testSupport.generateTypedNanosecondDate() 69 | #expect(typedDate.year(calendar: calendar) == testSupport.year.value) 70 | #expect(typedDate.month(calendar: calendar) == testSupport.month.value) 71 | #expect(typedDate.day(calendar: calendar) == testSupport.day.value) 72 | #expect(typedDate.hour(calendar: calendar) == testSupport.hour.value) 73 | #expect(typedDate.minute(calendar: calendar) == testSupport.minute.value) 74 | #expect(typedDate.second(calendar: calendar) == testSupport.second.value) 75 | #expect(typedDate.nanosecond(calendar: calendar) == testSupport.nanosecond.value) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Tests/TypedDateTests/TypedDateEquatableTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Testing 3 | @testable import TypedDate 4 | 5 | @Suite 6 | struct TypedDateEquatableTests { 7 | let calendar: Calendar 8 | let testSupport: TypedDateTestSupport 9 | 10 | init() { 11 | calendar = Calendar(identifier: .gregorian) 12 | testSupport = TypedDateTestSupport.init(calendar: calendar) 13 | } 14 | 15 | @Test 16 | func equalYear() { 17 | let lhsDate = testSupport.generateTypedYearDate() 18 | let rhsDate = testSupport.generateTypedYearDate() 19 | 20 | #expect(lhsDate == rhsDate) 21 | } 22 | 23 | @Test 24 | func equalMonth() { 25 | let lhsDate = testSupport.generateTypedMonthDate() 26 | let rhsDate = testSupport.generateTypedMonthDate() 27 | 28 | #expect(lhsDate == rhsDate) 29 | } 30 | 31 | @Test 32 | func equalDay() { 33 | let lhsDate = testSupport.generateTypedDayDate() 34 | let rhsDate = testSupport.generateTypedDayDate() 35 | 36 | #expect(lhsDate == rhsDate) 37 | } 38 | 39 | @Test 40 | func equalHour() { 41 | let lhsDate = testSupport.generateTypedHourDate() 42 | let rhsDate = testSupport.generateTypedHourDate() 43 | 44 | #expect(lhsDate == rhsDate) 45 | } 46 | 47 | @Test 48 | func equalMinute() { 49 | let lhsDate = testSupport.generateTypedMinuteDate() 50 | let rhsDate = testSupport.generateTypedMinuteDate() 51 | 52 | #expect(lhsDate == rhsDate) 53 | } 54 | 55 | @Test 56 | func equalSecond() { 57 | let lhsDate = testSupport.generateTypedSecondDate() 58 | let rhsDate = testSupport.generateTypedSecondDate() 59 | 60 | #expect(lhsDate == rhsDate) 61 | } 62 | 63 | @Test 64 | func equalNanosecond() { 65 | let lhsDate = testSupport.generateTypedNanosecondDate() 66 | let rhsDate = testSupport.generateTypedNanosecondDate() 67 | 68 | #expect(lhsDate == rhsDate) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Tests/TypedDateTests/TypedDateEraseTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Testing 3 | @testable import TypedDate 4 | 5 | @Suite 6 | struct TypedDateEraseTests { 7 | let calendar: Calendar 8 | let testSupport: TypedDateTestSupport 9 | 10 | init() { 11 | calendar = Calendar(identifier: .gregorian) 12 | testSupport = TypedDateTestSupport(calendar: calendar) 13 | } 14 | 15 | @Test 16 | func testMonthErasedDate() { 17 | let typedDate = testSupport.generateTypedMonthDate() 18 | testSupport.assertTypedDate(for: typedDate) 19 | testSupport.assertTypedDate(for: typedDate.erase(to: \.year, calendar: calendar)) 20 | } 21 | 22 | @Test 23 | func testDayErasedDate() { 24 | let typedDate = testSupport.generateTypedDayDate() 25 | testSupport.assertTypedDate(for: typedDate) 26 | testSupport.assertTypedDate(for: typedDate.erase(to: \.year, calendar: calendar)) 27 | testSupport.assertTypedDate(for: typedDate.erase(to: \.month, calendar: calendar)) 28 | } 29 | 30 | @Test 31 | func testHourErasedDate() { 32 | let typedDate = testSupport.generateTypedHourDate() 33 | testSupport.assertTypedDate(for: typedDate) 34 | testSupport.assertTypedDate(for: typedDate.erase(to: \.year, calendar: calendar)) 35 | testSupport.assertTypedDate(for: typedDate.erase(to: \.month, calendar: calendar)) 36 | testSupport.assertTypedDate(for: typedDate.erase(to: \.day, calendar: calendar)) 37 | } 38 | 39 | @Test 40 | func testMinuteErasedDate() { 41 | let typedDate = testSupport.generateTypedMinuteDate() 42 | testSupport.assertTypedDate(for: typedDate) 43 | testSupport.assertTypedDate(for: typedDate.erase(to: \.year, calendar: calendar)) 44 | testSupport.assertTypedDate(for: typedDate.erase(to: \.month, calendar: calendar)) 45 | testSupport.assertTypedDate(for: typedDate.erase(to: \.day, calendar: calendar)) 46 | testSupport.assertTypedDate(for: typedDate.erase(to: \.hour, calendar: calendar)) 47 | } 48 | 49 | @Test 50 | func testSecondErasedDate() { 51 | let typedDate = testSupport.generateTypedNanosecondDate() 52 | testSupport.assertTypedDate(for: typedDate) 53 | testSupport.assertTypedDate(for: typedDate.erase(to: \.year, calendar: calendar)) 54 | testSupport.assertTypedDate(for: typedDate.erase(to: \.month, calendar: calendar)) 55 | testSupport.assertTypedDate(for: typedDate.erase(to: \.day, calendar: calendar)) 56 | testSupport.assertTypedDate(for: typedDate.erase(to: \.hour, calendar: calendar)) 57 | testSupport.assertTypedDate(for: typedDate.erase(to: \.minute, calendar: calendar)) 58 | } 59 | 60 | @Test 61 | func testNanoSecondErasedDate() { 62 | let typedDate = testSupport.generateTypedNanosecondDate() 63 | testSupport.assertTypedDate(for: typedDate) 64 | testSupport.assertTypedDate(for: typedDate.erase(to: \.year, calendar: calendar)) 65 | testSupport.assertTypedDate(for: typedDate.erase(to: \.month, calendar: calendar)) 66 | testSupport.assertTypedDate(for: typedDate.erase(to: \.day, calendar: calendar)) 67 | testSupport.assertTypedDate(for: typedDate.erase(to: \.hour, calendar: calendar)) 68 | testSupport.assertTypedDate(for: typedDate.erase(to: \.minute, calendar: calendar)) 69 | testSupport.assertTypedDate(for: typedDate.erase(to: \.second, calendar: calendar)) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Tests/TypedDateTests/TypedDateFillTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Testing 3 | @testable import TypedDate 4 | 5 | @Suite 6 | struct TypedDateFillTests { 7 | let calendar: Calendar 8 | let testSupport: TypedDateTestSupport 9 | 10 | init() { 11 | self.calendar = Calendar(identifier: .gregorian) 12 | self.testSupport = TypedDateTestSupport(calendar: calendar) 13 | } 14 | 15 | @Test 16 | func testFillInYearDate() { 17 | let typedDate = testSupport.generateTypedYearDate() 18 | testSupport.assertTypedDate( 19 | for: typedDate.fill( 20 | to: \.month, 21 | arguments: (testSupport.month) 22 | ) 23 | ) 24 | testSupport.assertTypedDate( 25 | for: typedDate.fill( 26 | to: \.day, 27 | arguments: (testSupport.month, testSupport.day) 28 | ) 29 | ) 30 | testSupport.assertTypedDate( 31 | for: typedDate.fill( 32 | to: \.hour, 33 | arguments: (testSupport.month, testSupport.day, testSupport.hour) 34 | ) 35 | ) 36 | testSupport.assertTypedDate( 37 | for: typedDate.fill( 38 | to: \.minute, 39 | arguments: (testSupport.month, testSupport.day, testSupport.hour, testSupport.minute) 40 | ) 41 | ) 42 | testSupport.assertTypedDate( 43 | for: typedDate.fill( 44 | to: \.second, 45 | arguments: (testSupport.month, testSupport.day, testSupport.hour, testSupport.minute, testSupport.second) 46 | ) 47 | ) 48 | testSupport.assertTypedDate( 49 | for: typedDate.fill( 50 | to: \.nanosecond, 51 | arguments: (testSupport.month, testSupport.day, testSupport.hour, testSupport.minute, testSupport.second, testSupport.nanosecond) 52 | ) 53 | ) 54 | } 55 | 56 | @Test 57 | func testFillInMonthDate() { 58 | let typedDate = testSupport.generateTypedMonthDate() 59 | testSupport.assertTypedDate( 60 | for: typedDate.fill( 61 | to: \.day, 62 | arguments: (testSupport.day) 63 | ) 64 | ) 65 | testSupport.assertTypedDate( 66 | for: typedDate.fill( 67 | to: \.hour, 68 | arguments: (testSupport.day, testSupport.hour) 69 | ) 70 | ) 71 | testSupport.assertTypedDate( 72 | for: typedDate.fill( 73 | to: \.minute, 74 | arguments: (testSupport.day, testSupport.hour, testSupport.minute) 75 | ) 76 | ) 77 | testSupport.assertTypedDate( 78 | for: typedDate.fill( 79 | to: \.second, 80 | arguments: (testSupport.day, testSupport.hour, testSupport.minute, testSupport.second) 81 | ) 82 | ) 83 | testSupport.assertTypedDate( 84 | for: typedDate.fill( 85 | to: \.nanosecond, 86 | arguments: (testSupport.day, testSupport.hour, testSupport.minute, testSupport.second, testSupport.nanosecond) 87 | ) 88 | ) 89 | } 90 | 91 | @Test 92 | func testFillInDayDate() { 93 | let typedDate = testSupport.generateTypedDayDate() 94 | testSupport.assertTypedDate( 95 | for: typedDate.fill( 96 | to: \.hour, 97 | arguments: ( testSupport.hour) 98 | ) 99 | ) 100 | testSupport.assertTypedDate( 101 | for: typedDate.fill( 102 | to: \.minute, 103 | arguments: (testSupport.hour, testSupport.minute) 104 | ) 105 | ) 106 | testSupport.assertTypedDate( 107 | for: typedDate.fill( 108 | to: \.second, 109 | arguments: (testSupport.hour, testSupport.minute, testSupport.second) 110 | ) 111 | ) 112 | testSupport.assertTypedDate( 113 | for: typedDate.fill( 114 | to: \.nanosecond, 115 | arguments: (testSupport.hour, testSupport.minute, testSupport.second, testSupport.nanosecond) 116 | ) 117 | ) 118 | } 119 | 120 | @Test 121 | func testFillInHourDate() { 122 | let typedDate = testSupport.generateTypedHourDate() 123 | testSupport.assertTypedDate( 124 | for: typedDate.fill( 125 | to: \.minute, 126 | arguments: (testSupport.minute) 127 | ) 128 | ) 129 | testSupport.assertTypedDate( 130 | for: typedDate.fill( 131 | to: \.second, 132 | arguments: (testSupport.minute, testSupport.second) 133 | ) 134 | ) 135 | testSupport.assertTypedDate( 136 | for: typedDate.fill( 137 | to: \.nanosecond, 138 | arguments: (testSupport.minute, testSupport.second, testSupport.nanosecond) 139 | ) 140 | ) 141 | } 142 | 143 | @Test 144 | func testFillInMinuteDate() { 145 | let typedDate = testSupport.generateTypedMinuteDate() 146 | testSupport.assertTypedDate( 147 | for: typedDate.fill( 148 | to: \.second, 149 | arguments: (testSupport.second) 150 | ) 151 | ) 152 | testSupport.assertTypedDate( 153 | for: typedDate.fill( 154 | to: \.nanosecond, 155 | arguments: (testSupport.second, testSupport.nanosecond) 156 | ) 157 | ) 158 | } 159 | 160 | @Test 161 | func testFillInSecondDate() { 162 | let typedDate = testSupport.generateTypedSecondDate() 163 | testSupport.assertTypedDate( 164 | for: typedDate.fill( 165 | to: \.nanosecond, 166 | arguments: (testSupport.nanosecond) 167 | ) 168 | ) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /Tests/TypedDateTests/TypedDateInitTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Testing 3 | @testable import TypedDate 4 | 5 | @Suite 6 | struct TypedDateInitTests { 7 | let calendar: Calendar 8 | let testSupport: TypedDateTestSupport 9 | 10 | init() { 11 | self.calendar = Calendar(identifier: .gregorian) 12 | self.testSupport = TypedDateTestSupport(calendar: calendar) 13 | } 14 | 15 | @Test 16 | func year() { 17 | let typedDate = testSupport.generateTypedYearDate() 18 | testSupport.assertTypedDate(for: typedDate) 19 | } 20 | 21 | @Test 22 | func month() { 23 | let typedDate = testSupport.generateTypedMonthDate() 24 | testSupport.assertTypedDate(for: typedDate) 25 | } 26 | 27 | @Test 28 | func day() { 29 | let typedDate = testSupport.generateTypedDayDate() 30 | testSupport.assertTypedDate(for: typedDate) 31 | } 32 | 33 | @Test 34 | func hour() { 35 | let typedDate = testSupport.generateTypedHourDate() 36 | testSupport.assertTypedDate(for: typedDate) 37 | } 38 | 39 | @Test 40 | func minute() { 41 | let typedDate = testSupport.generateTypedMinuteDate() 42 | testSupport.assertTypedDate(for: typedDate) 43 | } 44 | 45 | @Test 46 | func second() { 47 | let typedDate = testSupport.generateTypedSecondDate() 48 | testSupport.assertTypedDate(for: typedDate) 49 | } 50 | 51 | @Test 52 | func nanosecond() { 53 | let typedDate = testSupport.generateTypedNanosecondDate() 54 | testSupport.assertTypedDate(for: typedDate) 55 | } 56 | 57 | @Test 58 | func fractionalSecond() { 59 | let typedDate = TypedDate(testSupport.year, testSupport.month, testSupport.day, testSupport.hour, testSupport.minute, testSupport.fractionalSecond, calendar: calendar) 60 | testSupport.assertTypedDate(for: typedDate) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Tests/TypedDateTests/TypedDateModifyTests.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | @testable import TypedDate 3 | import TypedDateCore 4 | import Foundation 5 | 6 | @Suite 7 | struct TypedDateModifyTests { 8 | let testSupport: TypedDateTestSupport 9 | let calendar: Calendar 10 | 11 | init() { 12 | calendar = Calendar(identifier: .gregorian) 13 | testSupport = TypedDateTestSupport(calendar: calendar) 14 | } 15 | 16 | @Test 17 | func testNanosecondModify() { 18 | let typedDate = testSupport.generateTypedNanosecondDate() 19 | .modifying(\.year) { $0 += 1 } 20 | .modifying(\.month) { $0 -= 2 } 21 | .modifying(\.day) { $0 += 3 } 22 | .modifying(\.hour) { $0 += 4 } 23 | .modifying(\.minute) { $0 += 5 } 24 | .modifying(\.second) { $0 += 6 } 25 | #expect(typedDate.year(calendar: calendar) == testSupport.year.add(1)) 26 | #expect(typedDate.month(calendar: calendar) == testSupport.month.add(-2)) 27 | #expect(typedDate.day(calendar: calendar) == testSupport.day.add(3)) 28 | #expect(typedDate.hour(calendar: calendar) == testSupport.hour.add(4)) 29 | #expect(typedDate.minute(calendar: calendar) == testSupport.minute.add(5)) 30 | #expect(typedDate.second(calendar: calendar) == testSupport.second.add(6)) 31 | #expect(typedDate.nanosecond(calendar: calendar) == testSupport.nanosecond) 32 | } 33 | 34 | @Test 35 | func testSecondModify() { 36 | let typedDate = testSupport.generateTypedSecondDate() 37 | .modifying(\.year) { $0 += 1 } 38 | .modifying(\.month) { $0 -= 2 } 39 | .modifying(\.day) { $0 += 3 } 40 | .modifying(\.hour) { $0 += 4 } 41 | .modifying(\.minute) { $0 += 5 } 42 | .modifying(\.second) { $0 += 6 } 43 | #expect(typedDate.year(calendar: calendar) == testSupport.year.add(1)) 44 | #expect(typedDate.month(calendar: calendar) == testSupport.month.add(-2)) 45 | #expect(typedDate.day(calendar: calendar) == testSupport.day.add(3)) 46 | #expect(typedDate.hour(calendar: calendar) == testSupport.hour.add(4)) 47 | #expect(typedDate.minute(calendar: calendar) == testSupport.minute.add(5)) 48 | #expect(typedDate.second(calendar: calendar) == testSupport.second.add(6)) 49 | } 50 | 51 | @Test 52 | func testMinuteModify() { 53 | let typedDate = testSupport.generateTypedMinuteDate() 54 | .modifying(\.year) { $0 += 1 } 55 | .modifying(\.month) { $0 -= 2 } 56 | .modifying(\.day) { $0 += 3 } 57 | .modifying(\.hour) { $0 += 4 } 58 | .modifying(\.minute) { $0 += 5 } 59 | #expect(typedDate.year(calendar: calendar) == testSupport.year.add(1)) 60 | #expect(typedDate.month(calendar: calendar) == testSupport.month.add(-2)) 61 | #expect(typedDate.day(calendar: calendar) == testSupport.day.add(3)) 62 | #expect(typedDate.hour(calendar: calendar) == testSupport.hour.add(4)) 63 | #expect(typedDate.minute(calendar: calendar) == testSupport.minute.add(5)) 64 | } 65 | 66 | @Test 67 | func testHourModify() { 68 | let typedDate = testSupport.generateTypedHourDate() 69 | .modifying(\.year) { $0 += 1 } 70 | .modifying(\.month) { $0 -= 2 } 71 | .modifying(\.day) { $0 += 3 } 72 | .modifying(\.hour) { $0 += 4 } 73 | #expect(typedDate.year(calendar: calendar) == testSupport.year.add(1)) 74 | #expect(typedDate.month(calendar: calendar) == testSupport.month.add(-2)) 75 | #expect(typedDate.day(calendar: calendar) == testSupport.day.add(3)) 76 | #expect(typedDate.hour(calendar: calendar) == testSupport.hour.add(4)) 77 | } 78 | 79 | @Test 80 | func testDayModify() { 81 | let typedDate = testSupport.generateTypedDayDate() 82 | .modifying(\.year) { $0 += 1 } 83 | .modifying(\.month) { $0 -= 2 } 84 | .modifying(\.day) { $0 += 3 } 85 | #expect(typedDate.year(calendar: calendar) == testSupport.year.add(1)) 86 | #expect(typedDate.month(calendar: calendar) == testSupport.month.add(-2)) 87 | #expect(typedDate.day(calendar: calendar) == testSupport.day.add(3)) 88 | } 89 | 90 | @Test 91 | func testMonthModify() { 92 | let typedDate = testSupport.generateTypedMonthDate() 93 | .modifying(\.year) { $0 += 1 } 94 | .modifying(\.month) { $0 -= 2 } 95 | #expect(typedDate.year(calendar: calendar) == testSupport.year.add(1)) 96 | #expect(typedDate.month(calendar: calendar) == testSupport.month.add(-2)) 97 | } 98 | 99 | @Test 100 | func testYearModify() { 101 | let typedDate = testSupport.generateTypedYearDate() 102 | .modifying(\.year) { $0 += 1 } 103 | #expect(typedDate.year(calendar: calendar) == testSupport.year.add(1)) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Tests/TypedDateTests/TypedDateScopeTests.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | @testable import TypedDate 3 | import TypedDateCore 4 | import Foundation 5 | 6 | @Suite 7 | struct TypedDateScopeTests { 8 | let calendar: Calendar 9 | let testSupport: TypedDateTestSupport 10 | 11 | init() { 12 | calendar = Calendar(identifier: .gregorian) 13 | testSupport = TypedDateTestSupport(calendar: calendar) 14 | } 15 | 16 | @Test 17 | func testScope() throws { 18 | let generatedDate = testSupport.generateDate() 19 | let date = try #require(generatedDate) 20 | testSupport.assertTypedDate(for: date.scope(to: \.year, calendar: calendar)) 21 | testSupport.assertTypedDate(for: date.scope(to: \.month, calendar: calendar)) 22 | testSupport.assertTypedDate(for: date.scope(to: \.day, calendar: calendar)) 23 | testSupport.assertTypedDate(for: date.scope(to: \.hour, calendar: calendar)) 24 | testSupport.assertTypedDate(for: date.scope(to: \.minute, calendar: calendar)) 25 | testSupport.assertTypedDate(for: date.scope(to: \.second, calendar: calendar)) 26 | testSupport.assertTypedDate(for: date.scope(to: \.nanosecond, calendar: calendar)) 27 | } 28 | 29 | @Test(arguments: Calendar.Identifier.allCases) 30 | func testScopeNotNil(identifier: Calendar.Identifier) { 31 | let calendar = Calendar(identifier: identifier) 32 | testSupport.dates(calendar: calendar).compactMap { $0 }.forEach { 33 | let nanosecond = $0.scope(to: \.nanosecond, calendar: calendar) 34 | #expect(nanosecond != nil) 35 | let second = $0.scope(to: \.second, calendar: calendar) 36 | #expect(second != nil) 37 | let minute = $0.scope(to: \.minute, calendar: calendar) 38 | #expect(minute != nil) 39 | let hour = $0.scope(to: \.hour, calendar: calendar) 40 | #expect(hour != nil) 41 | let day = $0.scope(to: \.day, calendar: calendar) 42 | #expect(day != nil) 43 | let month = $0.scope(to: \.month, calendar: calendar) 44 | #expect(month != nil) 45 | let year = $0.scope(to: \.year, calendar: calendar) 46 | #expect(year != nil) 47 | } 48 | } 49 | 50 | @Test(arguments: Calendar.Identifier.allCases) 51 | func testDateNotNil(identifier: Calendar.Identifier) { 52 | testSupport.dates(calendar: Calendar(identifier: identifier)).forEach { date in 53 | #expect(date != nil) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Tests/TypedDateTests/TypedDateTestSupport.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Testing 3 | @testable import TypedDate 4 | 5 | struct TypedDateTestSupport { 6 | let year = Year(2023) 7 | let month = Month(11) 8 | let day = Day(10) 9 | let hour = Hour(3) 10 | let minute = Minute(50) 11 | let second = Second(10) 12 | let nanosecond = Nanosecond(111111164) 13 | let fractionalSecond = FractionalSecond(10.111111164) 14 | 15 | let calendar: Calendar 16 | 17 | init(calendar: Calendar) { 18 | self.calendar = calendar 19 | } 20 | 21 | func assertTypedDate(for typedDate: TypedDate<(Year)>) { 22 | #expect(typedDate.year(calendar: calendar) == year.value) 23 | #expect(calendar.dateComponents([.month], from: typedDate.date).month == 1) 24 | #expect(calendar.dateComponents([.day], from: typedDate.date).day == 1) 25 | #expect(calendar.dateComponents([.hour], from: typedDate.date).hour == 0) 26 | #expect(calendar.dateComponents([.minute], from: typedDate.date).minute == 0) 27 | #expect(calendar.dateComponents([.second], from: typedDate.date).second == 0) 28 | } 29 | 30 | func assertTypedDate(for typedDate: TypedDate<(Year, Month)>) { 31 | #expect(typedDate.year(calendar: calendar) == year.value) 32 | #expect(typedDate.month(calendar: calendar) == month.value) 33 | #expect(calendar.dateComponents([.day], from: typedDate.date).day == 1) 34 | #expect(calendar.dateComponents([.hour], from: typedDate.date).hour == 0) 35 | #expect(calendar.dateComponents([.minute], from: typedDate.date).minute == 0) 36 | #expect(calendar.dateComponents([.second], from: typedDate.date).second == 0) 37 | } 38 | 39 | func assertTypedDate(for typedDate: TypedDate<(Year, Month, Day)>) { 40 | #expect(typedDate.year(calendar: calendar) == year.value) 41 | #expect(typedDate.month(calendar: calendar) == month.value) 42 | #expect(typedDate.day(calendar: calendar) == day.value) 43 | #expect(calendar.dateComponents([.hour], from: typedDate.date).hour == 0) 44 | #expect(calendar.dateComponents([.minute], from: typedDate.date).minute == 0) 45 | #expect(calendar.dateComponents([.second], from: typedDate.date).second == 0) 46 | } 47 | 48 | func assertTypedDate(for typedDate: TypedDate<(Year, Month, Day, Hour)>) { 49 | #expect(typedDate.year(calendar: calendar) == year.value) 50 | #expect(typedDate.month(calendar: calendar) == month.value) 51 | #expect(typedDate.day(calendar: calendar) == day.value) 52 | #expect(typedDate.hour(calendar: calendar) == hour.value) 53 | #expect(calendar.dateComponents([.minute], from: typedDate.date).minute == 0) 54 | #expect(calendar.dateComponents([.second], from: typedDate.date).second == 0) 55 | } 56 | 57 | func assertTypedDate(for typedDate: TypedDate<(Year, Month, Day, Hour, Minute)>) { 58 | #expect(typedDate.year(calendar: calendar) == year.value) 59 | #expect(typedDate.month(calendar: calendar) == month.value) 60 | #expect(typedDate.day(calendar: calendar) == day.value) 61 | #expect(typedDate.hour(calendar: calendar) == hour.value) 62 | #expect(typedDate.minute(calendar: calendar) == minute.value) 63 | #expect(calendar.dateComponents([.second], from: typedDate.date).second == 0) 64 | #expect(calendar.dateComponents([.nanosecond], from: typedDate.date).nanosecond == 0) 65 | } 66 | 67 | func assertTypedDate(for typedDate: TypedDate<(Year, Month, Day, Hour, Minute, Second)>) { 68 | #expect(typedDate.year(calendar: calendar) == year.value) 69 | #expect(typedDate.month(calendar: calendar) == month.value) 70 | #expect(typedDate.day(calendar: calendar) == day.value) 71 | #expect(typedDate.hour(calendar: calendar) == hour.value) 72 | #expect(typedDate.minute(calendar: calendar) == minute.value) 73 | #expect(typedDate.second(calendar: calendar) == second.value) 74 | #expect(calendar.dateComponents([.nanosecond], from: typedDate.date).nanosecond == 0) 75 | } 76 | 77 | func assertTypedDate(for typedDate: TypedDate<(Year, Month, Day, Hour, Minute, Second, Nanosecond)>) { 78 | #expect(typedDate.year(calendar: calendar) == year.value) 79 | #expect(typedDate.month(calendar: calendar) == month.value) 80 | #expect(typedDate.day(calendar: calendar) == day.value) 81 | #expect(typedDate.hour(calendar: calendar) == hour.value) 82 | #expect(typedDate.minute(calendar: calendar) == minute.value) 83 | #expect(typedDate.second(calendar: calendar) == second.value) 84 | #expect(typedDate.nanosecond(calendar: calendar) == nanosecond.value) 85 | } 86 | 87 | func dates(calendar: Calendar) -> [Date?] { 88 | [ 89 | generateDate(calendar: calendar), 90 | generateDate(calendar: calendar, year: 2023), generateDate(calendar: calendar, month: 1), generateDate(calendar: calendar, day: 1), 91 | generateDate(calendar: calendar, minute: 1), generateDate(calendar: calendar, second: 1), generateDate(calendar: calendar, nanosecond: 1), 92 | generateDate(calendar: calendar, year: 2023, month: 1, day: 31), 93 | generateDate(calendar: calendar, year: 2023, month: 2, day: 28), 94 | generateDate(calendar: calendar, year: 2024, month: 2, day: 29), 95 | generateDate(calendar: calendar, year: 2023, month: 2, day: 30), 96 | generateDate(calendar: calendar, year: 2023, month: 13, day: 1), 97 | generateDate(calendar: calendar, hour: 23, minute: 59, second: 59), 98 | generateDate(calendar: calendar, year: 2023, month: 12, day: 31, hour: 23, minute: 59, second: 59), 99 | generateDate(calendar: calendar, year: 3000, month: 1, day: 1), 100 | generateDate(calendar: calendar, year: 1900, month: 1, day: 1), 101 | generateDate(calendar: calendar, year: 2023, month: 1, day: 1, hour: 25, minute: 0, second: 0), 102 | generateDate(calendar: calendar, year: 2023, month: 1, day: 1, hour: 12, minute: 60, second: 0), 103 | generateDate(calendar: calendar, year: 2023, month: 1, day: 1, hour: 12, minute: 0, second: 61), 104 | generateDate(calendar: calendar, year: 2023, month: 1, day: 1, hour: 12, minute: 0, second: 0, nanosecond: 500000000), 105 | generateDate(calendar: calendar, year: 2023, month: 1, day: 1, hour: 12, minute: 0, second: 0, nanosecond: 1_000_000_000), 106 | generateDate(calendar: calendar, year: -1, month: 1, day: 1), 107 | generateDate(calendar: calendar, year: 2023, month: -1, day: 1), 108 | generateDate(calendar: calendar, year: 2023, month: 1, day: -1), 109 | generateDate(calendar: calendar, year: 2023, month: 1, day: 1, hour: 0, minute: -1, second: 0), 110 | generateDate(calendar: calendar, year: 2023, month: 1, day: 1, hour: 0, minute: 0, second: -1), 111 | generateDate(calendar: calendar, year: 2023, month: 1, day: 1, hour: 0, minute: 0, second: 0, nanosecond: -1) 112 | ] 113 | } 114 | 115 | func generateTypedNanosecondDate() -> TypedDate<(Year, Month, Day, Hour, Minute, Second, Nanosecond)> { 116 | TypedDate(year, month, day, hour, minute, second, nanosecond, calendar: calendar) 117 | } 118 | 119 | func generateTypedSecondDate() -> TypedDate<(Year, Month, Day, Hour, Minute, Second)> { 120 | TypedDate(year, month, day, hour, minute, second, calendar: calendar) 121 | } 122 | 123 | func generateTypedMinuteDate() -> TypedDate<(Year, Month, Day, Hour, Minute)> { 124 | TypedDate(year, month, day, hour, minute, calendar: calendar) 125 | } 126 | 127 | func generateTypedHourDate() -> TypedDate<(Year, Month, Day, Hour)> { 128 | TypedDate(year, month, day, hour, calendar: calendar) 129 | } 130 | 131 | func generateTypedDayDate() -> TypedDate<(Year, Month, Day)> { 132 | TypedDate(year, month, day, calendar: calendar) 133 | } 134 | 135 | func generateTypedMonthDate() -> TypedDate<(Year, Month)> { 136 | TypedDate(year, month, calendar: calendar) 137 | } 138 | 139 | func generateTypedYearDate() -> TypedDate<(Year)> { 140 | TypedDate(year, calendar: calendar) 141 | } 142 | 143 | func generateDate() -> Date? { 144 | generateDate( 145 | calendar: calendar, 146 | year: year.value, 147 | month: month.value, 148 | day: day.value, 149 | hour: hour.value, 150 | minute: minute.value, 151 | second: second.value, 152 | nanosecond: nanosecond.value 153 | ) 154 | } 155 | 156 | func generateDate( 157 | calendar: Calendar, 158 | year: Int? = nil, 159 | month: Int? = nil, 160 | day: Int? = nil, 161 | hour: Int? = nil, 162 | minute: Int? = nil, 163 | second: Int? = nil, 164 | nanosecond: Int? = nil 165 | ) -> Date? { 166 | DateComponents( 167 | calendar: .current, 168 | year: year, 169 | month: month, 170 | day: day, 171 | hour: hour, 172 | minute: minute, 173 | second: second, 174 | nanosecond: nanosecond 175 | ).date 176 | } 177 | } 178 | 179 | extension Calendar.Identifier { 180 | static var allCases: Set { 181 | [ 182 | .buddhist, .chinese, .coptic, .ethiopicAmeteAlem, .ethiopicAmeteMihret, .gregorian, .hebrew, .indian, 183 | .islamic, .islamicCivil, .islamicTabular, .islamicUmmAlQura, .iso8601, .japanese, .persian, .republicOfChina 184 | ] 185 | } 186 | } 187 | --------------------------------------------------------------------------------