Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bin/feedio
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ function read(FeedIo $feedIo, string $url)

$updateStats = $result->getUpdateStats();

echo "\033[32mMinimum interval between items : \033[34m".formatDateInterval($updateStats->getMedianInterval())."\033[0m" . PHP_EOL;
echo "\033[32mMinimum interval between items : \033[34m".formatDateInterval($updateStats->getMinInterval())."\033[0m" . PHP_EOL;
echo "\033[32mMedian interval : \033[34m".formatDateInterval($updateStats->getMedianInterval())."\033[0m" . PHP_EOL;
echo "\033[32mAverage interval : \033[34m".formatDateInterval($updateStats->getAverageInterval())."\033[0m" . PHP_EOL;
echo "\033[32mMaximum interval : \033[34m".formatDateInterval($updateStats->getMaxInterval())."\033[0m". PHP_EOL;
Expand Down
59 changes: 52 additions & 7 deletions src/FeedIo/Reader/Result/UpdateStats.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,22 @@ class UpdateStats

protected array $intervals = [];

protected int $newestItemDate = 0;

/**
* UpdateStats constructor.
* @param FeedInterface $feed
*/
public function __construct(
protected FeedInterface $feed
) {
$this->intervals = $this->computeIntervals($this->extractDates($feed));
$dates = $this->extractDates($feed);
if (count($dates) > 0) {
$this->newestItemDate = min(max($dates), time());
Copy link

Copilot AI Jul 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The expression min(max($dates), time()) is unclear. Consider adding a comment explaining why the newest date is capped at the current time, or extract this logic to a descriptively named method.

Suggested change
$this->newestItemDate = min(max($dates), time());
$this->newestItemDate = $this->getCappedNewestDate($dates);

Copilot uses AI. Check for mistakes.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

possible comment here could be "get the most recent item date that is not in the future"

} else {
$this->newestItemDate = $this->getFeedTimestamp();
}
$this->intervals = $this->computeIntervals($dates);
}

/**
Expand All @@ -57,7 +65,6 @@ public function computeNextUpdate(
if ($this->isSleepy($sleepyDuration, $marginRatio)) {
return (new \DateTime())->setTimestamp(time() + $sleepyDelay);
}
$feedTimeStamp = $this->getFeedTimestamp();
$now = time();
$intervals = [
$this->getAverageInterval(),
Expand All @@ -66,7 +73,7 @@ public function computeNextUpdate(
sort($intervals);
$newTimestamp = $now + $minDelay;
foreach ($intervals as $interval) {
$computedTimestamp = $this->addInterval($feedTimeStamp, $interval, $marginRatio);
$computedTimestamp = $this->addInterval($this->newestItemDate, $interval, $marginRatio);
if ($computedTimestamp > $now) {
$newTimestamp = $computedTimestamp;
break;
Expand All @@ -82,7 +89,7 @@ public function computeNextUpdate(
*/
public function isSleepy(int $sleepyDuration, float $marginRatio): bool
{
return time() > $this->addInterval($this->getFeedTimestamp(), $sleepyDuration, $marginRatio);
return time() > $this->addInterval($this->newestItemDate, $sleepyDuration, $marginRatio);
}

/**
Expand Down Expand Up @@ -125,7 +132,27 @@ public function getMaxInterval(): int
*/
public function getAverageInterval(): int
{
$total = array_sum($this->intervals);
sort($this->intervals);

$count = count($this->intervals);
if ($count === 0) {
return 0;
}

// some feeds could have very old historic
// articles so eliminate them with statistic
$q1 = $this->intervals[floor($count * 0.25)];
$q3 = $this->intervals[floor($count * 0.75)];
Comment on lines +144 to +145
Copy link

Copilot AI Jul 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using floor() for quartile calculation can cause index out of bounds when the array is small. For example, with count=1, floor(10.25)=0 is valid, but with count=2, floor(20.75)=1 accesses a valid index, but the logic doesn't handle edge cases consistently.

Suggested change
$q1 = $this->intervals[floor($count * 0.25)];
$q3 = $this->intervals[floor($count * 0.75)];
$q1_index = max(0, intdiv($count - 1, 4)); // Calculate Q1 index
$q3_index = min($count - 1, intdiv(3 * ($count - 1), 4)); // Calculate Q3 index
$q1 = $this->intervals[$q1_index];
$q3 = $this->intervals[$q3_index];

Copilot uses AI. Check for mistakes.

Comment on lines +144 to +145
Copy link

Copilot AI Jul 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same quartile calculation issue as Q1 - using floor() without bounds checking can lead to incorrect quartile values or potential array access issues in edge cases.

Suggested change
$q1 = $this->intervals[floor($count * 0.25)];
$q3 = $this->intervals[floor($count * 0.75)];
$q1_index = max(0, min($count - 1, intval(floor($count * 0.25))));
$q3_index = max(0, min($count - 1, intval(floor($count * 0.75))));
$q1 = $this->intervals[$q1_index];
$q3 = $this->intervals[$q3_index];

Copilot uses AI. Check for mistakes.

$iqr = $q3 - $q1;

$lower_bound = $q1 - 1.5 * $iqr;
$upper_bound = $q3 + 1.5 * $iqr;

$result = array_filter($this->intervals, function($value) use ($lower_bound, $upper_bound) {
return $value >= $lower_bound && $value <= $upper_bound;
});

$total = array_sum($result);

return count($this->intervals) ? intval(floor($total / count($this->intervals))) : 0;
Copy link

Copilot AI Jul 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The average calculation uses count($this->intervals) as divisor but $total is calculated from the filtered $result array. This will produce incorrect averages since the divisor should be count($result) to match the filtered data.

Suggested change
return count($this->intervals) ? intval(floor($total / count($this->intervals))) : 0;
return count($result) ? intval(floor($total / count($result))) : 0;

Copilot uses AI. Check for mistakes.

}
Expand All @@ -136,9 +163,27 @@ public function getAverageInterval(): int
public function getMedianInterval(): int
{
sort($this->intervals);
$num = floor(count($this->intervals) / 2);

return isset($this->intervals[$num]) ? $this->intervals[$num] : 0;
$count = count($this->intervals);
if ($count === 0) {
return 0;
}

$num = floor($count / 2);

if ($count % 2 === 0) {
return intval(floor(($this->intervals[$num - 1] + $this->intervals[$num]) / 2));
} else {
return $this->intervals[$num];
}
}

/**
* @return int
*/
public function getNewestItemDate(): int
{
return $this->newestItemDate;
}

private function computeIntervals(array $dates): array
Expand Down
4 changes: 3 additions & 1 deletion tests/FeedIo/Reader/Result/UpdateStatsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ public function testIntervals()
$this->assertEquals(86400, $stats->getMinInterval());
$nextUpdate = $stats->computeNextUpdate();
$averageInterval = $stats->getAverageInterval();
$this->assertEquals($feed->getLastModified()->getTimestamp() + intval($averageInterval + 0.1 * $averageInterval), $nextUpdate->getTimestamp());
$medianInterval = $stats->getMedianInterval();
$computedInterval = ($medianInterval < $averageInterval ? $medianInterval : $averageInterval);
$this->assertEquals($stats->getNewestItemDate() + intval($computedInterval + 0.1 * $computedInterval), $nextUpdate->getTimestamp());
}

public function testSleepyFeed()
Expand Down