├── .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: [DateComponents - 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 |
--------------------------------------------------------------------------------