├── .gitignore ├── _Media └── icon.png ├── Tests ├── LinuxMain.swift └── DateBuilderTests │ ├── XCTestManifests.swift │ └── DateBuilderTests.swift ├── .swiftpm └── xcode │ └── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── LICENSE ├── Package.swift ├── README.md └── Sources └── DateBuilder └── DateBuilder.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /_Media/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreymonde/DateBuilder/HEAD/_Media/icon.png -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import DateBuilderTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += DateBuilderTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tests/DateBuilderTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | #if !os(watchOS) 2 | import XCTest 3 | #endif 4 | 5 | #if !canImport(ObjectiveC) 6 | public func allTests() -> [XCTestCaseEntry] { 7 | return [ 8 | testCase(DateBuilderTests.allTests), 9 | ] 10 | } 11 | #endif 12 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Nice Photon 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.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | import Foundation 6 | 7 | let package = Package( 8 | name: "DateBuilder", 9 | platforms: [ 10 | .iOS(.v10), 11 | .macOS(.v10_12), 12 | .tvOS(.v10), 13 | .watchOS(.v3), 14 | ], 15 | products: [ 16 | // Products define the executables and libraries a package produces, and make them visible to other packages. 17 | .library( 18 | name: "DateBuilder", 19 | targets: ["DateBuilder"]), 20 | ], 21 | dependencies: [ 22 | // Dependencies declare other packages that this package depends on. 23 | // .package(url: /* package url */, from: "1.0.0"), 24 | ], 25 | targets: [ 26 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 27 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 28 | .target( 29 | name: "DateBuilder", 30 | dependencies: []), 31 | .testTarget( 32 | name: "DateBuilderTests", 33 | dependencies: ["DateBuilder"]), 34 | ] 35 | ) 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DateBuilder 2 | 3 | 4 | 5 | **DateBuilder** allows you to create `Date` and `DateComponents` instances with ease in a visual and declarative manner. With **DateBuilder**, it's very trivial to define dates from as simple as *"tomorrow at 9pm"* or as complex as *"first fridays for the next 24 months, at random times between 3pm and 7pm"*. 6 | 7 | Maintainer: [@dreymonde](https://github.com/dreymonde) 8 | 9 | As of now, **DateBuilder** is in beta. Some APIs might be changed between releases. 10 | 11 | **DateBuilder** is a stand-alone part of **[NiceNotifications](https://github.com/dreymonde/NiceNotifications)**, a framework that radically simplifies local notifications, from content to permissions. 12 | 13 | ## Usage 14 | 15 | ```swift 16 | import DateBuilder 17 | 18 | Today() 19 | .at(hour: 20, minute: 15) 20 | .dateComponents() // year: 2021, month: 1, day: 31, hour: 20, minute: 15 21 | 22 | NextWeek() 23 | .weekday(.saturday) 24 | .at(hour: 18, minute: 50) 25 | .dateComponents() // DateComponents 26 | 27 | EveryWeek(forWeeks: 10, starting: .thisWeek) 28 | .weekendStartDay 29 | .at(hour: 9, minute: 00) 30 | .dates() // [Date] 31 | 32 | ExactlyAt(account.createdAt) 33 | .addingDays(15) 34 | .date() // Date 35 | 36 | WeekOf(account.createdAt) 37 | .addingWeeks(1) 38 | .lastDay 39 | .at(hour: 10, minute: 00) 40 | .dateComponents() // DateComponents 41 | 42 | EveryMonth(forMonths: 12, starting: .thisMonth) 43 | .lastDay 44 | .at(hour: 23, minute: 50) 45 | .dateComponents() // [DateComponents] 46 | 47 | NextYear().addingYears(2) 48 | .firstMonth.addingMonths(3) // April (in Gregorian) 49 | .first(.thursday) 50 | .dateComponents() // year: 2024, month: 4, day: 4 51 | 52 | ExactDay(year: 2020, month: 10, day: 5) 53 | .at(hour: 10, minute: 15) 54 | .date() // Date 55 | 56 | ExactYear(year: 2020) 57 | .lastMonth 58 | .lastDay 59 | .dateComponents() 60 | ``` 61 | 62 | ## Guide 63 | 64 | ### Anatomy of a date builder 65 | 66 | Every **DateBuilder** expression ends on a specific _day_ (or a set of days if you use functions like `EveryDay`/`EveryMonth`/etc.). First you specify your expression down to a day, and then define the time of day by calling `at(hour:minute:)` function. For example: 67 | 68 | ```swift 69 | NextWeek() 70 | .firstDay 71 | .at(hour: 10, minute: 15) 72 | ``` 73 | 74 | Once you have your `at` expression, your date is now fully resolved. You can get a ready-to-use `Date` or `DateComponents` instance by calling `.date()` or `.dateComponents()`. 75 | 76 | Slightly more complicated example would be: 77 | 78 | ```swift 79 | let dateComponents = NextYear() 80 | .firstMonth.addingMonths(3) 81 | .first(.thursday) 82 | .at(hour: 21, minute: 00) 83 | .dateComponents() 84 | ``` 85 | 86 | So we start on the scale of years, then we notch it down to the scale of months, and then we finally get the specific day, which in this case will be the first thursday of a 4th month of the next year. After that, we finalize our query by using the `at` function. 87 | 88 | ### Available functions 89 | 90 | #### Day 91 | 92 | ```swift 93 | // top-level 94 | Today() 95 | Tomorrow() 96 | DayOf(account.createdAt) 97 | ExactDay(year: 2021, month: 1, day: 26) 98 | AddingDays(15, to: .today) 99 | AddingDays(15, to: .dayOf(account.createdAt)) 100 | EveryDay(forDays: 100, starting: .tomorrow) 101 | EveryDay(forDays: 100, starting: .dayOf(account.createdAt)) 102 | 103 | // instance 104 | Today() 105 | --->.addingDays(10) 106 | ``` 107 | 108 | #### Week 109 | 110 | **NOTE:** the start and end of the week is determined by the currently set `Calendar` and its `Locale`. To learn how to customize the calendar object used for **DateBuilder** queries, see *"Customizing the Calendar / Locale / Timezone"* section below 111 | 112 | ```swift 113 | // top-level 114 | ThisWeek() 115 | NextWeek() 116 | WeekOf(account.createdAt) 117 | WeekOf(Today()) // use any `DateBuilder.Day` instance here 118 | AddingWeeks(5, to: .thisWeek) 119 | EveryWeek(forWeeks: 10, starting: .nextWeek) 120 | 121 | // instance 122 | ThisWeek() 123 | --->.addingWeeks(10) // Week 124 | --->.firstDay // Day 125 | --->.lastDay // Day 126 | --->.allDays // [Day] 127 | --->.weekday(.thursday) // Day 128 | --->.weekendStartDay // Day 129 | --->.weekendEndDay // Day 130 | ``` 131 | 132 | #### Month 133 | 134 | ```swift 135 | // top-level 136 | ThisMonth() 137 | NextMonth() 138 | MonthOf(account.createdAt) 139 | MonthOf(Today()) // use any `DateBuilder.Day` instance here 140 | ExactMonth(year: 2021, month: 03) 141 | AddingMonths(3, to: .thisMonth) 142 | EveryMonth(forMonths: 5, starting: .monthOf(account.createdAt)) 143 | 144 | // instance 145 | ThisMonth() 146 | --->.addingMonths(5) // Month 147 | --->.firstDay // Day 148 | --->.lastDay // Day 149 | --->.allDays // [Day] 150 | --->.first(.saturday) // Day 151 | --->.weekday(.third, .friday) // Day 152 | ``` 153 | 154 | #### Year 155 | 156 | ```swift 157 | // top-level 158 | ThisYear() 159 | NextYear() 160 | YearOf(account.createdAt) 161 | YearOf(Tomorrow()) // use any `DateBuilder.Day` instance here 162 | YearOf(NextMonth()) // use any `DateBuilder.Month` instance here 163 | ExactYear(year: 2022) 164 | AddingYears(1, to: ThisYear()) 165 | EveryYear(forYears: 100, starting: .thisYear) 166 | 167 | // instance 168 | ThisYear() 169 | --->.addingYears(1) // Year 170 | --->.firstMonth // Month 171 | --->.lastMonth // Month 172 | --->.allMonths // [Month] 173 | ``` 174 | 175 | #### Resolving the date 176 | 177 | ```swift 178 | Today() 179 | --->.at(hour: 10, minute: 15) 180 | --->.at(hour: 19, minute: 30, second: 30) 181 | --->.at(TimeOfDay(hour: 10, minute: 30, second: 0)) // equivalent to: 182 | --->.at(.time(hour: 10, minute: 30)) 183 | --->.at(.randomTime(from: .time(hour: 10, minute: 15), to: .time(hour: 15, minute: 30))) 184 | ``` 185 | 186 | ```swift 187 | Today() 188 | .at(hour: 9, minute: 15) 189 | .date() // Date 190 | 191 | // or 192 | 193 | Today() 194 | .at(hour: 9, minute: 15) 195 | .dateComponents() // DateComponents 196 | ``` 197 | 198 | You can also get the `DateComponents` (but not `Date`) instance by calling `dateComponents()` on an instance of `DateBuilder.Day`, without using `at`: 199 | 200 | ```swift 201 | NextMonth() 202 | .firstDay 203 | .dateComponents() // year: 2021, month: 2, day: 1 204 | ``` 205 | 206 | #### Using `ExactlyAt` function 207 | 208 | `ExactlyAt` creates a resolved date from the existing `Date` instance. You can then use it to perform easy date calculations (functions `addingMinutes`/`addingHours` etc.) and easily get `Date` or `DateComponents` instances. 209 | 210 | ```swift 211 | ExactlyAt(account.createdAt) 212 | --->.addingSeconds(30) 213 | --->.addingMinutes(1) 214 | --->.addingHours(5) 215 | --->.addingDays(20) 216 | --->.addingMonths(3) 217 | --->.addingWeeks(14) 218 | --->.addingYears(1) 219 | 220 | // usge: 221 | ExactlyAt(account.createdAt) 222 | .addingMinutes(15) 223 | .dateComponents() // DateComponents 224 | ``` 225 | 226 | ### Using `Every` functions 227 | 228 | You can use `EveryDay`, `EveryWeek`, `EveryMonth` and `EveryYear` functions in the same way as you would use something like `Today()` or `NextYear()`. The only difference is that at the end you will get an array of dates instead of a single instance: 229 | 230 | ```swift 231 | let dates = EveryMonth(forMonths: 12, starting: .thisMonth) 232 | .firstDay.addingDays(9) 233 | .at(hour: 20, minute: 00) 234 | .dates() // [Date] 235 | 236 | // or 237 | 238 | let dates = EveryMonth(forMonths: 12, starting: .thisMonth) 239 | .lastDay.addingDays(-5) 240 | .at(hour: 20, minute: 00) 241 | .dateComponents() // [DateComponents] 242 | ``` 243 | 244 | In case you use `.at(.randomTime( ... ))` function with `Every` functions, the exact resolved time will be different each day. 245 | 246 | ### Customizing the Calendar / Locale / Timezone 247 | 248 | By default, **DateBuilder** uses `Calendar.current` for all calculations. If you need to customize it, you can either change it globally: 249 | 250 | ```swift 251 | var customCalendar = DateBuilder.calendar 252 | customCalendar.firstWeekday = 6 253 | DateBuilder.calendar = customCalendar 254 | ``` 255 | 256 | Or temporarily, using the `DateBuilder.withCalendar` function: 257 | 258 | ```swift 259 | DateBuilder.withCalendar(customCalendar) { 260 | ThisWeek().firstDay.dateComponents() 261 | } 262 | ``` 263 | 264 | **DateBuilder** will return to its global `Calendar` instance after evaluating the expression. 265 | 266 | In a similar manner, you can also use `DateBuilder.withTimeZone` and `DateBuilder.withLocale` functions: 267 | 268 | ```swift 269 | DateBuilder.withTimeZone(TimeZone(identifier: "America/Cancun")) { 270 | Tomorrow().at(hour: 9, minute: 15).date() 271 | } 272 | 273 | let nextFriday = DateBuilder.withLocale(Locale(identifier: "he_IL")) { 274 | NextWeek() 275 | .weekendStartDay 276 | .at(hour: 7, minute: 00) 277 | .date() // next friday! 278 | } 279 | ``` 280 | 281 | All of these functions support returning the result of the closure (see above). 282 | 283 | ## Installation 284 | 285 | ### Swift Package Manager 286 | 1. Click File → Swift Packages → Add Package Dependency. 287 | 2. Enter `http://github.com/nicephoton/DateBuilder.git`. 288 | 289 | ## Acknowledgments 290 | 291 | Special thanks to: 292 | 293 | - [@mattt](https://github.com/mattt) for his wonderful article: [Date​Components - NSHipster](https://nshipster.com/datecomponents/) 294 | - [@camanjj](https://github.com/camanjj) for his valuable feedback on the API 295 | 296 | Related materials: 297 | 298 | - **[Time](https://github.com/dreymonde/Time)** by [@dreymonde](https://github.com/dreymonde) - Type-safe time calculations in Swift, powered by generics 299 | - **[Time](https://github.com/davedelong/Time)** by [@davedelong](https://github.com/dreymonde) - Building a better date/time library for Swift 300 | -------------------------------------------------------------------------------- /Tests/DateBuilderTests/DateBuilderTests.swift: -------------------------------------------------------------------------------- 1 | #if !os(watchOS) 2 | import XCTest 3 | @testable import DateBuilder 4 | 5 | final class DateBuilderTests: XCTestCase { 6 | func testExample() { 7 | // This is an example of a functional test case. 8 | // Use XCTAssert and related functions to verify your tests produce the correct 9 | // results. 10 | twitter() 11 | 12 | let normalDelays = DelayDistribution.normal.generate(count: 100, start: 0, addDelay: { $0 + $1 }) 13 | print("DELAYS", normalDelays) 14 | 15 | let optimisedDelays = DelayDistribution.optimized.generate(count: 100, start: 0, addDelay: { $0 + $1 }) 16 | print("DELAYS", optimisedDelays) 17 | } 18 | 19 | override func tearDown() { 20 | super.tearDown() 21 | DateBuilder.calendar = .current 22 | } 23 | 24 | func testToday() { 25 | let dc = Today() 26 | .at(hour: 10, minute: 15) 27 | .dateComponents() 28 | let st = DateBuilder.Day.today.at(.time(hour: 10, minute: 15)).dateComponents() 29 | 30 | let actual = Date()._makeComponents() 31 | 32 | for today in [dc, st] { 33 | XCTAssertEqual(today.day, actual.day) 34 | XCTAssertEqual(today.month, actual.month) 35 | XCTAssertEqual(today.year, actual.year) 36 | XCTAssertEqual(today.hour, 10) 37 | XCTAssertEqual(today.minute, 15) 38 | XCTAssertEqual(today.second, 0) 39 | } 40 | } 41 | 42 | func testTomorrow() { 43 | let dc = Tomorrow() 44 | .at(hour: 14, minute: 30, second: 30) 45 | .dateComponents() 46 | let st = DateBuilder.Day.tomorrow.at(.time(hour: 14, minute: 30, second: 30)).dateComponents() 47 | 48 | let actual = Calendar.current.date(byAdding: .day, value: 1, to: Date())!._makeComponents() 49 | 50 | for tomorrow in [dc, st] { 51 | XCTAssertEqual(tomorrow.day, actual.day) 52 | XCTAssertEqual(tomorrow.month, actual.month) 53 | XCTAssertEqual(tomorrow.year, actual.year) 54 | XCTAssertEqual(tomorrow.hour, 14) 55 | XCTAssertEqual(tomorrow.minute, 30) 56 | XCTAssertEqual(tomorrow.second, 30) 57 | } 58 | } 59 | 60 | func testDayOf() { 61 | let randomDate = Date().addingTimeInterval(.random(in: 1 ... 5000) * 360 * 600) 62 | let dc = DayOf(randomDate).at(hour: 18, minute: 11).dateComponents() 63 | let st = DateBuilder.Day.dayOf(randomDate).at(hour: 18, minute: 11, second: 00).dateComponents() 64 | 65 | let actual = randomDate._makeComponents() 66 | 67 | for gen in [dc, st] { 68 | XCTAssertEqual(gen.day, actual.day) 69 | XCTAssertEqual(gen.month, actual.month) 70 | XCTAssertEqual(gen.year, actual.year) 71 | XCTAssertEqual(gen.hour, 18) 72 | XCTAssertEqual(gen.minute, 11) 73 | XCTAssertEqual(gen.second, 00) 74 | } 75 | } 76 | 77 | func testExactDay() { 78 | let dc = ExactDay(year: 2019, month: 10, day: 17).dateComponents() 79 | for gen in [dc] { 80 | XCTAssertEqual(gen.day, 17) 81 | XCTAssertEqual(gen.month, 10) 82 | XCTAssertEqual(gen.year, 2019) 83 | } 84 | } 85 | 86 | func testChangingCalendar() { 87 | var modified = Calendar.current 88 | let startOfNextWeek = NextWeek().firstDay.at(hour: 10, minute: 15).date() 89 | print(startOfNextWeek) 90 | modified.firstWeekday = 4 91 | DateBuilder.calendar = modified 92 | let _startOfNextWeek = NextWeek().firstDay.at(hour: 10, minute: 15).date() 93 | print(_startOfNextWeek) 94 | XCTAssertNotEqual(startOfNextWeek, _startOfNextWeek) 95 | DateBuilder.calendar = .current 96 | let againStartOfNextWeek = NextWeek().firstDay.at(hour: 10, minute: 15).date() 97 | print(againStartOfNextWeek) 98 | XCTAssertEqual(startOfNextWeek, againStartOfNextWeek) 99 | } 100 | 101 | func testWithCalendar() { 102 | var current = Calendar.current 103 | let startOfNextWeek = NextWeek().firstDay.at(hour: 10, minute: 15).date() 104 | print(startOfNextWeek) 105 | current.firstWeekday = 4 106 | DateBuilder.withCalendar(current) { 107 | let _startOfNextWeek = NextWeek().firstDay.at(hour: 10, minute: 15).date() 108 | print(_startOfNextWeek) 109 | XCTAssertNotEqual(startOfNextWeek, _startOfNextWeek) 110 | } 111 | let againStartOfNextWeek = NextWeek().firstDay.at(hour: 10, minute: 15).date() 112 | print(againStartOfNextWeek) 113 | XCTAssertEqual(startOfNextWeek, againStartOfNextWeek) 114 | } 115 | 116 | func testWithTimeZone() { 117 | var current = Calendar.current 118 | current.timeZone = TimeZone(identifier: "America/Los_Angeles")! 119 | DateBuilder.calendar = current 120 | let tomorrowMorning = Tomorrow().at(hour: 9, minute: 00).date() 121 | print(tomorrowMorning) 122 | DateBuilder.withTimeZone(TimeZone(identifier: "Europe/Moscow")!) { 123 | print(DateBuilder.calendar.timeZone) 124 | let _tomorrowMorning = Tomorrow().at(hour: 9, minute: 00).date() 125 | print(_tomorrowMorning) 126 | XCTAssertNotEqual(tomorrowMorning.timeIntervalSince1970, _tomorrowMorning.timeIntervalSince1970) 127 | } 128 | let againTomorrowMorning = Tomorrow().at(hour: 9, minute: 00).date() 129 | print(againTomorrowMorning) 130 | XCTAssertEqual(tomorrowMorning, againTomorrowMorning) 131 | DateBuilder.calendar = .current 132 | } 133 | 134 | func testWithLocale() { 135 | var current = Calendar.current 136 | current.locale = Locale(identifier: "en_US") 137 | DateBuilder.calendar = current 138 | let startOfNextWeek = NextWeek().firstDay.at(hour: 10, minute: 15).date() 139 | print(startOfNextWeek) 140 | DateBuilder.withLocale(Locale(identifier: "ru_RU")) { 141 | let _startOfNextWeek = NextWeek().firstDay.at(hour: 10, minute: 15).date() 142 | print(_startOfNextWeek) 143 | XCTAssertNotEqual(startOfNextWeek, _startOfNextWeek) 144 | } 145 | let againStartOfNextWeek = NextWeek().firstDay.at(hour: 10, minute: 15).date() 146 | print(againStartOfNextWeek) 147 | XCTAssertEqual(startOfNextWeek, againStartOfNextWeek) 148 | DateBuilder.calendar = .current 149 | } 150 | 151 | func testTimeOfDayComparable() { 152 | let timeOfDay = TimeOfDay(hour: 15, minute: 00) 153 | let lower = TimeOfDay(hour: 8, minute: 00) 154 | let upper = TimeOfDay(hour: 20, minute: 00) 155 | 156 | XCTAssertGreaterThan(timeOfDay, lower) 157 | XCTAssertLessThan(timeOfDay, upper) 158 | } 159 | 160 | static var allTests = [ 161 | ("testExample", testExample), 162 | ] 163 | } 164 | 165 | extension Date { 166 | func _makeComponents() -> DateComponents { 167 | return Calendar.current.dateComponents([.era, .year, .month, .day, .hour, .minute, .second, .weekday, .weekOfYear, .yearForWeekOfYear], from: self) 168 | } 169 | } 170 | 171 | extension DateComponents { 172 | func _makeDate() -> Date { 173 | var copy = self 174 | copy.calendar = .current 175 | return copy.date! 176 | } 177 | } 178 | 179 | struct Account { 180 | let createdAt: Date = .init() 181 | } 182 | 183 | let account = Account() 184 | 185 | func readme() { 186 | 187 | ExactlyAt(account.createdAt) 188 | .addingMinutes(1) 189 | .dateComponents() 190 | 191 | Today() 192 | .addingDays(10) 193 | Tomorrow() 194 | DayOf(account.createdAt) 195 | ExactDay(year: 2021, month: 1, day: 26) 196 | AddingDays(15, to: .today) 197 | AddingDays(15, to: .dayOf(account.createdAt)) 198 | EveryDay(forDays: 100, starting: .tomorrow) 199 | EveryDay(forDays: 100, starting: .dayOf(account.createdAt)) 200 | 201 | ThisWeek() 202 | NextWeek() 203 | WeekOf(account.createdAt) 204 | WeekOf(Today()) // use any `DateBuilder.Day` instance here 205 | AddingWeeks(5, to: .thisWeek) 206 | EveryWeek(forWeeks: 10, starting: .nextWeek) 207 | 208 | ThisWeek() 209 | .addingWeeks(10) 210 | 211 | ThisMonth() 212 | NextMonth() 213 | MonthOf(account.createdAt) 214 | MonthOf(Today()) // use any `DateBuilder.Day` instance here 215 | ExactMonth(year: 2021, month: 03) 216 | AddingMonths(3, to: .thisMonth) 217 | EveryMonth(forMonths: 5, starting: .monthOf(account.createdAt)) 218 | 219 | ThisMonth().addingMonths(5) 220 | ThisMonth().firstDay 221 | ThisMonth().lastDay 222 | ThisMonth().allDays 223 | ThisMonth().first(.saturday) 224 | ThisMonth().weekday(.third, .friday) 225 | 226 | ThisYear() 227 | NextYear() 228 | YearOf(account.createdAt) 229 | YearOf(Tomorrow()) // use any `DateBuilder.Day` instance here 230 | YearOf(NextMonth()) // use any `DateBuilder.Month` instance here 231 | ExactYear(year: 2022) 232 | AddingYears(1, to: ThisYear()) 233 | EveryYear(forYears: 100, starting: .thisYear) 234 | 235 | ThisYear().addingYears(1) 236 | ThisYear().firstMonth 237 | ThisYear().lastMonth 238 | ThisYear().allMonths 239 | 240 | Today().at(hour: 10, minute: 15) 241 | Today().at(hour: 19, minute: 30, second: 30) 242 | Today().at(TimeOfDay(hour: 10, minute: 30, second: 0)) // equivalent to: 243 | Today().at(.time(hour: 10, minute: 30)) 244 | Today().at(.randomTime(from: .time(hour: 10, minute: 15), to: .time(hour: 15, minute: 30))) 245 | 246 | var customCalendar = DateBuilder.calendar 247 | customCalendar.firstWeekday = 6 248 | DateBuilder.calendar = customCalendar 249 | 250 | DateBuilder.withCalendar(customCalendar) { 251 | ThisWeek().firstDay.dateComponents() 252 | } 253 | 254 | let tomorrowMorning = DateBuilder.withTimeZone(TimeZone(identifier: "America/Cancun")!) { 255 | return Tomorrow().at(hour: 9, minute: 15).date() 256 | } 257 | 258 | DateBuilder.withLocale(Locale(identifier: "he_IL")) { 259 | NextWeek() 260 | .weekendStartDay 261 | .at(hour: 7, minute: 00) 262 | .date() // next friday! 263 | } 264 | } 265 | 266 | func readme2() { 267 | NextYear() 268 | .firstMonth.addingMonths(3) 269 | .first(.thursday) 270 | .dateComponents() 271 | } 272 | 273 | func twitter() { 274 | 275 | Today() 276 | .at(hour: 20, minute: 15) 277 | .dateComponents() // year: 2021, month: 1, day: 31, hour: 20, minute: 15 278 | 279 | NextWeek() 280 | .weekday(.saturday) 281 | .at(hour: 18, minute: 50) 282 | .dateComponents() // DateComponents 283 | 284 | EveryWeek(forWeeks: 10, starting: .thisWeek) 285 | .weekendStartDay 286 | .at(hour: 9, minute: 00) 287 | .dates() // [Date] 288 | 289 | ExactlyAt(account.createdAt) 290 | .addingDays(15) 291 | .date() // Date 292 | 293 | WeekOf(account.createdAt) 294 | .addingWeeks(1) 295 | .lastDay 296 | .at(hour: 10, minute: 00) 297 | .dateComponents() // DateComponents 298 | 299 | EveryMonth(forMonths: 12, starting: .thisMonth) 300 | .lastDay 301 | .at(hour: 23, minute: 50) 302 | .dateComponents() // [DateComponents] 303 | 304 | NextYear().addingYears(2) 305 | .firstMonth.addingMonths(3) // April (in Gregorian) 306 | .first(.thursday) 307 | .dateComponents() // year: 2024, month: 4, day: 4 308 | 309 | ExactDay(year: 2020, month: 10, day: 5) 310 | .at(hour: 10, minute: 15) 311 | .date() // Date 312 | 313 | ExactYear(year: 2020) 314 | .lastMonth 315 | .lastDay 316 | .dateComponents() 317 | 318 | EveryWeek(forWeeks: 50, starting: .thisWeek) 319 | .firstDay 320 | .at(hour: 10, minute: 00) 321 | .dateComponents() 322 | 323 | EveryMonth(forMonths: 12, starting: .thisMonth) 324 | .first(.friday) 325 | .at(hour: 20, minute: 15) 326 | .dates() 327 | 328 | let dates = EveryMonth(forMonths: 12, starting: .thisMonth) 329 | .firstDay.addingDays(9) 330 | .at(hour: 20, minute: 00) 331 | .dates() // [Date] 332 | } 333 | #endif 334 | -------------------------------------------------------------------------------- /Sources/DateBuilder/DateBuilder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum DateBuilder { 4 | private static var _calendar = { Calendar.current } 5 | public static var calendar: Calendar { 6 | get { 7 | return _calendar() 8 | } 9 | set { 10 | if newValue == Calendar.current { 11 | _calendar = { Calendar.current } 12 | } else { 13 | _calendar = { newValue } 14 | } 15 | } 16 | } 17 | 18 | public static func withCalendar(_ calendar: Calendar, _ perform: () -> T) -> T { 19 | let backup = _calendar 20 | _calendar = { calendar } 21 | let returnValue = perform() 22 | _calendar = backup 23 | return returnValue 24 | } 25 | 26 | public static func withTimeZone(_ timeZone: TimeZone, _ perform: () -> T) -> T { 27 | var current = calendar 28 | current.timeZone = timeZone 29 | return withCalendar(current, perform) 30 | } 31 | 32 | public static func withLocale(_ locale: Locale, _ perform: () -> T) -> T { 33 | var current = calendar 34 | current.locale = locale 35 | return withCalendar(current, perform) 36 | } 37 | } 38 | 39 | public struct TimeOfDay: Codable, Hashable, Comparable { 40 | public var hour: Int 41 | public var minute: Int 42 | public var second: Int = 0 43 | 44 | public init(hour: Int, minute: Int, second: Int = 0) { 45 | self.hour = hour 46 | self.minute = minute 47 | self.second = second 48 | } 49 | 50 | public static func time(hour: Int, minute: Int, second: Int = 0) -> TimeOfDay { 51 | return TimeOfDay(hour: hour, minute: minute, second: second) 52 | } 53 | 54 | public static func randomTime(from lower: TimeOfDay, to upper: TimeOfDay) -> TimeOfDay { 55 | let reference = Date() 56 | let calendar = DateBuilder.calendar 57 | let lowerDate = calendar.date(bySettingHour: lower.hour, minute: lower.minute, second: lower.second, of: reference)! 58 | let upperDate = calendar.date(bySettingHour: upper.hour, minute: upper.minute, second: upper.second, of: reference)! 59 | assert(lowerDate < upperDate, "make sure 'from' is before 'to'") 60 | let timeIntervalDif = Int(upperDate.timeIntervalSince(lowerDate)) 61 | let randomTimeDiff = Int.random(in: 0 ..< timeIntervalDif) 62 | let randomDate = lowerDate.addingTimeInterval(TimeInterval(randomTimeDiff)) 63 | let timeOfDay = TimeOfDay(date: randomDate, calendar: calendar) 64 | return timeOfDay 65 | } 66 | 67 | public static func < (lhs: TimeOfDay, rhs: TimeOfDay) -> Bool { 68 | guard lhs.hour == rhs.hour else { 69 | return lhs.hour < rhs.hour 70 | } 71 | 72 | guard lhs.minute == rhs.minute else { 73 | return lhs.minute < rhs.minute 74 | } 75 | 76 | return lhs.second < rhs.second 77 | } 78 | } 79 | 80 | extension TimeOfDay { 81 | public init(date: Date, calendar: Calendar = DateBuilder.calendar) { 82 | let components = calendar.dateComponents([.hour, .minute, .second], from: date) 83 | self.init(hour: components.hour ?? 0, minute: components.minute ?? 0, second: components.second ?? 0) 84 | } 85 | } 86 | 87 | extension DateBuilder { 88 | public enum ResolvedDate { 89 | case exact(Foundation.Date) 90 | case components(DateComponents) 91 | 92 | public func dateComponents() -> DateComponents { 93 | switch self { 94 | case .exact(let date): 95 | let components = DateBuilder.calendar.dateComponents([.era, .year, .month, .weekday, .day, .hour, .minute, .second], from: date) 96 | return components 97 | case .components(let components): 98 | return components 99 | } 100 | } 101 | 102 | public func date() -> Date { 103 | switch self { 104 | case .exact(let date): 105 | return date 106 | case .components(let components): 107 | if let date = DateBuilder.calendar.date(from: components) { 108 | return date 109 | } else { 110 | print("UNEXPECTED - INVALID COMPONENTS: \(components)") 111 | assertionFailure("UNEXPECTED - INVALID COMPONENTS: \(components)") 112 | return .distantPast 113 | } 114 | } 115 | } 116 | 117 | public func adding(dateComponents: DateComponents) -> ResolvedDate? { 118 | let added = DateBuilder.calendar.date(byAdding: dateComponents, to: date()) 119 | return added.map(ResolvedDate.exact) 120 | } 121 | 122 | public func addingSeconds(_ seconds: Int) -> ResolvedDate { 123 | let components = DateComponents(second: seconds) 124 | if let date = adding(dateComponents: components) { 125 | return date 126 | } else { 127 | print("UNEXPECTED - INVALID COMPONENTS: \(components)") 128 | assertionFailure("UNEXPECTED - INVALID COMPONENTS: \(components)") 129 | return .exact(.distantPast) 130 | } 131 | } 132 | 133 | public func addingMinutes(_ minutes: Int) -> ResolvedDate { 134 | let components = DateComponents(minute: minutes) 135 | if let date = adding(dateComponents: components) { 136 | return date 137 | } else { 138 | print("UNEXPECTED - INVALID COMPONENTS: \(components)") 139 | assertionFailure("UNEXPECTED - INVALID COMPONENTS: \(components)") 140 | return .exact(.distantPast) 141 | } 142 | } 143 | 144 | public func addingHours(_ hours: Int) -> ResolvedDate { 145 | let components = DateComponents(hour: hours) 146 | if let date = adding(dateComponents: components) { 147 | return date 148 | } else { 149 | print("UNEXPECTED - INVALID COMPONENTS: \(components)") 150 | assertionFailure("UNEXPECTED - INVALID COMPONENTS: \(components)") 151 | return .exact(.distantPast) 152 | } 153 | } 154 | 155 | public func addingDays(_ days: Int) -> ResolvedDate { 156 | let components = DateComponents(day: days) 157 | if let date = adding(dateComponents: components) { 158 | return date 159 | } else { 160 | print("UNEXPECTED - INVALID COMPONENTS: \(components)") 161 | assertionFailure("UNEXPECTED - INVALID COMPONENTS: \(components)") 162 | return .exact(.distantPast) 163 | } 164 | } 165 | 166 | public func addingWeeks(_ weeks: Int) -> ResolvedDate { 167 | let components = DateComponents(weekOfYear: weeks) 168 | if let date = adding(dateComponents: components) { 169 | return date 170 | } else { 171 | print("UNEXPECTED - INVALID COMPONENTS: \(components)") 172 | assertionFailure("UNEXPECTED - INVALID COMPONENTS: \(components)") 173 | return .exact(.distantPast) 174 | } 175 | } 176 | 177 | public func addingMonths(_ months: Int) -> ResolvedDate { 178 | let components = DateComponents(month: months) 179 | if let date = adding(dateComponents: components) { 180 | return date 181 | } else { 182 | print("UNEXPECTED - INVALID COMPONENTS: \(components)") 183 | assertionFailure("UNEXPECTED - INVALID COMPONENTS: \(components)") 184 | return .exact(.distantPast) 185 | } 186 | } 187 | 188 | public func addingYears(_ years: Int) -> ResolvedDate { 189 | let components = DateComponents(year: years) 190 | if let date = adding(dateComponents: components) { 191 | return date 192 | } else { 193 | print("UNEXPECTED - INVALID COMPONENTS: \(components)") 194 | assertionFailure("UNEXPECTED - INVALID COMPONENTS: \(components)") 195 | return .exact(.distantPast) 196 | } 197 | } 198 | } 199 | 200 | public struct Day { 201 | var base: Date 202 | var offset: Int 203 | 204 | public static var today: Day { Today() } 205 | 206 | public static func dayOf(_ date: Date) -> Day { 207 | return DayOf(date) 208 | } 209 | 210 | public static var tomorrow: Day { Tomorrow() } 211 | 212 | fileprivate func finalize() -> Date { 213 | return DateBuilder.calendar.date(byAdding: .day, value: offset, to: base) ?? .distantFuture 214 | } 215 | 216 | public func addingDays(_ days: Int) -> Day { 217 | return Day(base: base, offset: offset + days) 218 | } 219 | 220 | public func dateComponents() -> DateComponents { 221 | let components = finalize()._extract(components: [.era, .year, .month, .day]) 222 | return components 223 | } 224 | 225 | public func at(_ timeOfDay: @autoclosure () -> TimeOfDay) -> ResolvedDate { 226 | let date = finalize() 227 | let timeOfDay = timeOfDay() 228 | let calendar = DateBuilder.calendar 229 | var components = calendar.dateComponents([.year, .month, .day], from: date) 230 | components.hour = timeOfDay.hour 231 | components.minute = timeOfDay.minute 232 | components.second = timeOfDay.second 233 | return .components(components) 234 | } 235 | 236 | public func at(hour: Int, minute: Int, second: Int = 0) -> ResolvedDate { 237 | return at(.init(hour: hour, minute: minute, second: second)) 238 | } 239 | } 240 | } 241 | 242 | extension Sequence where Element == DateBuilder.ResolvedDate { 243 | public func dates() -> [Date] { 244 | return map({ $0.date() }) 245 | } 246 | 247 | public func dateComponents() -> [DateComponents] { 248 | return map({ $0.dateComponents() }) 249 | } 250 | } 251 | 252 | extension Sequence where Element == DateBuilder.Day { 253 | public func addingDays(_ days: Int) -> [DateBuilder.Day] { 254 | return map({ $0.addingDays(days) }) 255 | } 256 | 257 | public func at(_ timeOfDay: @autoclosure () -> TimeOfDay) -> [DateBuilder.ResolvedDate] { 258 | return map { $0.at(timeOfDay()) } 259 | } 260 | 261 | public func at(hour: Int, minute: Int, second: Int = 0) -> [DateBuilder.ResolvedDate] { 262 | at(.init(hour: hour, minute: minute, second: second)) 263 | } 264 | 265 | public func dateComponents() -> [DateComponents] { 266 | return map({ $0.dateComponents() }) 267 | } 268 | } 269 | 270 | public func ExactDay(year: Int, month: Int, day: Int) -> DateBuilder.Day { 271 | let calendar = DateBuilder.calendar 272 | let components = DateComponents(year: year, month: month, day: day) 273 | let date = calendar.date(from: components) ?? .distantPast 274 | return .init(base: date.addingTimeInterval(5.0), offset: 0) 275 | } 276 | 277 | public func Today() -> DateBuilder.Day { 278 | return DayOf(Date()) 279 | } 280 | 281 | public func Tomorrow() -> DateBuilder.Day { 282 | return Today().addingDays(1) 283 | } 284 | 285 | public func DayOf(_ date: Date) -> DateBuilder.Day { 286 | return .init(base: date, offset: 0) 287 | } 288 | 289 | public func AddingDays(_ days: Int, to day: DateBuilder.Day) -> DateBuilder.Day { 290 | return day.addingDays(days) 291 | } 292 | 293 | public func DayOf(_ date: Date, at timeOfDay: TimeOfDay) -> DateBuilder.ResolvedDate { 294 | return DayOf(date).at(timeOfDay) 295 | } 296 | 297 | public func Today(at timeOfDay: TimeOfDay) -> DateBuilder.ResolvedDate { 298 | let today = Date() 299 | return DayOf(today, at: timeOfDay) 300 | } 301 | 302 | public func Tomorrow(at timeOfDay: TimeOfDay) -> DateBuilder.ResolvedDate { 303 | return AfterToday(days: 1, at: timeOfDay) 304 | } 305 | 306 | public func AfterToday(days: Int, at timeOfDay: TimeOfDay) -> DateBuilder.ResolvedDate { 307 | let today = Date() 308 | return After(dayOf: today, days: days, at: timeOfDay) 309 | } 310 | 311 | public func After(dayOf date: Date, days: Int, at timeOfDay: TimeOfDay) -> DateBuilder.ResolvedDate { 312 | return AddingDays(days, to: .dayOf(date)).at(timeOfDay) 313 | } 314 | 315 | public struct DelayDistribution { 316 | public init(delayForNumber: @escaping (_ number: Int, _ totalCount: Int) -> Int) { 317 | self.delayForNumber = delayForNumber 318 | } 319 | 320 | public let delayForNumber: (_ number: Int, _ totalCount: Int) -> Int 321 | 322 | public static let normal = DelayDistribution(delayForNumber: { number, _ in number }) 323 | 324 | public static let optimized = DelayDistribution { number, totalCount in 325 | let breakpoint = totalCount / 2 326 | if number <= breakpoint { 327 | return number 328 | } else { 329 | let shift = number - breakpoint 330 | let exponent = shift * shift 331 | return number + exponent 332 | } 333 | } 334 | 335 | public func generate(count: Int, start: Unit, addDelay: (Unit, Int) -> Unit) -> [Unit] { 336 | precondition(count >= 0) 337 | return (0 ..< count) 338 | .lazy 339 | .map({ self.delayForNumber($0, Int(count)) }) 340 | .map({ addDelay(start, $0) }) 341 | } 342 | } 343 | 344 | public func EveryDay(forDays nextDays: Int, starting startDay: DateBuilder.Day, distribution: DelayDistribution = .normal) -> [DateBuilder.Day] { 345 | return distribution.generate(count: nextDays, start: startDay, addDelay: { $0.addingDays($1) }) 346 | } 347 | 348 | public func EveryDay(starting day: DateBuilder.Day, forDays days: Int, at timeOfDay: @autoclosure () -> TimeOfDay) -> [DateBuilder.ResolvedDate] { 349 | return EveryDay(forDays: days, starting: day).at(timeOfDay()) 350 | } 351 | 352 | extension DateBuilder { 353 | public struct Week { 354 | var base: (yearForWeekOfYear: Int, weekOfYear: Int) 355 | var offset: Int 356 | 357 | public static var thisWeek: Week { 358 | return weekOf(Date()) 359 | } 360 | 361 | public static var nextWeek: Week { 362 | return thisWeek.addingWeeks(1) 363 | } 364 | 365 | public static func weekOf(_ date: Date) -> Week { 366 | return Week(base: (yearForWeekOfYear: date._extract(.yearForWeekOfYear), weekOfYear: date._extract(.weekOfYear)), offset: 0) 367 | } 368 | 369 | private var baseDateComponents: DateComponents { 370 | return DateComponents(weekOfYear: base.weekOfYear, yearForWeekOfYear: base.yearForWeekOfYear) 371 | } 372 | 373 | fileprivate func finalize() -> Date { 374 | let base = DateBuilder.calendar.date(from: baseDateComponents) ?? .distantFuture 375 | return DateBuilder.calendar.date(byAdding: .weekOfYear, value: offset, to: base) ?? .distantFuture 376 | } 377 | 378 | public func addingWeeks(_ weeks: Int) -> Week { 379 | return Week(base: self.base, offset: self.offset + weeks) 380 | } 381 | 382 | public var firstDay: Day { 383 | return Day(base: finalize(), offset: 0) 384 | } 385 | 386 | public func weekday(_ weekday: GregorianWeekday) -> Day { 387 | let finalized = finalize() 388 | var components = DateBuilder.calendar.dateComponents([.weekOfYear, .yearForWeekOfYear], from: finalized) 389 | components.weekday = weekday.rawValue 390 | let date = DateBuilder.calendar.date(from: components) ?? .distantFuture 391 | return DayOf(date) 392 | } 393 | 394 | public struct GregorianWeekday: ExpressibleByIntegerLiteral { 395 | public var rawValue: Int 396 | 397 | public static let sunday: GregorianWeekday = 1 398 | public static let monday: GregorianWeekday = 2 399 | public static let tuesday: GregorianWeekday = 3 400 | public static let wednesday: GregorianWeekday = 4 401 | public static let thursday: GregorianWeekday = 5 402 | public static let friday: GregorianWeekday = 6 403 | public static let saturday: GregorianWeekday = 7 404 | 405 | public typealias IntegerLiteralType = Int 406 | 407 | public init(rawValue: Int) { 408 | self.rawValue = rawValue 409 | } 410 | 411 | public init(integerLiteral value: Int) { 412 | self.rawValue = value 413 | } 414 | } 415 | 416 | public var weekendStartDay: Day { 417 | guard var weekend = DateBuilder.calendar.nextWeekend(startingAfter: finalize()) else { 418 | return lastDay 419 | } 420 | weekend.start.addTimeInterval(5) 421 | return DayOf(weekend.start) 422 | } 423 | 424 | public var weekendEndDay: Day { 425 | guard var weekend = DateBuilder.calendar.nextWeekend(startingAfter: finalize()) else { 426 | return lastDay 427 | } 428 | weekend.end.addTimeInterval(-5) 429 | return DayOf(weekend.end) 430 | } 431 | 432 | public var lastDay: Day { 433 | guard var interval = DateBuilder.calendar.dateInterval(of: .weekOfYear, for: finalize()) else { 434 | return firstDay 435 | } 436 | // otherwise the end of the interval will be midnight of the first day of next week 437 | interval.end.addTimeInterval(-5) 438 | return Day(base: interval.end, offset: 0) 439 | } 440 | 441 | public var allDays: [Day] { 442 | guard var interval = DateBuilder.calendar.dateInterval(of: .weekOfYear, for: finalize()) else { 443 | return [] 444 | } 445 | interval.start.addTimeInterval(5) 446 | interval.end.addTimeInterval(-5) 447 | var current = interval.start 448 | var all: [Day] = [] 449 | while interval.contains(current) { 450 | all.append(Day(base: current, offset: 0)) 451 | current = DateBuilder.calendar.date(byAdding: .day, value: 1, to: current) ?? .distantFuture 452 | } 453 | return all 454 | } 455 | } 456 | } 457 | 458 | extension Sequence where Element == DateBuilder.Week { 459 | public func addingWeeks(_ weeks: Int) -> [DateBuilder.Week] { 460 | return map({ $0.addingWeeks(weeks) }) 461 | } 462 | 463 | public var firstDay: [DateBuilder.Day] { 464 | return map(\.firstDay) 465 | } 466 | 467 | public var lastDay: [DateBuilder.Day] { 468 | return map(\.lastDay) 469 | } 470 | 471 | public func weekday(_ weekday: DateBuilder.Week.GregorianWeekday) -> [DateBuilder.Day] { 472 | return map({ $0.weekday(weekday) }) 473 | } 474 | 475 | public var weekendStartDay: [DateBuilder.Day] { 476 | return map(\.weekendStartDay) 477 | } 478 | 479 | public var weekendEndDay: [DateBuilder.Day] { 480 | return map(\.weekendEndDay) 481 | } 482 | } 483 | 484 | public func ThisWeek() -> DateBuilder.Week { 485 | return .thisWeek 486 | } 487 | 488 | public func NextWeek() -> DateBuilder.Week { 489 | return .nextWeek 490 | } 491 | 492 | public func WeekOf(_ date: Date) -> DateBuilder.Week { 493 | return .weekOf(date) 494 | } 495 | 496 | public func WeekOf(_ day: DateBuilder.Day) -> DateBuilder.Week { 497 | return .weekOf(day.finalize()) 498 | } 499 | 500 | public func AddingWeeks(_ weeks: Int, to week: DateBuilder.Week) -> DateBuilder.Week { 501 | return week.addingWeeks(weeks) 502 | } 503 | 504 | public func EveryWeek(forWeeks nextWeeks: Int, starting startWeek: DateBuilder.Week, distribution: DelayDistribution = .normal) -> [DateBuilder.Week] { 505 | return distribution.generate(count: nextWeeks, start: startWeek, addDelay: { $0.addingWeeks($1) }) 506 | } 507 | 508 | extension DateBuilder { 509 | public struct Month { 510 | var base: (year: Int, month: Int) 511 | var offset: Int 512 | 513 | public static var thisMonth: Month { 514 | return monthOf(Date()) 515 | } 516 | 517 | public static var nextMonth: Month { 518 | return thisMonth.addingMonths(1) 519 | } 520 | 521 | public static func monthOf(_ date: Date) -> Month { 522 | return Month(base: (year: date._extract(.year), month: date._extract(.month)), offset: 0) 523 | } 524 | 525 | private var baseDateComponents: DateComponents { 526 | return DateComponents(year: base.year, month: base.month) 527 | } 528 | 529 | fileprivate func finalize() -> Date { 530 | let base = DateBuilder.calendar.date(from: baseDateComponents) ?? .distantFuture 531 | return DateBuilder.calendar.date(byAdding: .month, value: offset, to: base) ?? .distantFuture 532 | } 533 | 534 | public func addingMonths(_ months: Int) -> Month { 535 | return Month(base: self.base, offset: offset + months) 536 | } 537 | 538 | public var firstDay: Day { 539 | return Day(base: finalize(), offset: 0) 540 | } 541 | 542 | public var lastDay: Day { 543 | guard var interval = DateBuilder.calendar.dateInterval(of: .month, for: finalize()) else { 544 | return firstDay 545 | } 546 | // otherwise the end of the interval will be midnight of the first day of next month 547 | interval.end.addTimeInterval(-5) 548 | return Day(base: interval.end, offset: 0) 549 | } 550 | 551 | public func weekday(_ ordinal: Ordinal, _ weekday: Week.GregorianWeekday) -> Day? { 552 | var components = DateBuilder.calendar.dateComponents([.year, .month], from: finalize()) 553 | components.weekday = weekday.rawValue 554 | components.weekdayOrdinal = ordinal.rawValue 555 | let date = DateBuilder.calendar.date(from: components) 556 | return date.map(DayOf) 557 | } 558 | 559 | public func first(_ weekday: Week.GregorianWeekday) -> Day { 560 | return self.weekday(.first, weekday) ?? DayOf(.distantFuture) 561 | } 562 | 563 | public var allDays: [Day] { 564 | guard var interval = DateBuilder.calendar.dateInterval(of: .month, for: finalize()) else { 565 | return [] 566 | } 567 | interval.start.addTimeInterval(5) 568 | interval.end.addTimeInterval(-5) 569 | var current = interval.start 570 | var all: [Day] = [] 571 | while interval.contains(current) { 572 | all.append(Day(base: current, offset: 0)) 573 | current = DateBuilder.calendar.date(byAdding: .day, value: 1, to: current) ?? .distantFuture 574 | } 575 | return all 576 | } 577 | } 578 | 579 | public struct Ordinal { 580 | public var rawValue: Int 581 | 582 | public static let first = Ordinal(rawValue: 1) 583 | public static let second = Ordinal(rawValue: 2) 584 | public static let third = Ordinal(rawValue: 3) 585 | public static let fourth = Ordinal(rawValue: 4) 586 | public static let fifth = Ordinal(rawValue: 5) 587 | 588 | public init(rawValue: Int) { 589 | self.rawValue = rawValue 590 | } 591 | } 592 | } 593 | 594 | extension Sequence where Element == DateBuilder.Month { 595 | public func addingMonths(_ months: Int) -> [DateBuilder.Month] { 596 | return map({ $0.addingMonths(months) }) 597 | } 598 | 599 | public var firstDay: [DateBuilder.Day] { 600 | return map({ $0.firstDay }) 601 | } 602 | 603 | public var lastDay: [DateBuilder.Day] { 604 | return map({ $0.lastDay }) 605 | } 606 | 607 | public func weekday(_ ordinal: DateBuilder.Ordinal, _ weekday: DateBuilder.Week.GregorianWeekday) -> [DateBuilder.Day] { 608 | return compactMap({ $0.weekday(ordinal, weekday) }) 609 | } 610 | 611 | public func first(_ weekday: DateBuilder.Week.GregorianWeekday) -> [DateBuilder.Day] { 612 | return map({ $0.first(weekday) }) 613 | } 614 | } 615 | 616 | public func ExactMonth(year: Int, month: Int) -> DateBuilder.Month { 617 | return .init(base: (year: year, month: month), offset: 0) 618 | } 619 | 620 | public func ThisMonth() -> DateBuilder.Month { 621 | return .thisMonth 622 | } 623 | 624 | public func NextMonth() -> DateBuilder.Month { 625 | return .nextMonth 626 | } 627 | 628 | public func MonthOf(_ date: Date) -> DateBuilder.Month { 629 | return .monthOf(date) 630 | } 631 | 632 | public func MonthOf(_ day: DateBuilder.Day) -> DateBuilder.Month { 633 | return .monthOf(day.finalize()) 634 | } 635 | 636 | public func AddingMonths(_ months: Int, to month: DateBuilder.Month) -> DateBuilder.Month { 637 | return month.addingMonths(months) 638 | } 639 | 640 | public func EveryMonth(forMonths nextMonths: Int, starting startMonth: DateBuilder.Month, distribution: DelayDistribution = .normal) -> [DateBuilder.Month] { 641 | return distribution.generate(count: nextMonths, start: startMonth, addDelay: { $0.addingMonths($1) }) 642 | } 643 | 644 | extension DateBuilder { 645 | public struct Year { 646 | var baseYear: Int 647 | var offset: Int 648 | 649 | public static var thisYear: Year { 650 | return yearOf(Date()) 651 | } 652 | 653 | public static var nextYear: Year { 654 | return thisYear.addingYears(1) 655 | } 656 | 657 | public static func yearOf(_ date: Date) -> Year { 658 | let y = Year(baseYear: date._extract(.year), offset: 0) 659 | return y 660 | } 661 | 662 | private var baseDateComponents: DateComponents { 663 | return DateComponents(year: baseYear) 664 | } 665 | 666 | fileprivate func finalize() -> Date { 667 | let base = DateBuilder.calendar.date(from: baseDateComponents) ?? .distantFuture 668 | return DateBuilder.calendar.date(byAdding: .year, value: offset, to: base) ?? .distantFuture 669 | } 670 | 671 | public func addingYears(_ years: Int) -> Year { 672 | return Year(baseYear: baseYear, offset: offset + years) 673 | } 674 | 675 | public var firstMonth: Month { 676 | let month = Month.monthOf(finalize()) 677 | return month 678 | } 679 | 680 | public var lastMonth: Month { 681 | guard var interval = DateBuilder.calendar.dateInterval(of: .year, for: finalize()) else { 682 | return firstMonth 683 | } 684 | // otherwise the end of the interval will be midnight of the first day of next month 685 | interval.end.addTimeInterval(-5) 686 | return Month.monthOf(interval.end) 687 | } 688 | 689 | public var allMonths: [Month] { 690 | guard var interval = DateBuilder.calendar.dateInterval(of: .year, for: finalize()) else { 691 | return [] 692 | } 693 | interval.start.addTimeInterval(5) 694 | interval.end.addTimeInterval(-5) 695 | var current = interval.start 696 | var all: [Month] = [] 697 | while interval.contains(current) { 698 | all.append(Month.monthOf(current)) 699 | current = DateBuilder.calendar.date(byAdding: .month, value: 1, to: current) ?? .distantFuture 700 | } 701 | return all 702 | } 703 | } 704 | } 705 | 706 | extension Sequence where Element == DateBuilder.Year { 707 | public func addingYears(_ years: Int) -> [DateBuilder.Year] { 708 | return map({ $0.addingYears(years) }) 709 | } 710 | 711 | public var firstMonth: [DateBuilder.Month] { 712 | return map({ $0.firstMonth }) 713 | } 714 | 715 | public var lastMonth: [DateBuilder.Month] { 716 | return map({ $0.lastMonth }) 717 | } 718 | } 719 | 720 | public func ExactYear(year: Int) -> DateBuilder.Year { 721 | return .init(baseYear: year, offset: 0) 722 | } 723 | 724 | public func ThisYear() -> DateBuilder.Year { 725 | return .thisYear 726 | } 727 | 728 | public func NextYear() -> DateBuilder.Year { 729 | return .nextYear 730 | } 731 | 732 | public func YearOf(_ date: Date) -> DateBuilder.Year { 733 | return .yearOf(date) 734 | } 735 | 736 | public func YearOf(_ day: DateBuilder.Day) -> DateBuilder.Year { 737 | return .yearOf(day.finalize()) 738 | } 739 | 740 | public func YearOf(_ month: DateBuilder.Month) -> DateBuilder.Year { 741 | return YearOf(month.firstDay) 742 | } 743 | 744 | public func AddingYears(_ years: Int, to year: DateBuilder.Year) -> DateBuilder.Year { 745 | return year.addingYears(years) 746 | } 747 | 748 | public func EveryYear(forYears nextYears: Int, starting startYear: DateBuilder.Year, distribution: DelayDistribution = .normal) -> [DateBuilder.Year] { 749 | return distribution.generate(count: nextYears, start: startYear, addDelay: { $0.addingYears($1) }) 750 | } 751 | 752 | public func ExactlyAt(_ date: Date) -> DateBuilder.ResolvedDate { 753 | return .exact(date) 754 | } 755 | 756 | fileprivate extension Date { 757 | func _extract(_ component: Calendar.Component, calendar: Calendar = DateBuilder.calendar) -> Int { 758 | return calendar.component(component, from: self) 759 | } 760 | 761 | func _extract(components: Set, calendar: Calendar = DateBuilder.calendar) -> DateComponents { 762 | return calendar.dateComponents(components, from: self) 763 | } 764 | } 765 | --------------------------------------------------------------------------------