@@ -157,6 +157,27 @@ final class RelativeDateTests: XCTestCase {
157157 XCTAssertEqual ( in30HoursAbsolute, Date . from ( relativeDate: in30HoursRelative) )
158158 }
159159
160+ func testDateFromRelativeQuarter( ) throws {
161+ // Jan 12, 2026 is in Q1 2026
162+ // -1 quarter = -3 months → Oct 12, 2025 (Q4 2025)
163+ // Beginning of Q4 2025 = Oct 1, 2025
164+ let beginningOfLastQuarterRelative = RelativeDate ( . beginning, of: . quarter, adding: - 1 )
165+ let beginningOfLastQuarterAbsolute = Date ( iso8601String: " 2025-10-01T00:00:00.000Z " ) !
166+
167+ XCTAssertEqual ( beginningOfLastQuarterAbsolute, Date . from ( relativeDate: beginningOfLastQuarterRelative, originDate: Date ( iso8601String: " 2026-01-12T00:00:00.000Z " ) !) )
168+ }
169+
170+ func testDateFromRelativeQuarterOverYear( ) throws {
171+ // Jan 12, 2026 is in Q1 2026
172+ // -2 quarters = -6 months → Jul 12, 2025 (Q3 2025)
173+ // Beginning of Q3 2025 = Jul 1, 2025
174+ let beginningOfLastQuarterRelative = RelativeDate ( . beginning, of: . quarter, adding: - 2 )
175+ let beginningOfLastQuarterAbsolute = Date ( iso8601String: " 2025-07-01T00:00:00.000Z " ) !
176+
177+ XCTAssertEqual ( beginningOfLastQuarterAbsolute, Date . from ( relativeDate: beginningOfLastQuarterRelative, originDate: Date ( iso8601String: " 2026-01-12T00:00:00.000Z " ) !) )
178+ }
179+
180+
160181 func testWeekBeginsOnMonday( ) throws {
161182 let beginningOfNextWeekRelative = RelativeDate ( . beginning, of: . week, adding: 1 )
162183 let beginningOfNextWeekAbsolute = Date . from ( relativeDate: beginningOfNextWeekRelative)
@@ -167,4 +188,107 @@ final class RelativeDateTests: XCTestCase {
167188
168189 XCTAssertEqual ( " Monday " , weekDay)
169190 }
191+
192+ // MARK: - Comprehensive Quarter Tests
193+
194+ func testQuarterEndCalculation( ) throws {
195+ // Jan 12, 2026 is in Q1 2026
196+ // End of current quarter (Q1 2026) = Mar 31, 2026 23:59:59
197+ let endOfCurrentQuarterRelative = RelativeDate ( . end, of: . quarter, adding: 0 )
198+ let endOfCurrentQuarterAbsolute = Date . from ( relativeDate: endOfCurrentQuarterRelative, originDate: Date ( iso8601String: " 2026-01-12T00:00:00.000Z " ) !)
199+
200+ // Use UTC calendar for timezone-safe comparison
201+ var calendar = Calendar ( identifier: . gregorian)
202+ calendar. timeZone = TimeZone ( identifier: " UTC " ) !
203+ let components = calendar. dateComponents ( [ . year, . month, . day] , from: endOfCurrentQuarterAbsolute)
204+ XCTAssertEqual ( components. year, 2026 )
205+ XCTAssertEqual ( components. month, 3 )
206+ XCTAssertEqual ( components. day, 31 )
207+ }
208+
209+ func testQuarterAddingPositiveOffset( ) throws {
210+ // Jan 12, 2026 is in Q1 2026
211+ // +1 quarter = +3 months → Apr 12, 2026 (Q2 2026)
212+ // Beginning of Q2 2026 = Apr 1, 2026
213+ let beginningOfNextQuarterRelative = RelativeDate ( . beginning, of: . quarter, adding: 1 )
214+ let beginningOfNextQuarterAbsolute = Date ( iso8601String: " 2026-04-01T00:00:00.000Z " ) !
215+
216+ XCTAssertEqual ( beginningOfNextQuarterAbsolute, Date . from ( relativeDate: beginningOfNextQuarterRelative, originDate: Date ( iso8601String: " 2026-01-12T00:00:00.000Z " ) !) )
217+ }
218+
219+ func testQuarterAddingMultiplePositiveOffsets( ) throws {
220+ // Jan 12, 2026 is in Q1 2026
221+ // +4 quarters = +12 months → Jan 12, 2027 (Q1 2027)
222+ // Beginning of Q1 2027 = Jan 1, 2027
223+ let beginningOfFourQuartersAheadRelative = RelativeDate ( . beginning, of: . quarter, adding: 4 )
224+ let beginningOfFourQuartersAheadAbsolute = Date ( iso8601String: " 2027-01-01T00:00:00.000Z " ) !
225+
226+ XCTAssertEqual ( beginningOfFourQuartersAheadAbsolute, Date . from ( relativeDate: beginningOfFourQuartersAheadRelative, originDate: Date ( iso8601String: " 2026-01-12T00:00:00.000Z " ) !) )
227+ }
228+
229+ func testQuarterCurrentQuarter( ) throws {
230+ // Jan 12, 2026 is in Q1 2026
231+ // 0 quarters offset = stay in Q1 2026
232+ // Beginning of Q1 2026 = Jan 1, 2026
233+ let beginningOfCurrentQuarterRelative = RelativeDate ( . beginning, of: . quarter, adding: 0 )
234+ let beginningOfCurrentQuarterAbsolute = Date ( iso8601String: " 2026-01-01T00:00:00.000Z " ) !
235+
236+ XCTAssertEqual ( beginningOfCurrentQuarterAbsolute, Date . from ( relativeDate: beginningOfCurrentQuarterRelative, originDate: Date ( iso8601String: " 2026-01-12T00:00:00.000Z " ) !) )
237+ }
238+
239+ func testQuarterFromMiddleOfQuarter( ) throws {
240+ // May 15, 2026 is in Q2 2026
241+ // -1 quarter = -3 months → Feb 15, 2026 (Q1 2026)
242+ // Beginning of Q1 2026 = Jan 1, 2026
243+ let beginningOfPreviousQuarterRelative = RelativeDate ( . beginning, of: . quarter, adding: - 1 )
244+ let beginningOfPreviousQuarterAbsolute = Date ( iso8601String: " 2026-01-01T00:00:00.000Z " ) !
245+
246+ XCTAssertEqual ( beginningOfPreviousQuarterAbsolute, Date . from ( relativeDate: beginningOfPreviousQuarterRelative, originDate: Date ( iso8601String: " 2026-05-15T00:00:00.000Z " ) !) )
247+ }
248+
249+ func testQuarterFromEndOfQuarter( ) throws {
250+ // Mar 31, 2026 is in Q1 2026
251+ // -1 quarter = -3 months → Dec 31, 2025 (Q4 2025)
252+ // Beginning of Q4 2025 = Oct 1, 2025
253+ let beginningOfPreviousQuarterRelative = RelativeDate ( . beginning, of: . quarter, adding: - 1 )
254+ let beginningOfPreviousQuarterAbsolute = Date ( iso8601String: " 2025-10-01T00:00:00.000Z " ) !
255+
256+ XCTAssertEqual ( beginningOfPreviousQuarterAbsolute, Date . from ( relativeDate: beginningOfPreviousQuarterRelative, originDate: Date ( iso8601String: " 2026-03-31T00:00:00.000Z " ) !) )
257+ }
258+
259+ func testQuarterCrossingMultipleYears( ) throws {
260+ // Jan 12, 2026 is in Q1 2026
261+ // -5 quarters = -15 months → Oct 12, 2024 (Q4 2024)
262+ // Beginning of Q4 2024 = Oct 1, 2024
263+ let beginningOfFiveQuartersBackRelative = RelativeDate ( . beginning, of: . quarter, adding: - 5 )
264+ let beginningOfFiveQuartersBackAbsolute = Date ( iso8601String: " 2024-10-01T00:00:00.000Z " ) !
265+
266+ XCTAssertEqual ( beginningOfFiveQuartersBackAbsolute, Date . from ( relativeDate: beginningOfFiveQuartersBackRelative, originDate: Date ( iso8601String: " 2026-01-12T00:00:00.000Z " ) !) )
267+ }
268+
269+ func testQuarterEndCrossingYear( ) throws {
270+ // Jan 12, 2026 is in Q1 2026
271+ // -1 quarter = -3 months → Oct 12, 2025 (Q4 2025)
272+ // End of Q4 2025 = Dec 31, 2025 23:59:59
273+ let endOfPreviousQuarterRelative = RelativeDate ( . end, of: . quarter, adding: - 1 )
274+ let endOfPreviousQuarterAbsolute = Date . from ( relativeDate: endOfPreviousQuarterRelative, originDate: Date ( iso8601String: " 2026-01-12T00:00:00.000Z " ) !)
275+
276+ // Use UTC calendar for timezone-safe comparison
277+ var calendar = Calendar ( identifier: . gregorian)
278+ calendar. timeZone = TimeZone ( identifier: " UTC " ) !
279+ let components = calendar. dateComponents ( [ . year, . month, . day] , from: endOfPreviousQuarterAbsolute)
280+ XCTAssertEqual ( components. year, 2025 )
281+ XCTAssertEqual ( components. month, 12 )
282+ XCTAssertEqual ( components. day, 31 )
283+ }
284+
285+ func testQuarterFromQ3( ) throws {
286+ // Aug 15, 2025 is in Q3 2025
287+ // -2 quarters = -6 months → Feb 15, 2025 (Q1 2025)
288+ // Beginning of Q1 2025 = Jan 1, 2025
289+ let beginningOfTwoQuartersBackRelative = RelativeDate ( . beginning, of: . quarter, adding: - 2 )
290+ let beginningOfTwoQuartersBackAbsolute = Date ( iso8601String: " 2025-01-01T00:00:00.000Z " ) !
291+
292+ XCTAssertEqual ( beginningOfTwoQuartersBackAbsolute, Date . from ( relativeDate: beginningOfTwoQuartersBackRelative, originDate: Date ( iso8601String: " 2025-08-15T00:00:00.000Z " ) !) )
293+ }
170294}
0 commit comments