diff --git a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift index b86eebf5d..ddbb68a21 100644 --- a/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift +++ b/apps/calendar-macos/CompassData/Sources/CompassData/NativeCalendarRootModel.swift @@ -174,7 +174,7 @@ public final class NativeCalendarRootModel { let anchor: Date = { let today = demoPresentation?.referenceNow ?? now() - return EffectiveTimeZone.calendar.dateInterval(of: .weekOfYear, for: today)?.start ?? today + return CalendarWindowMath.sundayWeekViewStart(containing: today) }() viewStore = ViewStore( view: .week, @@ -372,9 +372,7 @@ public final class NativeCalendarRootModel { contentTrackWidth = CGFloat(ParityCaptureGeometry.contentTrackWidth) viewStore.setVisibleDayCount(ParityCaptureGeometry.visibleDayCount) let today = referenceNow - let calendar = EffectiveTimeZone.calendar - let weekStart = calendar.dateInterval(of: .weekOfYear, for: today)?.start ?? today - viewStore.setAnchorDate(weekStart) + viewStore.setAnchorDate(CalendarWindowMath.sundayWeekViewStart(containing: today)) monthPickerMonth = today pendingScroll = .scrollToNowOrPulse rebuildPresentation() @@ -922,8 +920,7 @@ public final class NativeCalendarRootModel { if viewStore.visibleDayCount == 1 { viewStore.setAnchorDate(today) } else { - let weekStart = calendar.dateInterval(of: .weekOfYear, for: today)?.start ?? today - viewStore.setAnchorDate(weekStart) + viewStore.setAnchorDate(CalendarWindowMath.sundayWeekViewStart(containing: today)) } monthPickerMonth = today rebuildPresentation() diff --git a/apps/calendar-macos/CompassKit/Sources/CompassKit/Calendar/CalendarWindowMath.swift b/apps/calendar-macos/CompassKit/Sources/CompassKit/Calendar/CalendarWindowMath.swift index 5bd3f31f2..8b8ee28aa 100644 --- a/apps/calendar-macos/CompassKit/Sources/CompassKit/Calendar/CalendarWindowMath.swift +++ b/apps/calendar-macos/CompassKit/Sources/CompassKit/Calendar/CalendarWindowMath.swift @@ -3,6 +3,17 @@ import Foundation public enum CalendarWindowMath { public static let weekDayCount = 7 + /// Web Day.js `startOf("week")`: Sunday 00:00 in the effective timezone. + public static func sundayWeekViewStart(containing date: Date) -> Date { + var calendar = EffectiveTimeZone.calendar + calendar.firstWeekday = 1 + calendar.minimumDaysInFirstWeek = 1 + let startOfDay = calendar.startOfDay(for: date) + let weekday = calendar.component(.weekday, from: startOfDay) + let daysFromSunday = (weekday - calendar.firstWeekday + 7) % 7 + return calendar.date(byAdding: .day, value: -daysFromSunday, to: startOfDay) ?? startOfDay + } + /// Seven local days from the anchor's start of day; `end` is exclusive next midnight. public static func weekEventsQueryWindow(anchor: Date) -> DateRange { let calendar = EffectiveTimeZone.calendar diff --git a/apps/calendar-macos/CompassKit/Sources/CompassKit/Timezone/TimeZoneFormatting.swift b/apps/calendar-macos/CompassKit/Sources/CompassKit/Timezone/TimeZoneFormatting.swift index dd8a19ed2..fd9986d53 100644 --- a/apps/calendar-macos/CompassKit/Sources/CompassKit/Timezone/TimeZoneFormatting.swift +++ b/apps/calendar-macos/CompassKit/Sources/CompassKit/Timezone/TimeZoneFormatting.swift @@ -5,12 +5,22 @@ public enum TimeZoneFormatting { TimeZone(identifier: identifier) != nil } + /// Matches web `formatTimeZoneAbbreviation` (Intl short, offset fallback). public static func abbreviation(for timeZoneId: String, at date: Date = Date()) -> String { guard let zone = TimeZone(identifier: timeZoneId) else { return timeZoneId } - let formatter = DateFormatter() - formatter.timeZone = zone - formatter.dateFormat = "z" - return formatter.string(from: date) + if timeZoneId == "UTC" { + return "UTC" + } + + let short = timeZoneNamePart(timeZone: zone, at: date, style: .short) + if let short, !looksLikeIanaId(short) { + return short + } + + if let offset = timeZoneNamePart(timeZone: zone, at: date, style: .shortOffset) { + return offset + } + return short ?? timeZoneId } public static func cityName(for timeZoneId: String) -> String { @@ -18,4 +28,31 @@ public enum TimeZoneFormatting { guard let city = parts.last else { return timeZoneId } return city.replacingOccurrences(of: "_", with: " ") } + + private enum NameStyle { + case short + case shortOffset + } + + private static func timeZoneNamePart( + timeZone: TimeZone, + at date: Date, + style: NameStyle + ) -> String? { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US") + formatter.timeZone = timeZone + switch style { + case .short: + formatter.dateFormat = "zzz" + case .shortOffset: + formatter.dateFormat = "ZZZZZ" + } + let value = formatter.string(from: date) + return value.isEmpty ? nil : value + } + + private static func looksLikeIanaId(_ value: String) -> Bool { + value.contains("/") + } } diff --git a/apps/calendar-macos/CompassKitTests/CalendarWindowMathTests.swift b/apps/calendar-macos/CompassKitTests/CalendarWindowMathTests.swift index df862fda0..c1420df43 100644 --- a/apps/calendar-macos/CompassKitTests/CalendarWindowMathTests.swift +++ b/apps/calendar-macos/CompassKitTests/CalendarWindowMathTests.swift @@ -25,6 +25,13 @@ final class CalendarWindowMathTests: XCTestCase { ) } + func testSundayWeekViewStartMatchesWeb() throws { + let reference = try XCTUnwrap( + CompassDateParsing.parseInEffectiveTimeZone("2026-06-10T15:00:00.000Z")) + let weekStart = CalendarWindowMath.sundayWeekViewStart(containing: reference) + XCTAssertEqual(CompassDateParsing.formatCalendarDay(weekStart), "2026-06-07") + } + func testDayEventQueryRangeUsesExclusiveEnd() throws { let anchor = try XCTUnwrap( CompassDateParsing.calendarDateInEffectiveTimeZone("2026-06-28") diff --git a/apps/calendar-macos/CompassKitTests/ParityCaptureGeometryTests.swift b/apps/calendar-macos/CompassKitTests/ParityCaptureGeometryTests.swift index 36f67e497..f346efec5 100644 --- a/apps/calendar-macos/CompassKitTests/ParityCaptureGeometryTests.swift +++ b/apps/calendar-macos/CompassKitTests/ParityCaptureGeometryTests.swift @@ -7,4 +7,5 @@ final class ParityCaptureGeometryTests: XCTestCase { XCTAssertEqual(ParityCaptureGeometry.contentTrackWidth, 871, accuracy: 0.1) XCTAssertEqual(ParityCaptureGeometry.visibleDayCount, 5) } + } diff --git a/apps/calendar-macos/CompassKitTests/TimeZoneFormattingTests.swift b/apps/calendar-macos/CompassKitTests/TimeZoneFormattingTests.swift new file mode 100644 index 000000000..19880cc48 --- /dev/null +++ b/apps/calendar-macos/CompassKitTests/TimeZoneFormattingTests.swift @@ -0,0 +1,18 @@ +import CompassKit +import XCTest + +final class TimeZoneFormattingTests: XCTestCase { + func testUTCLabelMatchesWeb() throws { + let at = try XCTUnwrap( + CompassDateParsing.parseInEffectiveTimeZone("2026-07-15T12:00:00.000Z")) + XCTAssertEqual(TimeZoneFormatting.abbreviation(for: "UTC", at: at), "UTC") + } + + func testNamedZoneUsesShortAbbreviation() throws { + let summer = try XCTUnwrap( + CompassDateParsing.parseInEffectiveTimeZone("2026-07-15T18:00:00.000Z")) + XCTAssertEqual( + TimeZoneFormatting.abbreviation(for: "America/Denver", at: summer), + "MDT") + } +}