@@ -65,6 +65,281 @@ public function testSleepyFeed()
6565 }
6666
6767
68+ /**
69+ * Test getAverageInterval with a single interval
70+ * Edge case: count = 1
71+ */
72+ public function testAverageIntervalWithSingleInterval ()
73+ {
74+ $ feed = new Feed ();
75+ $ feed ->setLastModified (new \DateTime ('-1 day ' ));
76+
77+ // Two items = 1 interval
78+ $ item1 = new Feed \Item ();
79+ $ item1 ->setLastModified (new \DateTime ('-1 day ' ));
80+ $ feed ->add ($ item1 );
81+
82+ $ item2 = new Feed \Item ();
83+ $ item2 ->setLastModified (new \DateTime ('-2 days ' ));
84+ $ feed ->add ($ item2 );
85+
86+ $ stats = new UpdateStats ($ feed );
87+ $ intervals = $ stats ->getIntervals ();
88+
89+ $ this ->assertCount (1 , $ intervals );
90+ $ this ->assertIsInt ($ stats ->getAverageInterval ());
91+ $ this ->assertGreaterThanOrEqual (0 , $ stats ->getAverageInterval ());
92+ }
93+
94+ /**
95+ * Test getAverageInterval with two intervals
96+ * Edge case: count = 2
97+ */
98+ public function testAverageIntervalWithTwoIntervals ()
99+ {
100+ $ feed = new Feed ();
101+ $ feed ->setLastModified (new \DateTime ('-1 day ' ));
102+
103+ // Three items = 2 intervals
104+ $ item1 = new Feed \Item ();
105+ $ item1 ->setLastModified (new \DateTime ('-1 day ' ));
106+ $ feed ->add ($ item1 );
107+
108+ $ item2 = new Feed \Item ();
109+ $ item2 ->setLastModified (new \DateTime ('-2 days ' ));
110+ $ feed ->add ($ item2 );
111+
112+ $ item3 = new Feed \Item ();
113+ $ item3 ->setLastModified (new \DateTime ('-3 days ' ));
114+ $ feed ->add ($ item3 );
115+
116+ $ stats = new UpdateStats ($ feed );
117+ $ intervals = $ stats ->getIntervals ();
118+
119+ $ this ->assertCount (2 , $ intervals );
120+ $ this ->assertIsInt ($ stats ->getAverageInterval ());
121+ $ this ->assertGreaterThanOrEqual (0 , $ stats ->getAverageInterval ());
122+ }
123+
124+ /**
125+ * Test getAverageInterval with three intervals
126+ * Edge case: count = 3
127+ */
128+ public function testAverageIntervalWithThreeIntervals ()
129+ {
130+ $ feed = new Feed ();
131+ $ feed ->setLastModified (new \DateTime ('-1 day ' ));
132+
133+ // Four items = 3 intervals
134+ $ item1 = new Feed \Item ();
135+ $ item1 ->setLastModified (new \DateTime ('-1 day ' ));
136+ $ feed ->add ($ item1 );
137+
138+ $ item2 = new Feed \Item ();
139+ $ item2 ->setLastModified (new \DateTime ('-2 days ' ));
140+ $ feed ->add ($ item2 );
141+
142+ $ item3 = new Feed \Item ();
143+ $ item3 ->setLastModified (new \DateTime ('-3 days ' ));
144+ $ feed ->add ($ item3 );
145+
146+ $ item4 = new Feed \Item ();
147+ $ item4 ->setLastModified (new \DateTime ('-4 days ' ));
148+ $ feed ->add ($ item4 );
149+
150+ $ stats = new UpdateStats ($ feed );
151+ $ intervals = $ stats ->getIntervals ();
152+
153+ $ this ->assertCount (3 , $ intervals );
154+ $ this ->assertIsInt ($ stats ->getAverageInterval ());
155+ $ this ->assertGreaterThanOrEqual (0 , $ stats ->getAverageInterval ());
156+ }
157+
158+ /**
159+ * Test getAverageInterval with four intervals
160+ * Edge case: count = 4 (minimum for proper quartile calculation)
161+ */
162+ public function testAverageIntervalWithFourIntervals ()
163+ {
164+ $ feed = new Feed ();
165+ $ feed ->setLastModified (new \DateTime ('-1 day ' ));
166+
167+ // Five items = 4 intervals
168+ $ item1 = new Feed \Item ();
169+ $ item1 ->setLastModified (new \DateTime ('-1 day ' ));
170+ $ feed ->add ($ item1 );
171+
172+ $ item2 = new Feed \Item ();
173+ $ item2 ->setLastModified (new \DateTime ('-2 days ' ));
174+ $ feed ->add ($ item2 );
175+
176+ $ item3 = new Feed \Item ();
177+ $ item3 ->setLastModified (new \DateTime ('-3 days ' ));
178+ $ feed ->add ($ item3 );
179+
180+ $ item4 = new Feed \Item ();
181+ $ item4 ->setLastModified (new \DateTime ('-4 days ' ));
182+ $ feed ->add ($ item4 );
183+
184+ $ item5 = new Feed \Item ();
185+ $ item5 ->setLastModified (new \DateTime ('-5 days ' ));
186+ $ feed ->add ($ item5 );
187+
188+ $ stats = new UpdateStats ($ feed );
189+ $ intervals = $ stats ->getIntervals ();
190+
191+ $ this ->assertCount (4 , $ intervals );
192+ $ this ->assertIsInt ($ stats ->getAverageInterval ());
193+ $ this ->assertGreaterThanOrEqual (0 , $ stats ->getAverageInterval ());
194+ }
195+
196+ /**
197+ * Test getAverageInterval with empty intervals
198+ * Edge case: count = 0
199+ */
200+ public function testAverageIntervalWithNoIntervals ()
201+ {
202+ $ feed = new Feed ();
203+ $ feed ->setLastModified (new \DateTime ('-1 day ' ));
204+
205+ // Single item = 0 intervals
206+ $ item = new Feed \Item ();
207+ $ item ->setLastModified (new \DateTime ('-1 day ' ));
208+ $ feed ->add ($ item );
209+
210+ $ stats = new UpdateStats ($ feed );
211+ $ intervals = $ stats ->getIntervals ();
212+
213+ $ this ->assertCount (0 , $ intervals );
214+ $ this ->assertEquals (0 , $ stats ->getAverageInterval ());
215+ }
216+
217+ /**
218+ * Test getAverageInterval with outliers that should be filtered
219+ */
220+ public function testAverageIntervalWithOutliers ()
221+ {
222+ $ feed = new Feed ();
223+ $ feed ->setLastModified (new \DateTime ('-1 day ' ));
224+
225+ // Create items with one extreme outlier
226+ $ dates = [
227+ '-1 day ' ,
228+ '-2 days ' ,
229+ '-3 days ' ,
230+ '-4 days ' ,
231+ '-5 days ' ,
232+ '-100 days ' , // Outlier
233+ ];
234+
235+ foreach ($ dates as $ date ) {
236+ $ item = new Feed \Item ();
237+ $ item ->setLastModified (new \DateTime ($ date ));
238+ $ feed ->add ($ item );
239+ }
240+
241+ $ stats = new UpdateStats ($ feed );
242+ $ intervals = $ stats ->getIntervals ();
243+
244+ $ this ->assertCount (5 , $ intervals );
245+
246+ // The average should be reasonable and not heavily skewed by the outlier
247+ $ average = $ stats ->getAverageInterval ();
248+ $ this ->assertIsInt ($ average );
249+ $ this ->assertGreaterThanOrEqual (0 , $ average );
250+
251+ // Verify the outlier filtering works by checking that average is close to 86400 (1 day)
252+ // and not close to the raw average which would be much higher
253+ $ rawAverage = array_sum ($ intervals ) / count ($ intervals );
254+ $ this ->assertLessThan ($ rawAverage , $ average );
255+ }
256+
257+ /**
258+ * Test that Q1 and Q3 indices don't go out of bounds
259+ */
260+ public function testQuartileIndicesWithinBounds ()
261+ {
262+ // Test with various array sizes
263+ $ testCases = [1 , 2 , 3 , 4 , 5 , 10 , 100 ];
264+
265+ foreach ($ testCases as $ itemCount ) {
266+ $ feed = new Feed ();
267+ $ feed ->setLastModified (new \DateTime ('-1 day ' ));
268+
269+ // Add items
270+ for ($ i = 0 ; $ i < $ itemCount + 1 ; $ i ++) {
271+ $ item = new Feed \Item ();
272+ $ item ->setLastModified (new \DateTime ("- {$ i } days " ));
273+ $ feed ->add ($ item );
274+ }
275+
276+ $ stats = new UpdateStats ($ feed );
277+
278+ // This should not throw any errors
279+ $ average = $ stats ->getAverageInterval ();
280+ $ this ->assertIsInt ($ average );
281+ $ this ->assertGreaterThanOrEqual (0 , $ average );
282+ }
283+ }
284+
285+ /**
286+ * Test getAverageInterval with uniform intervals
287+ */
288+ public function testAverageIntervalWithUniformIntervals ()
289+ {
290+ $ feed = new Feed ();
291+ $ feed ->setLastModified (new \DateTime ('-1 hour ' ));
292+
293+ // Create items with exactly 1 hour intervals
294+ for ($ i = 1 ; $ i <= 10 ; $ i ++) {
295+ $ item = new Feed \Item ();
296+ $ item ->setLastModified (new \DateTime ("- {$ i } hours " ));
297+ $ feed ->add ($ item );
298+ }
299+
300+ $ stats = new UpdateStats ($ feed );
301+ $ intervals = $ stats ->getIntervals ();
302+
303+ $ this ->assertCount (9 , $ intervals );
304+
305+ // All intervals should be 3600 seconds (1 hour)
306+ foreach ($ intervals as $ interval ) {
307+ $ this ->assertEquals (3600 , $ interval );
308+ }
309+
310+ // Average should be 3600
311+ $ this ->assertEquals (3600 , $ stats ->getAverageInterval ());
312+ }
313+
314+ /**
315+ * Test that the implementation handles edge cases safely
316+ */
317+ public function testBothImplementationsAreSafe ()
318+ {
319+ for ($ itemCount = 1 ; $ itemCount <= 20 ; $ itemCount ++) {
320+ $ feed = new Feed ();
321+ $ feed ->setLastModified (new \DateTime ('-1 day ' ));
322+
323+ // Add items with varied intervals
324+ for ($ i = 0 ; $ i <= $ itemCount ; $ i ++) {
325+ $ item = new Feed \Item ();
326+ $ item ->setLastModified (new \DateTime ("- " . ($ i * 2 ) . " hours " ));
327+ $ feed ->add ($ item );
328+ }
329+
330+ $ stats = new UpdateStats ($ feed );
331+
332+ // This should never throw an error
333+ try {
334+ $ average = $ stats ->getAverageInterval ();
335+ $ this ->assertIsInt ($ average );
336+ $ this ->assertGreaterThanOrEqual (0 , $ average );
337+ } catch (\Throwable $ e ) {
338+ $ this ->fail ("getAverageInterval() threw an exception with {$ itemCount } intervals: " . $ e ->getMessage ());
339+ }
340+ }
341+ }
342+
68343 private function getDates (): array
69344 {
70345 return [
0 commit comments