Skip to content

Commit b3c0622

Browse files
authored
[Stats Revamp] Changing "First day of week" results in incorrect numbers in Views & Visitors and Total Likes details (#19829)
* Use weekIncludingDate to determine end of week for Monday - Sunday week StatsPeriodHelper().weekIncludingDate is used in the Stats to determine start and end of week, when week is Monday - Sunday. Use this method in the lastDayOfTheWeek to have a consistent start and end of week calculation and avoid bugs with different iOS firstDayOfWeek settings. * Remove duplicated code from calculateEndDate calculateEndDate and lastDayOfTheWeek duplicate the same code and comment. Use lastDayOfTheWeek in calculateEndDate. * Fix TotalLikes details numbers by passing correct parameter to futureEndOfWeekDate * Remove a -12 hour offset when calculating last day of the week This is not necessary with the current calculation approach and it produces wrong results with the current date is around the beginning or end of the week * Returning the current date instead of nil * Keeping the changes under the Feature Flag * Use FeatureFlag.statsNewInsights
1 parent 641ac88 commit b3c0622

File tree

4 files changed

+168
-28
lines changed

4 files changed

+168
-28
lines changed

WordPress/Classes/ViewRelated/Stats/Helpers/StatsPeriodHelper.swift

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,10 @@ class StatsPeriodHelper {
9292
}
9393
}
9494

