diff --git a/Sources/SkipFoundation/Calendar.swift b/Sources/SkipFoundation/Calendar.swift index a2fa952..0a965d8 100644 --- a/Sources/SkipFoundation/Calendar.swift +++ b/Sources/SkipFoundation/Calendar.swift @@ -91,9 +91,13 @@ public struct Calendar : Hashable, Codable, CustomStringConvertible { return platformValue as? java.util.GregorianCalendar } - @available(*, unavailable) public var firstWeekday: Int { - fatalError() + get { + return platformValue.getFirstDayOfWeek() + } + set { + platformValue.setFirstDayOfWeek(newValue) + } } @available(*, unavailable) @@ -196,39 +200,188 @@ public struct Calendar : Hashable, Codable, CustomStringConvertible { return dateFormatSymbols.getAmPmStrings()[1] } - @available(*, unavailable) public func minimumRange(of component: Calendar.Component) -> Range? { - fatalError() + let platformCal = platformValue.clone() as java.util.Calendar + + switch component { + case .year: + // Year typically starts at 1 and has no defined maximum. + return 1.. Range? { - fatalError() + let platformCal = platformValue.clone() as java.util.Calendar + switch component { + case .day: + // Maximum number of days in a month can vary (e.g., 28, 29, 30, or 31 days) + return platformCal.getMinimum(java.util.Calendar.DATE)..<(platformCal.getMaximum(java.util.Calendar.DATE) + 1) + case .weekOfMonth, .weekOfYear: + // Not supported yet... + fatalError() + default: + // Maximum range is usually the same logic as minimum but could differ in some cases. + return minimumRange(of: component) + } } - @available(*, unavailable) + public func range(of smaller: Calendar.Component, in larger: Calendar.Component, for date: Date) -> Range? { - fatalError() + let platformCal = platformValue.clone() as java.util.Calendar + platformCal.time = date.platformValue + + switch larger { + case .month: + if smaller == .day { + // Range of days in the current month + let numDays = platformCal.getActualMaximum(java.util.Calendar.DAY_OF_MONTH) + return 1.. Bool { - fatalError() + let platformCal = platformValue.clone() as java.util.Calendar + platformCal.time = date.platformValue + + switch component { + case .day: + platformCal.set(java.util.Calendar.HOUR_OF_DAY, 0) + platformCal.set(java.util.Calendar.MINUTE, 0) + platformCal.set(java.util.Calendar.SECOND, 0) + platformCal.set(java.util.Calendar.MILLISECOND, 0) + start = Date(platformValue: platformCal.time) + interval = TimeInterval(24 * 60 * 60) + return true + case .month: + platformCal.set(java.util.Calendar.DAY_OF_MONTH, 1) + platformCal.set(java.util.Calendar.HOUR_OF_DAY, 0) + platformCal.set(java.util.Calendar.MINUTE, 0) + platformCal.set(java.util.Calendar.SECOND, 0) + platformCal.set(java.util.Calendar.MILLISECOND, 0) + start = Date(platformValue: platformCal.time) + let numberOfDays = platformCal.getActualMaximum(java.util.Calendar.DAY_OF_MONTH) + interval = TimeInterval(numberOfDays) * TimeInterval(24 * 60 * 60) + return true + case .weekOfMonth, .weekOfYear: + platformCal.set(java.util.Calendar.DAY_OF_WEEK, platformCal.firstDayOfWeek) + platformCal.set(java.util.Calendar.HOUR_OF_DAY, 0) + platformCal.set(java.util.Calendar.MINUTE, 0) + platformCal.set(java.util.Calendar.SECOND, 0) + platformCal.set(java.util.Calendar.MILLISECOND, 0) + start = Date(platformValue: platformCal.time) + interval = TimeInterval(7 * 24 * 60 * 60) + return true + case .quarter: + let currentMonth = platformCal.get(java.util.Calendar.MONTH) + let quarterStartMonth = (currentMonth / 3) * 3 // Find the first month of the current quarter + platformCal.set(java.util.Calendar.MONTH, quarterStartMonth) + platformCal.set(java.util.Calendar.DAY_OF_MONTH, 1) + platformCal.set(java.util.Calendar.HOUR_OF_DAY, 0) + platformCal.set(java.util.Calendar.MINUTE, 0) + platformCal.set(java.util.Calendar.SECOND, 0) + platformCal.set(java.util.Calendar.MILLISECOND, 0) + start = Date(platformValue: platformCal.time) + interval = TimeInterval(platformCal.getActualMaximum(java.util.Calendar.DAY_OF_MONTH)) * TimeInterval(24 * 60 * 60 * 3) + return true + default: + return false + } } - @available(*, unavailable) public func dateInterval(of component: Calendar.Component, for date: Date) -> DateInterval? { - fatalError() + var start = Date() + var interval: TimeInterval = 0 + if dateInterval(of: component, start: &start, interval: &interval, for: date) { + return DateInterval(start: start, duration: interval) + } + return nil } - @available(*, unavailable) public func ordinality(of smaller: Calendar.Component, in larger: Calendar.Component, for date: Date) -> Int? { - fatalError() + let platformCal = platformValue.clone() as java.util.Calendar + platformCal.time = date.platformValue + + switch larger { + case .year: + if smaller == .day { + return platformCal.get(java.util.Calendar.DAY_OF_YEAR) + } else if smaller == .weekOfYear { + return platformCal.get(java.util.Calendar.WEEK_OF_YEAR) + } + case .month: + if smaller == .day { + return platformCal.get(java.util.Calendar.DAY_OF_MONTH) + } else if smaller == .weekOfMonth { + return platformCal.get(java.util.Calendar.WEEK_OF_MONTH) + } + default: + return nil + } + return nil } public func date(from components: DateComponents) -> Date? { - // TODO: Need to set `this` calendar in the components.calendar - return Date(platformValue: components.createCalendarComponents(timeZone: self.timeZone).getTime()) + var localComponents = components + localComponents.calendar = self + return Date(platformValue: localComponents.createCalendarComponents(timeZone: self.timeZone).getTime()) } public func dateComponents(in zone: TimeZone? = nil, from date: Date) -> DateComponents { @@ -267,29 +420,69 @@ public struct Calendar : Hashable, Codable, CustomStringConvertible { return dateComponents([component], from: date).value(for: component) ?? 0 } - @available(*, unavailable) public func startOfDay(for date: Date) -> Date { - fatalError() + // Clone the calendar to avoid mutating the original + let platformCal = platformValue.clone() as java.util.Calendar + platformCal.time = date.platformValue + + // Set the time components to the start of the day + platformCal.set(java.util.Calendar.HOUR_OF_DAY, 0) + platformCal.set(java.util.Calendar.MINUTE, 0) + platformCal.set(java.util.Calendar.SECOND, 0) + platformCal.set(java.util.Calendar.MILLISECOND, 0) + + // Return the new Date representing the start of the day + return Date(platformValue: platformCal.time) } - @available(*, unavailable) public func compare(_ date1: Date, to date2: Date, toGranularity component: Calendar.Component) -> ComparisonResult { - fatalError() + let platformCal1 = platformValue.clone() as java.util.Calendar + let platformCal2 = platformValue.clone() as java.util.Calendar + + platformCal1.time = date1.platformValue + platformCal2.time = date2.platformValue + + switch component { + case .year: + let year1 = platformCal1.get(java.util.Calendar.YEAR) + let year2 = platformCal2.get(java.util.Calendar.YEAR) + return year1 < year2 ? .ascending : year1 > year2 ? .descending : .same + case .month: + let year1 = platformCal1.get(java.util.Calendar.YEAR) + let year2 = platformCal2.get(java.util.Calendar.YEAR) + let month1 = platformCal1.get(java.util.Calendar.MONTH) + let month2 = platformCal2.get(java.util.Calendar.MONTH) + if year1 != year2 { return year1 < year2 ? .ascending : .descending } + return month1 < month2 ? .ascending : month1 > month2 ? .descending : .same + case .day: + let year1 = platformCal1.get(java.util.Calendar.YEAR) + let year2 = platformCal2.get(java.util.Calendar.YEAR) + let day1 = platformCal1.get(java.util.Calendar.DAY_OF_YEAR) + let day2 = platformCal2.get(java.util.Calendar.DAY_OF_YEAR) + if year1 != year2 { return year1 < year2 ? .ascending : .descending } + return day1 < day2 ? .ascending : day1 > day2 ? .descending : .same + default: + return .same + } } - @available(*, unavailable) public func isDate(_ date1: Date, equalTo date2: Date, toGranularity component: Calendar.Component) -> Bool { - fatalError() + return compare(date1, to: date2, toGranularity: component) == .same } - @available(*, unavailable) public func isDate(_ date1: Date, inSameDayAs date2: Date) -> Bool { - fatalError() + return isDate(date1, equalTo: date2, toGranularity: .day) } - @available(*, unavailable) public func isDateInToday(_ date: Date) -> Bool { - fatalError() + let platformCal = platformValue.clone() as java.util.Calendar + platformCal.time = Date().platformValue + + let targetCal = platformValue.clone() as java.util.Calendar + targetCal.time = date.platformValue + + return platformCal.get(java.util.Calendar.YEAR) == targetCal.get(java.util.Calendar.YEAR) + && platformCal.get(java.util.Calendar.DAY_OF_YEAR) == targetCal.get(java.util.Calendar.DAY_OF_YEAR) } @available(*, unavailable) diff --git a/Sources/SkipFoundation/DateComponents.swift b/Sources/SkipFoundation/DateComponents.swift index 621271f..57f28e3 100644 --- a/Sources/SkipFoundation/DateComponents.swift +++ b/Sources/SkipFoundation/DateComponents.swift @@ -60,42 +60,76 @@ public struct DateComponents : Codable, Hashable, CustomStringConvertible { if components?.contains(.timeZone) != false { self.timeZone = tz } - if components?.contains(.era) != false { - if let endDate = endDate { - // TODO: if components.contains(.year) { dc.year = Int(ucal_getFieldDifference(ucalendar, goal, UCAL_YEAR, &status)) } - fatalError("TODO: Skip DateComponents field differences") - } else { + + if let endDate = endDate { + let endPlatformCal = calendar.platformValue.clone() as java.util.Calendar + endPlatformCal.time = endDate.platformValue + + // Calculate differences based on components + if components?.contains(.era) != false { + self.era = endPlatformCal.get(java.util.Calendar.ERA) - platformCal.get(java.util.Calendar.ERA) + } + if components?.contains(.year) != false { + self.year = endPlatformCal.get(java.util.Calendar.YEAR) - platformCal.get(java.util.Calendar.YEAR) + } + if components?.contains(.month) != false { + self.month = endPlatformCal.get(java.util.Calendar.MONTH) - platformCal.get(java.util.Calendar.MONTH) + } + if components?.contains(.day) != false { + self.day = endPlatformCal.get(java.util.Calendar.DATE) - platformCal.get(java.util.Calendar.DATE) + } + if components?.contains(.hour) != false { + self.hour = endPlatformCal.get(java.util.Calendar.HOUR_OF_DAY) - platformCal.get(java.util.Calendar.HOUR_OF_DAY) + } + if components?.contains(.minute) != false { + self.minute = endPlatformCal.get(java.util.Calendar.MINUTE) - platformCal.get(java.util.Calendar.MINUTE) + } + if components?.contains(.second) != false { + self.second = endPlatformCal.get(java.util.Calendar.SECOND) - platformCal.get(java.util.Calendar.SECOND) + } + if components?.contains(.weekday) != false { + self.weekday = endPlatformCal.get(java.util.Calendar.DAY_OF_WEEK) - platformCal.get(java.util.Calendar.DAY_OF_WEEK) + } + if components?.contains(.weekOfMonth) != false { + self.weekOfMonth = endPlatformCal.get(java.util.Calendar.WEEK_OF_MONTH) - platformCal.get(java.util.Calendar.WEEK_OF_MONTH) + } + if components?.contains(.weekOfYear) != false { + self.weekOfYear = endPlatformCal.get(java.util.Calendar.WEEK_OF_YEAR) - platformCal.get(java.util.Calendar.WEEK_OF_YEAR) + } + } else { + // If no endDate is provided, just extract the components from the current date + if components?.contains(.era) != false { self.era = platformCal.get(java.util.Calendar.ERA) } + if components?.contains(.year) != false { + self.year = platformCal.get(java.util.Calendar.YEAR) + } + if components?.contains(.month) != false { + self.month = platformCal.get(java.util.Calendar.MONTH) + 1 + } + if components?.contains(.day) != false { + self.day = platformCal.get(java.util.Calendar.DATE) + } + if components?.contains(.hour) != false { + self.hour = platformCal.get(java.util.Calendar.HOUR_OF_DAY) + } + if components?.contains(.minute) != false { + self.minute = platformCal.get(java.util.Calendar.MINUTE) + } + if components?.contains(.second) != false { + self.second = platformCal.get(java.util.Calendar.SECOND) + } + if components?.contains(.weekday) != false { + self.weekday = platformCal.get(java.util.Calendar.DAY_OF_WEEK) + } + if components?.contains(.weekOfMonth) != false { + self.weekOfMonth = platformCal.get(java.util.Calendar.WEEK_OF_MONTH) + } + if components?.contains(.weekOfYear) != false { + self.weekOfYear = platformCal.get(java.util.Calendar.WEEK_OF_YEAR) + } } - if components?.contains(.year) != false { - self.year = platformCal.get(java.util.Calendar.YEAR) - } - if components?.contains(.month) != false { - self.month = platformCal.get(java.util.Calendar.MONTH) + 1 - } - if components?.contains(.day) != false { - self.day = platformCal.get(java.util.Calendar.DATE) // i.e., DAY_OF_MONTH - } - if components?.contains(.hour) != false { - self.hour = platformCal.get(java.util.Calendar.HOUR_OF_DAY) - } - if components?.contains(.minute) != false { - self.minute = platformCal.get(java.util.Calendar.MINUTE) - } - if components?.contains(.second) != false { - self.second = platformCal.get(java.util.Calendar.SECOND) - } - if components?.contains(.weekday) != false { - self.weekday = platformCal.get(java.util.Calendar.DAY_OF_WEEK) - } - if components?.contains(.weekOfMonth) != false { - self.weekOfMonth = platformCal.get(java.util.Calendar.WEEK_OF_MONTH) - } - if components?.contains(.weekOfYear) != false { - self.weekOfYear = platformCal.get(java.util.Calendar.WEEK_OF_YEAR) - } - + // unsupported fields in java.util.Calendar: //self.nanosecond = platformCal.get(java.util.Calendar.NANOSECOND) //self.weekdayOrdinal = platformCal.get(java.util.Calendar.WEEKDAYORDINAL) diff --git a/Tests/SkipFoundationTests/DateTime/TestCalendar.swift b/Tests/SkipFoundationTests/DateTime/TestCalendar.swift index c8a2c56..6f97040 100644 --- a/Tests/SkipFoundationTests/DateTime/TestCalendar.swift +++ b/Tests/SkipFoundationTests/DateTime/TestCalendar.swift @@ -177,8 +177,8 @@ class TestCalendar: XCTestCase { func test_ampmSymbols() { let calendar = Calendar(identifier: Calendar.Identifier.gregorian) - XCTAssertEqual(calendar.amSymbol, "AM") - XCTAssertEqual(calendar.pmSymbol, "PM") + XCTAssertEqual(calendar.amSymbol.uppercased(), "AM") + XCTAssertEqual(calendar.pmSymbol.uppercased(), "PM") } func test_currentCalendarRRstability() { @@ -505,7 +505,106 @@ class TestCalendar: XCTestCase { expectTime(1728038797.58, [.year, .month, .day, .hour, .minute, .second, .nanosecond]) #endif } + + func testCalendarWithIdentifier() { + let gregorianCalendar = Calendar(identifier: .gregorian) + XCTAssertNotNil(gregorianCalendar) + XCTAssertEqual(gregorianCalendar.identifier, .gregorian) + + let iso8601Calendar = Calendar(identifier: .iso8601) + XCTAssertNotNil(iso8601Calendar) + // XCTAssertEqual(iso8601Calendar.identifier, .iso8601) + } + + func testCalendarCurrent() { + let calendar = Calendar.current + XCTAssertNotNil(calendar) + } + + func testLocale() { + let calendar = Calendar.current + XCTAssertEqual(calendar.locale, Locale.current) + } + + func testTimeZone() { + var calendar = Calendar(identifier: .gregorian) + let timeZone = TimeZone(secondsFromGMT: 0)! + calendar.timeZone = timeZone + XCTAssertEqual(calendar.timeZone, timeZone) + } + + func testFirstWeekday() { + var calendar = Calendar(identifier: .gregorian) + calendar.firstWeekday = 2 // Monday + XCTAssertEqual(calendar.firstWeekday, 2) + } + + func testSymbols() { + let calendar = Calendar(identifier: .gregorian) + XCTAssertGreaterThan(calendar.eraSymbols.count, 0) + XCTAssertGreaterThan(calendar.monthSymbols.count, 0) + XCTAssertGreaterThan(calendar.shortMonthSymbols.count, 0) + XCTAssertGreaterThan(calendar.weekdaySymbols.count, 0) + XCTAssertGreaterThan(calendar.shortWeekdaySymbols.count, 0) + } + + func testRangeOfComponents() { + let calendar = Calendar(identifier: .gregorian) -} + // Test range for months + let monthRange = calendar.minimumRange(of: .month) + XCTAssertEqual(monthRange, 1..<13) + // Test range for days in a month + let dayRange = calendar.minimumRange(of: .day) + XCTAssertEqual(dayRange, 1..<29) + // Test range for hours in a day + let hourRange = calendar.minimumRange(of: .hour) + XCTAssertEqual(hourRange, 0..<24) + + // Test range for minutes in an hour + let minuteRange = calendar.minimumRange(of: .minute) + XCTAssertEqual(minuteRange, 0..<60) + + // Test range for seconds in a minute + let secondRange = calendar.minimumRange(of: .second) + XCTAssertEqual(secondRange, 0..<60) + + // Test range for weekdays (usually 1..<8) where 1 = Sunday, 2 = Monday... + let eraRange = calendar.minimumRange(of: .weekday) + XCTAssertEqual(eraRange, 1..<8) + } + + func testDateComparison() { + let calendar = Calendar(identifier: .gregorian) + let date1 = Date() + let date2 = calendar.date(byAdding: .day, value: 1, to: date1)! + + let comparisonResult = calendar.compare(date1, to: date2, toGranularity: .day) +#if SKIP + XCTAssertEqual(comparisonResult, ComparisonResult.ascending) +#else + XCTAssertEqual(comparisonResult, .orderedAscending) +#endif + } + + func testDateFromComponents() { + var components = DateComponents() + components.year = 2024 + components.month = 10 + components.day = 15 + + let calendar = Calendar(identifier: .gregorian) + let date = calendar.date(from: components) + XCTAssertNotNil(date) + } + + func testDateByAddingComponents() { + let calendar = Calendar(identifier: .gregorian) + let date = Date() + + let newDate = calendar.date(byAdding: .day, value: 1, to: date) + XCTAssertNotNil(newDate) + } +} diff --git a/Tests/SkipFoundationTests/DateTime/TestDateComponents.swift b/Tests/SkipFoundationTests/DateTime/TestDateComponents.swift index a0d9043..4f0f649 100644 --- a/Tests/SkipFoundationTests/DateTime/TestDateComponents.swift +++ b/Tests/SkipFoundationTests/DateTime/TestDateComponents.swift @@ -134,6 +134,145 @@ class TestDateComponents: XCTestCase { XCTAssertTrue(dc.isValidDate) } + + #if SKIP + // Internal DateComponents init + + func testDateComponentsInitializationWithDate() { + let calendar = Calendar(identifier: .gregorian) + let timeZone = TimeZone(secondsFromGMT: 0)! + + var startComponents = DateComponents() + startComponents.year = 2024 + startComponents.month = 10 + startComponents.day = 15 + guard let startDate = calendar.date(from: startComponents) else { + XCTFail("Failed to create start date") + return + } + + let componentsSet: Set = [ + Calendar.Component.year, Calendar.Component.month, Calendar.Component.day, Calendar.Component.timeZone + ] + + let dateComponents: DateComponents = DateComponents( + fromCalendar: calendar, + in: timeZone, + from: startDate, + with: componentsSet + ) + + XCTAssertEqual(dateComponents.year, 2024) + XCTAssertEqual(dateComponents.month, 10) + XCTAssertEqual(dateComponents.day, 15) + XCTAssertEqual(dateComponents.timeZone, timeZone) + } + + func testDateComponentsInitializationWithStartDateAndEndDate() { + let calendar = Calendar(identifier: .gregorian) + let timeZone = TimeZone(secondsFromGMT: 0)! + + var startComponents = DateComponents() + startComponents.year = 2024 + startComponents.month = 10 + startComponents.day = 15 + guard let startDate = calendar.date(from: startComponents) else { + XCTFail("Failed to create start date") + return + } + + var endComponents = DateComponents() + endComponents.year = 2024 + endComponents.month = 12 + endComponents.day = 31 + guard let endDate = calendar.date(from: endComponents) else { + XCTFail("Failed to create end date") + return + } + + let componentsSet: Set = [ + Calendar.Component.year, Calendar.Component.month, Calendar.Component.day + ] + let dateComponents = DateComponents( + fromCalendar: calendar, + in: timeZone, + from: startDate, + to: endDate, + with: componentsSet + ) + + XCTAssertEqual(dateComponents.year, 0) + XCTAssertEqual(dateComponents.month, 2) + XCTAssertEqual(dateComponents.day, 16) + } + + func testDateComponentsWithSelectedComponents() { + let calendar = Calendar(identifier: .gregorian) + let timeZone = TimeZone(secondsFromGMT: 0)! + + var startComponents = DateComponents() + startComponents.year = 2024 + startComponents.month = 10 + startComponents.day = 15 + guard let startDate = calendar.date(from: startComponents) else { + XCTFail("Failed to create start date") + return + } + + let componentsSet: Set = [Calendar.Component.month, Calendar.Component.day] + let dateComponents = DateComponents( + fromCalendar: calendar, + in: timeZone, + from: startDate, + with: componentsSet + ) + + XCTAssertEqual(dateComponents.month, 10) + XCTAssertEqual(dateComponents.day, 15) + XCTAssertNil(dateComponents.year) + } + + func testDateComponentsWithStartAndEndDate() { + let calendar = Calendar(identifier: .gregorian) + let timeZone = TimeZone(secondsFromGMT: 0)! + + // Start Date: 2024-10-15 + var startComponents = DateComponents() + startComponents.year = 2024 + startComponents.month = 10 + startComponents.day = 15 + guard let startDate = calendar.date(from: startComponents) else { + XCTFail("Failed to create start date") + return + } + + // End Date: 2024-12-25 (for calculating the difference) + var endComponents = DateComponents() + endComponents.year = 2024 + endComponents.month = 12 + endComponents.day = 25 + guard let endDate = calendar.date(from: endComponents) else { + XCTFail("Failed to create end date") + return + } + + let componentsSet: Set = [ + Calendar.Component.month, Calendar.Component.day + ] + let dateComponents = DateComponents( + fromCalendar: calendar, + in: timeZone, + from: startDate, + to: endDate, + with: componentsSet + ) + + XCTAssertEqual(dateComponents.month, 2) + XCTAssertEqual(dateComponents.day, 10) + XCTAssertNil(dateComponents.year) + } + + #endif }