Skip to content

Commit 7ed437d

Browse files
authored
Add support for weeks granularity (#24767)
1 parent 4c3a3c7 commit 7ed437d

10 files changed

Lines changed: 76 additions & 36 deletions

File tree

Modules/Sources/JetpackStats/Analytics/StatsEvent.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public enum StatsEvent {
4848

4949
/// Date range preset selected
5050
/// - Parameters:
51-
/// - "selected_preset": The preset selected (e.g., "last_7_days", "last_28_days", "last_90_days", "last_365_days")
51+
/// - "selected_preset": The preset selected (e.g., "last_7_days", "last_28_days", "last_12_weeks", "last_365_days")
5252
case dateRangePresetSelected
5353

5454
/// Custom date range selected
@@ -148,7 +148,7 @@ extension DateIntervalPreset {
148148
case .last7Days: "last_7_days"
149149
case .last28Days: "last_28_days"
150150
case .last30Days: "last_30_days"
151-
case .last90Days: "last_90_days"
151+
case .last12Weeks: "last_12_weeks"
152152
case .last6Months: "last_6_months"
153153
case .last12Months: "last_12_months"
154154
case .last3Years: "last_3_years"

Modules/Sources/JetpackStats/Extensions/Calendar+Presets.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ enum DateIntervalPreset: String, CaseIterable, Identifiable {
1919
case last28Days
2020
/// The previous 30 days, not including today
2121
case last30Days
22-
/// The previous 90 days, not including today
23-
case last90Days
22+
/// The previous 12 weeks (84 days), not including today
23+
case last12Weeks
2424
/// The last 6 months, including the current month
2525
case last6Months
2626
/// The last 12 months, including the current month
@@ -42,7 +42,7 @@ enum DateIntervalPreset: String, CaseIterable, Identifiable {
4242
case .last7Days: Strings.Calendar.last7Days
4343
case .last28Days: Strings.Calendar.last28Days
4444
case .last30Days: Strings.Calendar.last30Days
45-
case .last90Days: Strings.Calendar.last90Days
45+
case .last12Weeks: Strings.Calendar.last12Weeks
4646
case .last6Months: Strings.Calendar.last6Months
4747
case .last12Months: Strings.Calendar.last12Months
4848
case .last3Years: Strings.Calendar.last3Years
@@ -52,7 +52,7 @@ enum DateIntervalPreset: String, CaseIterable, Identifiable {
5252

5353
var prefersDateIntervalFormatting: Bool {
5454
switch self {
55-
case .today, .last7Days, .last28Days, .last30Days, .last90Days, .last6Months, .last12Months, .thisWeek:
55+
case .today, .last7Days, .last28Days, .last30Days, .last12Weeks, .last6Months, .last12Months, .thisWeek:
5656
return false
5757
case .thisMonth, .thisYear, .thisQuarter, .last3Years, .last10Years:
5858
return true
@@ -72,7 +72,7 @@ enum DateIntervalPreset: String, CaseIterable, Identifiable {
7272
return .quarter
7373
case .thisYear, .last3Years, .last10Years:
7474
return .year
75-
case .last7Days, .last28Days, .last30Days, .last90Days:
75+
case .last7Days, .last28Days, .last30Days, .last12Weeks:
7676
return .day
7777
}
7878
}
@@ -110,7 +110,7 @@ extension Calendar {
110110
case .last7Days: makeDateInterval(offset: -7, component: .day, for: now)
111111
case .last28Days: makeDateInterval(offset: -28, component: .day, for: now)
112112
case .last30Days: makeDateInterval(offset: -30, component: .day, for: now)
113-
case .last90Days: makeDateInterval(offset: -90, component: .day, for: now)
113+
case .last12Weeks: makeDateInterval(offset: -84, component: .day, for: now)
114114
case .last6Months: makeDateInterval(offset: -6, component: .month, for: now)
115115
case .last12Months: makeDateInterval(offset: -12, component: .month, for: now)
116116
case .last3Years: makeDateInterval(offset: -3, component: .year, for: now)

Modules/Sources/JetpackStats/Services/StatsService.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,7 @@ private extension WordPressKit.StatsPeriodUnit {
425425
switch granularity {
426426
case .hour: self = .hour
427427
case .day: self = .day
428+
case .week: self = .week
428429
case .month: self = .month
429430
case .year: self = .year
430431
}

Modules/Sources/JetpackStats/Strings.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ enum Strings {
2020
static let last7Days = AppLocalizedString("jetpackStats.calendar.last7Days", value: "Last 7 Days", comment: "Last 7 days date range")
2121
static let last28Days = AppLocalizedString("jetpackStats.calendar.last28Days", value: "Last 28 Days", comment: "Last 28 days date range")
2222
static let last30Days = AppLocalizedString("jetpackStats.calendar.last30Days", value: "Last 30 Days", comment: "Last 30 days date range")
23-
static let last90Days = AppLocalizedString("jetpackStats.calendar.last90Days", value: "Last 90 Days", comment: "Last 90 days date range")
23+
static let last12Weeks = AppLocalizedString("jetpackStats.calendar.last12Weeks", value: "Last 12 Weeks", comment: "Last 12 weeks (84 days) date range")
2424
static let last6Months = AppLocalizedString("jetpackStats.calendar.last6Months", value: "Last 6 Months", comment: "Last 6 months date range")
2525
static let last12Months = AppLocalizedString("jetpackStats.calendar.last12Months", value: "Last 12 Months", comment: "Last 12 months date range")
2626
static let last3Years = AppLocalizedString("jetpackStats.calendar.last3Years", value: "Last 3 Years", comment: "Last 3 years date range")
@@ -88,7 +88,7 @@ enum Strings {
8888
enum DatePicker {
8989
static let customRange = AppLocalizedString("jetpackStats.datePicker.customRange", value: "Custom Range", comment: "Title for custom date range picker")
9090
static let customRangeMenu = AppLocalizedString("jetpackStats.datePicker.customRangeMenu", value: "Custom Range…", comment: "Menu item for custom date range picker")
91-
static let morePeriods = AppLocalizedString("jetpackStats.datePicker.morePeriods", value: "More Periods", comment: "Menu item for more date period options")
91+
static let morePeriods = AppLocalizedString("jetpackStats.datePicker.more", value: "More…", comment: "Menu item for more date period options")
9292
static let from = AppLocalizedString("jetpackStats.datePicker.from", value: "From", comment: "From date label")
9393
static let to = AppLocalizedString("jetpackStats.datePicker.to", value: "To", comment: "To date label")
9494
static let quickPeriodsForStartDate = AppLocalizedString("jetpackStats.datePicker.quickPeriodsForStartDate", value: "Quick periods for start date", comment: "Label for quick period selection")

Modules/Sources/JetpackStats/Utilities/DateRangeGranularity.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Foundation
33
enum DateRangeGranularity: Comparable {
44
case hour
55
case day
6+
case week
67
case month
78
case year
89
}
@@ -21,9 +22,12 @@ extension DateInterval {
2122
return .hour
2223
}
2324
// For ranges 2-90 days: show daily data (2-90 points)
24-
else if totalDays <= 90 {
25+
else if totalDays <= 31 {
2526
return .day
2627
}
28+
else if totalDays <= 90 {
29+
return .week
30+
}
2731
// For ranges under about 4 years, show months
2832
else if totalDays <= 365 * 4 {
2933
return .month
@@ -41,6 +45,7 @@ extension DateRangeGranularity {
4145
switch self {
4246
case .hour: [.year, .month, .day, .hour]
4347
case .day: [.year, .month, .day]
48+
case .week: [.year, .month, .day]
4449
case .month: [.year, .month]
4550
case .year: [.year]
4651
}
@@ -51,6 +56,7 @@ extension DateRangeGranularity {
5156
switch self {
5257
case .hour: .hour
5358
case .day: .day
59+
case .week: .weekOfYear
5460
case .month: .month
5561
case .year: .year
5662
}

Modules/Sources/JetpackStats/Utilities/Formatters/StatsDateFormatter.swift

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,9 @@ struct StatsDateFormatter: Sendable {
66
case regular
77
}
88

9-
var locale: Locale {
10-
didSet {
11-
updateFormatters()
12-
}
13-
}
14-
15-
var timeZone: TimeZone {
16-
didSet {
17-
updateFormatters()
18-
}
19-
}
9+
private let formatters: CachedFormatters
10+
private let calendar: Calendar
11+
private let now: @Sendable () -> Date
2012

2113
final class CachedFormatters: Sendable {
2214
let hour: DateFormatter
@@ -28,6 +20,7 @@ struct StatsDateFormatter: Sendable {
2820
let regularMonth: DateFormatter
2921

3022
let year: DateFormatter
23+
let week: StatsDateRangeFormatter
3124

3225
let timeOffset: DateFormatter
3326

@@ -51,6 +44,8 @@ struct StatsDateFormatter: Sendable {
5144
year = makeFormatter("yyyy")
5245

5346
timeOffset = makeFormatter("ZZZZ")
47+
48+
week = StatsDateRangeFormatter(locale: locale, timeZone: timeZone)
5449
}
5550

5651
func formatter(granularity: DateRangeGranularity, context: Context) -> DateFormatter {
@@ -59,37 +54,52 @@ struct StatsDateFormatter: Sendable {
5954
switch granularity {
6055
case .hour: hour
6156
case .day: compactDay
57+
case .week: compactDay
6258
case .month: compactMonth
6359
case .year: year
6460
}
6561
case .regular:
6662
switch granularity {
6763
case .hour: hour
6864
case .day: regularDay
65+
case .week: regularDay
6966
case .month: regularMonth
7067
case .year: year
7168
}
7269
}
7370
}
7471
}
7572

76-
private var formatters: CachedFormatters
77-
78-
private mutating func updateFormatters() {
79-
formatters = CachedFormatters(locale: locale, timeZone: timeZone)
80-
}
81-
82-
init(locale: Locale = .current, timeZone: TimeZone = .current) {
83-
self.locale = locale
84-
self.timeZone = timeZone
73+
init(
74+
locale: Locale = .current,
75+
timeZone: TimeZone = .current,
76+
now: @Sendable @escaping () -> Date = { Date() }
77+
) {
8578
self.formatters = CachedFormatters(locale: locale, timeZone: timeZone)
79+
self.calendar = {
80+
var calendar = Calendar.current
81+
calendar.timeZone = timeZone
82+
return calendar
83+
}()
84+
self.now = now
8685
}
8786

8887
func formatDate(_ date: Date, granularity: DateRangeGranularity, context: Context = .compact) -> String {
88+
if granularity == .week && context == .regular {
89+
return formatWeekRange(containing: date)
90+
}
8991
let formatter = formatters.formatter(granularity: granularity, context: context)
9092
return formatter.string(from: date)
9193
}
9294

95+
private func formatWeekRange(containing date: Date) -> String {
96+
guard var weekInterval = calendar.dateInterval(of: .weekOfYear, for: date) else {
97+
return formatters.formatter(granularity: .day, context: .compact).string(from: date)
98+
}
99+
weekInterval.end = weekInterval.end.addingTimeInterval(-1)
100+
return formatters.week.string(from: weekInterval, now: now())
101+
}
102+
93103
var formattedTimeOffset: String {
94104
formatters.timeOffset.string(from: .now)
95105
}

Modules/Sources/JetpackStats/Views/StatsDateRangePickerMenu.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ struct StatsDateRangePickerMenu: View {
2525
Section {
2626
makePresetButtons(for: [
2727
.last28Days,
28-
.last90Days,
28+
.last12Weeks,
2929
.last6Months,
3030
.last3Years,
3131
.last10Years

Modules/Tests/JetpackStatsTests/CalendarPresetsTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ struct CalendarDateRangePresetTests {
1515
(.last7Days, Date("2025-01-08T00:00:00-03:00"), Date("2025-01-15T00:00:00-03:00")),
1616
(.last28Days, Date("2024-12-18T00:00:00-03:00"), Date("2025-01-15T00:00:00-03:00")),
1717
(.last30Days, Date("2024-12-16T00:00:00-03:00"), Date("2025-01-15T00:00:00-03:00")),
18-
(.last90Days, Date("2024-10-17T00:00:00-03:00"), Date("2025-01-15T00:00:00-03:00")),
18+
(.last12Weeks, Date("2024-10-23T00:00:00-03:00"), Date("2025-01-15T00:00:00-03:00")),
1919
(.last6Months, Date("2024-08-01T00:00:00-03:00"), Date("2025-02-01T00:00:00-03:00")),
2020
(.last12Months, Date("2024-02-01T00:00:00-03:00"), Date("2025-02-01T00:00:00-03:00")),
2121
(.last3Years, Date("2023-01-01T00:00:00-03:00"), Date("2026-01-01T00:00:00-03:00")),
@@ -115,7 +115,7 @@ struct CalendarDateRangePresetTests {
115115
(.last7Days, Date("2025-01-24T00:00:00-03:00"), Date("2025-01-31T00:00:00-03:00")),
116116
(.last28Days, Date("2025-01-03T00:00:00-03:00"), Date("2025-01-31T00:00:00-03:00")),
117117
(.last30Days, Date("2025-01-01T00:00:00-03:00"), Date("2025-01-31T00:00:00-03:00")),
118-
(.last90Days, Date("2024-11-02T00:00:00-03:00"), Date("2025-01-31T00:00:00-03:00")),
118+
(.last12Weeks, Date("2024-11-08T00:00:00-03:00"), Date("2025-01-31T00:00:00-03:00")),
119119
(.last6Months, Date("2024-08-01T00:00:00-03:00"), Date("2025-02-01T00:00:00-03:00")),
120120
(.last12Months, Date("2024-02-01T00:00:00-03:00"), Date("2025-02-01T00:00:00-03:00")),
121121
(.last3Years, Date("2023-01-01T00:00:00-03:00"), Date("2026-01-01T00:00:00-03:00")),
@@ -142,7 +142,7 @@ struct CalendarDateRangePresetTests {
142142
(.last7Days, Date("2024-12-25T00:00:00-03:00"), Date("2025-01-01T00:00:00-03:00")),
143143
(.last28Days, Date("2024-12-04T00:00:00-03:00"), Date("2025-01-01T00:00:00-03:00")),
144144
(.last30Days, Date("2024-12-02T00:00:00-03:00"), Date("2025-01-01T00:00:00-03:00")),
145-
(.last90Days, Date("2024-10-03T00:00:00-03:00"), Date("2025-01-01T00:00:00-03:00")),
145+
(.last12Weeks, Date("2024-10-09T00:00:00-03:00"), Date("2025-01-01T00:00:00-03:00")),
146146
(.last6Months, Date("2024-08-01T00:00:00-03:00"), Date("2025-02-01T00:00:00-03:00")),
147147
(.last12Months, Date("2024-02-01T00:00:00-03:00"), Date("2025-02-01T00:00:00-03:00")),
148148
(.last3Years, Date("2023-01-01T00:00:00-03:00"), Date("2026-01-01T00:00:00-03:00")),

Modules/Tests/JetpackStatsTests/StatsDateFormatterTests.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,26 @@ struct StatsDateFormatterTests {
6868
let result = formatter.formatDate(date, granularity: .year, context: .regular)
6969
#expect(result == "2025")
7070
}
71+
72+
@Test func weekFormatting() {
73+
let date = Date("2025-03-9T14:00:00-03:00")
74+
let result = formatter.formatDate(date, granularity: .week, context: .regular)
75+
#expect(result == "Mar 9 – 15")
76+
77+
let mondayDate = Date("2025-03-10T14:00:00-03:00")
78+
let mondayResult = formatter.formatDate(mondayDate, granularity: .week, context: .regular)
79+
#expect(mondayResult == "Mar 9 – 15")
80+
81+
let yearBoundary = Date("2025-01-01T14:00:00-03:00")
82+
let yearBoundaryResult = formatter.formatDate(yearBoundary, granularity: .week, context: .regular)
83+
#expect(yearBoundaryResult == "Dec 29, 2024 – Jan 4, 2025")
84+
85+
let previousYear = Date("2024-03-15T14:00:00-03:00")
86+
let previousYearResult = formatter.formatDate(previousYear, granularity: .week, context: .regular)
87+
#expect(previousYearResult == "Mar 10 – 16, 2024")
88+
89+
let crossYear = Date("2024-12-30T14:00:00-03:00")
90+
let crossYearResult = formatter.formatDate(crossYear, granularity: .week, context: .regular)
91+
#expect(crossYearResult == "Dec 29, 2024 – Jan 4, 2025")
92+
}
7193
}

RELEASE-NOTES.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
26.2
22
-----
3-
* [*] New Stats: Fix dark mode support in custom range picker [#24770]
3+
* [*] New Stats: Add support for "weeks" granularity [#24767]
4+
* [*] New Stats: Fix dark mode support in custom range picker [#24770]
45

56
26.1
67
-----

0 commit comments

Comments
 (0)