95-
func calculateEndDate(from currentDate: Date, offsetBy count: Int = 1, unit: StatsPeriodUnit) -> Date? {
96-
let calendar = Calendar.autoupdatingCurrent
97-
95+
func calculateEndDate(from currentDate: Date,
96+
offsetBy count: Int = 1,
97+
unit: StatsPeriodUnit,
98+
calendar: Calendar = Calendar.autoupdatingCurrent) -> Date? {
9899
guard let adjustedDate = calendar.date(byAdding: unit.calendarComponent, value: count, to: currentDate) else {
99100
DDLogError("[Stats] Couldn't do basic math on Calendars in Stats. Returning original value.")
100101
return currentDate
@@ -105,21 +106,28 @@ class StatsPeriodHelper {
105106
return adjustedDate.normalizedDate()
106107

107108
case .week:
108-
109-
// The hours component here is because the `dateInterval` returned by Calendar is a closed range
110-
// — so the "end" of a specific week is also simultenously a 'start' of the next one.
111-
// This causes problem when calling this math on dates that _already are_ an end/start of a week.
112-
// This doesn't work for our calculations, so we force it to rollover using this hack.
113-
// (I *think* that's what's happening here. Doing Calendar math on this method has broken my brain.
114-
// I spend like 10h on this ~50 LoC method. Beware.)
115-
let components = DateComponents(day: 7 * count, hour: -12)
116-
117-
guard let weekAdjusted = calendar.date(byAdding: components, to: currentDate.normalizedDate()) else {
118-
DDLogError("[Stats] Couldn't add a multiple of 7 days and -12 hours to a date in Stats. Returning original value.")
119-
return currentDate
109+
if FeatureFlag.statsNewInsights.enabled {
110+
guard let endDate = currentDate.lastDayOfTheWeek(in: calendar, with: count) else {
111+
DDLogError("[Stats] Couldn't determine the last day of the week for a given date in Stats. Returning original value.")
112+
return currentDate
113+
}
114+
115+
return endDate.normalizedDate()
116+
} else {
117+
// The hours component here is because the `dateInterval` returned by Calendar is a closed range
118+
// — so the "end" of a specific week is also simultenously a 'start' of the next one.
119+
// This causes problem when calling this math on dates that _already are_ an end/start of a week.
120+
// This doesn't work for our calculations, so we force it to rollover using this hack.
121+
// (I *think* that's what's happening here. Doing Calendar math on this method has broken my brain.
122+
// I spend like 10h on this ~50 LoC method. Beware.)
123+
let components = DateComponents(day: 7 * count, hour: -12)
124+
125+
guard let weekAdjusted = calendar.date(byAdding: components, to: currentDate.normalizedDate()) else {
126+
DDLogError("[Stats] Couldn't add a multiple of 7 days and -12 hours to a date in Stats. Returning original value.")
127+
return currentDate
128+
}
129+
return calendar.dateInterval(of: .weekOfYear, for: weekAdjusted)?.end.normalizedDate()
120130
}
121-
return calendar.dateInterval(of: .weekOfYear, for: weekAdjusted)?.end.normalizedDate()
122-
123131
case .month:
124132
guard let endDate = adjustedDate.lastDayOfTheMonth(in: calendar) else {
125133
DDLogError("[Stats] Couldn't determine number of days in a given month in Stats. Returning original value.")
@@ -169,17 +177,11 @@ private extension Date {
169177
}
170178

171179
func lastDayOfTheWeek(in calendar: Calendar, with offset: Int) -> Date? {
172-
// The hours component here is because the `dateInterval` returnd by Calendar is a closed range
173-
// — so the "end" of a specific week is also simultenously a 'start' of the next one.
174-
// This causes problem when calling this math on dates that _already are_ an end/start of a week.
175-
// This doesn't work for our calculations, so we force it to rollover using this hack.
176-
// (I *think* that's what's happening here. Doing Calendar math on this method has broken my brain.
177-
// I spend like 10h on this ~50 LoC method. Beware.)
178-
let components = DateComponents(day: 7 * offset, hour: -12)
180+
let components = DateComponents(day: 7 * offset)
179181

180182
guard let weekAdjusted = calendar.date(byAdding: components, to: normalizedDate()),
181-
let endOfAdjustedWeek = calendar.dateInterval(of: .weekOfYear, for: weekAdjusted)?.end else {
182-
DDLogError("[Stats] Couldn't add a multiple of 7 days and -12 hours to a date in Stats. Returning original value.")
183+
let endOfAdjustedWeek = StatsPeriodHelper().weekIncludingDate(weekAdjusted)?.weekEnd else {
184+
DDLogError("[Stats] Couldn't add a multiple of 7 days to a date in Stats. Returning original value.")
183185
return nil
184186
}
185187
return endOfAdjustedWeek

WordPress/Classes/ViewRelated/Stats/Shared Views/Stats Detail/SiteStatsInsightsDetailsViewModel.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -502,8 +502,8 @@ class SiteStatsInsightsDetailsViewModel: Observable {
502502
return StatsTotalInsightsData.followersCount(insightsStore: insightsStore)
503503
}
504504

505-
func createLikesTotalInsightsRow(periodSummary: StatsSummaryTimeIntervalData?) -> StatsTotalInsightsData {
506-
let weekEnd = futureEndOfWeekDate(for: periodStore.getSummary())
505+
func createLikesTotalInsightsRow(periodSummary: StatsSummaryTimeIntervalData?) -> StatsTotalInsightsData {
506+
let weekEnd = futureEndOfWeekDate(for: periodSummary)
507507
var data = StatsTotalInsightsData.createTotalInsightsData(periodSummary: periodSummary,
508508
insightsStore: insightsStore,
509509
statsSummaryType: .likes,

WordPress/WordPress.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@
150150
01CE5013290A890E00A9C2E0 /* TracksConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CE5010290A890300A9C2E0 /* TracksConfiguration.swift */; };
151151
01CE5014290A890E00A9C2E0 /* TracksConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CE5010290A890300A9C2E0 /* TracksConfiguration.swift */; };
152152
01CE5015290A890F00A9C2E0 /* TracksConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01CE5010290A890300A9C2E0 /* TracksConfiguration.swift */; };
153+
01E78D1D296EA54F00FB6863 /* StatsPeriodHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E78D1C296EA54F00FB6863 /* StatsPeriodHelperTests.swift */; };
153154
02761EC02270072F009BAF0F /* BlogDetailsViewController+SectionHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02761EBF2270072F009BAF0F /* BlogDetailsViewController+SectionHelpers.swift */; };
154155
02761EC222700A9C009BAF0F /* BlogDetailsSubsectionToSectionCategoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02761EC122700A9C009BAF0F /* BlogDetailsSubsectionToSectionCategoryTests.swift */; };
155156
02761EC4227010BC009BAF0F /* BlogDetailsSectionIndexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02761EC3227010BC009BAF0F /* BlogDetailsSectionIndexTests.swift */; };
@@ -5530,6 +5531,7 @@
55305531
0148CC2A2859C87000CF5D96 /* BlogServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogServiceMock.swift; sourceTree = "<group>"; };
55315532
01CE5006290A889F00A9C2E0 /* TracksConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracksConfiguration.swift; sourceTree = "<group>"; };
55325533
01CE5010290A890300A9C2E0 /* TracksConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracksConfiguration.swift; sourceTree = "<group>"; };
5534+
01E78D1C296EA54F00FB6863 /* StatsPeriodHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsPeriodHelperTests.swift; sourceTree = "<group>"; };
55335535
02761EBF2270072F009BAF0F /* BlogDetailsViewController+SectionHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BlogDetailsViewController+SectionHelpers.swift"; sourceTree = "<group>"; };
55345536
02761EC122700A9C009BAF0F /* BlogDetailsSubsectionToSectionCategoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDetailsSubsectionToSectionCategoryTests.swift; sourceTree = "<group>"; };
55355537
02761EC3227010BC009BAF0F /* BlogDetailsSectionIndexTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDetailsSectionIndexTests.swift; sourceTree = "<group>"; };
@@ -11236,6 +11238,7 @@
1123611238
40C403F72215D88100E8C894 /* TopViewedStatsTests.swift */,
1123711239
400A2C942217B68D000A8A59 /* TopViewedVideoStatsRecordValueTests.swift */,
1123811240
400A2C962217B883000A8A59 /* VisitsSummaryStatsRecordValueTests.swift */,
11241+
01E78D1C296EA54F00FB6863 /* StatsPeriodHelperTests.swift */,
1123911242
);
1124011243
name = Stats;
1124111244
sourceTree = "<group>";
@@ -22560,6 +22563,7 @@
2256022563
010459ED2915519C000C7778 /* JetpackNotificationMigrationServiceTests.swift in Sources */,
2256122564
8B6214E627B1B446001DF7B6 /* BlogDashboardServiceTests.swift in Sources */,
2256222565
C856749A243F4292001A995E /* TenorMockDataHelper.swift in Sources */,
22566+
01E78D1D296EA54F00FB6863 /* StatsPeriodHelperTests.swift in Sources */,
2256322567
3FFE3C0828FE00D10021BB96 /* StatsSegmentedControlDataTests.swift in Sources */,
2256422568
D81C2F5820F86CEA002AE1F1 /* NetworkStatus.swift in Sources */,
2256522569
E1C545801C6C79BB001CEB0E /* MediaSettingsTests.swift in Sources */,
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import XCTest
2+
@testable import WordPress
3+
4+
final class StatsPeriodHelperTests: XCTestCase {
5+
private var sut: StatsPeriodHelper!
6+
7+
override func setUpWithError() throws {
8+
sut = StatsPeriodHelper()
9+
try? FeatureFlagOverrideStore().override(FeatureFlag.statsNewInsights, withValue: true)
10+
}
11+
12+
override func tearDownWithError() throws {
13+
sut = nil
14+
try? FeatureFlagOverrideStore().override(FeatureFlag.statsNewInsights, withValue: false)
15+
}
16+
17+
func testEndOfWeekWhenMondayIsSetAsFirstWeekday() {
18+
// A calendar with Monday as first weekday
19+
var calendar = Calendar(identifier: .gregorian)
20+
calendar.firstWeekday = 2
21+
22+
// Thursday
23+
let dateComponents = DateComponents(year: 2023, month: 02, day: 02, hour: 8, minute: 34)
24+
25+
let endDate = sut.calculateEndDate(
26+
from: calendar.date(from: dateComponents)!,
27+
offsetBy: 0,
28+
unit: .week,
29+
calendar: calendar
30+
)
31+
32+
// 2023-02-05 Sunday should be end of the week
33+
XCTAssertEqual(endDate?.dateAndTimeComponents().day, 5)
34+
}
35+
36+
func testEndOfWeekWhenSundayIsSetAsFirstWeekday() {
37+
// A calendar with Sunday as first weekday
38+
var calendar = Calendar(identifier: .gregorian)
39+
calendar.firstWeekday = 1
40+
41+
// Thursday
42+
let dateComponents = DateComponents(year: 2023, month: 02, day: 02, hour: 8, minute: 34)
43+
44+
let endDate = sut.calculateEndDate(
45+
from: calendar.date(from: dateComponents)!,
46+
offsetBy: 0,
47+
unit: .week,
48+
calendar: calendar
49+
)
50+
51+
// 2023-02-05 Sunday should still be end of the week
52+
XCTAssertEqual(endDate?.dateAndTimeComponents().day, 5)
53+
}
54+
55+
func testEndOfWeekWhenCurrentDateIsStartOfWeek() {
56+
// A calendar with Monday as first weekday
57+
var calendar = Calendar(identifier: .gregorian)
58+
calendar.firstWeekday = 2
59+
60+
// Start of Monday
61+
let dateComponents = DateComponents(year: 2021, month: 11, day: 15, hour: 00, minute: 00)
62+
63+
let endDate = sut.calculateEndDate(
64+
from: calendar.date(from: dateComponents)!,
65+
offsetBy: 0,
66+
unit: .week,
67+
calendar: calendar
68+
)
69+
70+
// 2021-11-21 Sunday should be end of the week
71+
XCTAssertEqual(endDate?.dateAndTimeComponents().day, 21)
72+
}
73+
74+
func testEndOfWeekWhenCurrentDateIsEndOfWeek() {
75+
// A calendar with Monday as first weekday
76+
var calendar = Calendar(identifier: .gregorian)
77+
calendar.firstWeekday = 2
78+
79+
// End of Sunday
80+
let dateComponents = DateComponents(year: 2021, month: 11, day: 21, hour: 23, minute: 59)
81+
82+
let endDate = sut.calculateEndDate(
83+
from: calendar.date(from: dateComponents)!,
84+
offsetBy: 0,
85+
unit: .week,
86+
calendar: calendar
87+
)
88+
89+
// 2021-11-21 Sunday should be end of the week
90+
XCTAssertEqual(endDate?.dateAndTimeComponents().day, 21)
91+
}
92+
93+
func testEndOfNextWeek() {
94+
// A calendar with Monday as first weekday
95+
var calendar = Calendar(identifier: .gregorian)
96+
calendar.firstWeekday = 2
97+
98+
// Thursday
99+
let dateComponents = DateComponents(year: 2023, month: 02, day: 02, hour: 8, minute: 34)
100+
101+
// Get end date of next week
102+
let endDate = sut.calculateEndDate(
103+
from: calendar.date(from: dateComponents)!,
104+
offsetBy: 1,
105+
unit: .week,
106+
calendar: calendar
107+
)
108+
109+
// 2023-02-12
110+
XCTAssertEqual(endDate?.dateAndTimeComponents().month, 2)
111+
XCTAssertEqual(endDate?.dateAndTimeComponents().day, 12)
112+
}
113+
114+
func testEndOfLastWeek() {
115+
// A calendar with Monday as first weekday
116+
var calendar = Calendar(identifier: .gregorian)
117+
calendar.firstWeekday = 2
118+
119+
// Thursday
120+
let dateComponents = DateComponents(year: 2023, month: 02, day: 02, hour: 8, minute: 34)
121+
122+
// Get end date of last week
123+
let endDate = sut.calculateEndDate(
124+
from: calendar.date(from: dateComponents)!,
125+
offsetBy: -1,
126+
unit: .week,
127+
calendar: calendar
128+
)
129+
130+
// 2023-01-29
131+
XCTAssertEqual(endDate?.dateAndTimeComponents().month, 1)
132+
XCTAssertEqual(endDate?.dateAndTimeComponents().day, 29)
133+
}
134+
}

0 commit comments

Comments
 (0)