Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
87.76% |
43 / 49 |
|
50.00% |
2 / 4 |
CRAP | |
0.00% |
0 / 1 |
ComputeEditingStreaks | |
87.76% |
43 / 49 |
|
50.00% |
2 / 4 |
13.31 | |
0.00% |
0 / 1 |
getEditingStreaks | |
100.00% |
38 / 38 |
|
100.00% |
1 / 1 |
9 | |||
getLongestEditingStreak | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
makeDatePeriod | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
isDateAdjacent | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace GrowthExperiments\UserImpact; |
4 | |
5 | use DateInterval; |
6 | use DatePeriod; |
7 | use DateTime; |
8 | use DateTimeInterface; |
9 | use Exception; |
10 | |
11 | /* |
12 | * Utility class for processing "Editing streaks", which are consecutive days when a user made edits. |
13 | * |
14 | * The minimum unit for an editing streak is a single day. So if a user has made a single edit, the editing streak |
15 | * is for the date of that edit. |
16 | */ |
17 | class ComputeEditingStreaks { |
18 | |
19 | /** |
20 | * Given an array of dates => edit counts (see UserImpact::getEditCountsByDay), generate an array of |
21 | * EditingStreak objects. An "editing streak" consists of a date range (minimum unit is one day) with a start and |
22 | * end date, and a total edit count number for that streak. |
23 | * |
24 | * @param array $editCountByDay @see UserImpact::getEditCountByDay() |
25 | * @return EditingStreak[] |
26 | * @throws Exception |
27 | */ |
28 | public static function getEditingStreaks( array $editCountByDay ): array { |
29 | $editingStreaks = []; |
30 | // Create an initial, empty editing streak to start with. |
31 | // This is also needed in the event that $editCountByDay is empty. |
32 | $editingStreak = new EditingStreak(); |
33 | |
34 | // Iterate over each row in the edit count data. At the start of each loop, |
35 | // we have an EditingStreak object. Check if its date is adjacent to the row's |
36 | // date time. |
37 | // - If it is adjacent, increment the total edit count for the streak, and increment |
38 | // the end period for the streak. |
39 | // - if not adjacent, add the editing streak to the list of streaks, and create a new |
40 | // object |
41 | foreach ( $editCountByDay as $dateStringIndex => $editCountForDate ) { |
42 | try { |
43 | $currentRowDateTime = new DateTime( $dateStringIndex ); |
44 | } catch ( \Exception $exception ) { |
45 | // silently discard row if date is invalid. |
46 | continue; |
47 | } |
48 | if ( !is_int( $editCountForDate ) || $editCountForDate === 0 ) { |
49 | // silently discard row if edit count format is invalid or 0. |
50 | continue; |
51 | } |
52 | |
53 | if ( $editingStreak->getDatePeriod() && |
54 | $editingStreak->getDatePeriod()->getEndDate() && |
55 | !self::isDateAdjacent( |
56 | $currentRowDateTime, |
57 | // isDateAdjacent takes two DateTimeInterface objects. |
58 | // getEndDate() can somehow return null (even though constructing a DatePeriod |
59 | // requires an end date?), but anyway, we know there is an end date because |
60 | // we just checked for one directly above this conditional. |
61 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable |
62 | $editingStreak->getDatePeriod()->getEndDate() |
63 | ) ) { |
64 | $editingStreaks[] = $editingStreak; |
65 | $editingStreak = new EditingStreak( |
66 | new DatePeriod( |
67 | $currentRowDateTime, |
68 | new DateInterval( 'P1D' ), |
69 | // We need to set an end time, so set it to the current row's date by default. |
70 | $currentRowDateTime |
71 | ), |
72 | $editCountForDate |
73 | ); |
74 | continue; |
75 | } |
76 | $editingStreak->setTotalEditCountForPeriod( |
77 | $editingStreak->getTotalEditCountForPeriod() + $editCountForDate |
78 | ); |
79 | $editingStreak->setDatePeriod( |
80 | new DatePeriod( |
81 | $editingStreak->getDatePeriod() ? |
82 | $editingStreak->getDatePeriod()->getStartDate() : |
83 | $currentRowDateTime, |
84 | new DateInterval( 'P1D' ), |
85 | $currentRowDateTime |
86 | ) |
87 | ); |
88 | } |
89 | // Add the last processed edit streak to the list. |
90 | // Also catches the special case of a single item in the editCountByDay array. |
91 | $editingStreaks[] = $editingStreak; |
92 | return $editingStreaks; |
93 | } |
94 | |
95 | /** |
96 | * Get the editing streak with the most consecutive days in a given set of edit count by day data. |
97 | * |
98 | * @param array $editCountByDay |
99 | * @return EditingStreak |
100 | * @throws Exception |
101 | */ |
102 | public static function getLongestEditingStreak( |
103 | array $editCountByDay |
104 | ): EditingStreak { |
105 | $editingStreaks = self::getEditingStreaks( $editCountByDay ); |
106 | usort( $editingStreaks, static function ( EditingStreak $a, EditingStreak $b ) { |
107 | return $a->getStreakNumberOfDays() < $b->getStreakNumberOfDays() ? 1 : -1; |
108 | } ); |
109 | return reset( $editingStreaks ); |
110 | } |
111 | |
112 | /** |
113 | * Utility method for constructing a DatePeriod. |
114 | * |
115 | * @param string $start ISO 8601 date, e.g. '2022-08-25'. |
116 | * @param string $end ISO 8601 date |
117 | * @return DatePeriod |
118 | * @throws Exception |
119 | */ |
120 | public static function makeDatePeriod( string $start, string $end ): DatePeriod { |
121 | return new DatePeriod( |
122 | new DateTime( $start ), |
123 | new DateInterval( 'P1D' ), |
124 | new DateTime( $end ) |
125 | ); |
126 | } |
127 | |
128 | /** |
129 | * Utility method to see if a given date is adjacent to another one. |
130 | * |
131 | * Examples: |
132 | * - "2022-10-01" is adjacent to "2022-09-30" |
133 | * - "2022-10-01" is not adjacent to "2022-10-03" |
134 | * @param DateTimeInterface $dateOne |
135 | * @param DateTimeInterface $dateTwo |
136 | * @return bool True if adjacent, false otherwise |
137 | */ |
138 | private static function isDateAdjacent( |
139 | DateTimeInterface $dateOne, DateTimeInterface $dateTwo |
140 | ): bool { |
141 | return $dateOne->diff( $dateTwo )->days === 1; |
142 | } |
143 | } |