Skip to content
Merged
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
5 changes: 3 additions & 2 deletions src/CarbonEmission.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
use Entity;
use Location;
use CommonDBTM;
use DBmysqlIterator;

class CarbonEmission extends CommonDBChild
{
Expand Down Expand Up @@ -178,9 +179,9 @@ public function rawSearchOptions()
* @param integer $id
* @param DateTimeInterface|null $start
* @param DateTimeInterface|null $stop
* @return array
* @return DBmysqlIterator
*/
public function findGaps(string $itemtype, int $id, ?DateTimeInterface $start, ?DateTimeInterface $stop = null): array
public function findGaps(string $itemtype, int $id, ?DateTimeInterface $start, ?DateTimeInterface $stop = null): DBmysqlIterator
{
$criteria = [
'itemtype' => $itemtype,
Expand Down
5 changes: 3 additions & 2 deletions src/CarbonIntensity.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
use DateTimeImmutable;
use DateTimeInterface;
use DBmysql;
use DBmysqlIterator;
use GlpiPlugin\Carbon\Source;
use GlpiPlugin\Carbon\Zone;
use Glpi\DBAL\QueryParam;
Expand Down Expand Up @@ -394,9 +395,9 @@ public function save(string $zone_name, string $source_name, array $data): int
* @param integer $zone_id
* @param DateTimeInterface $start
* @param DateTimeInterface|null $stop
* @return array
* @return DBmysqlIterator
*/
public function findGaps(int $source_id, int $zone_id, DateTimeInterface $start, ?DateTimeInterface $stop = null): array
public function findGaps(int $source_id, int $zone_id, DateTimeInterface $start, ?DateTimeInterface $stop = null): DBmysqlIterator
{
$criteria = [
Source::getForeignKeyField() => $source_id,
Expand Down
2 changes: 1 addition & 1 deletion src/Impact/Embodied/Boavizta/Computer.php
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ protected function analyzeHardware(): array
);
if ($device_hard_drive_type !== false && $device_hard_drive_type->fields['name'] === 'removable') {
// Ignore removable storage (USB sticks, ...)
continue;
break;
}
$interface_type = new InterfaceType();
$interface_type->getFromDB($device_hard_drive->fields['interfacetypes_id']);
Expand Down
168 changes: 80 additions & 88 deletions src/Toolbox.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,13 @@
use DateTimeImmutable;
use DateTimeInterface;
use DBmysql;
use DBmysqlIterator;
use Glpi\Dashboard\Dashboard as GlpiDashboard;
use Infocom;
use Location;
use Glpi\DBAL\QueryExpression;
use Glpi\DBAL\QuerySubQuery;
use Glpi\DBAL\QueryUnion;
use Mexitek\PHPColors\Color;

class Toolbox
Expand Down Expand Up @@ -460,116 +462,106 @@ public static function isLocationExistForZone(string $name): bool
/**
* Gets date intervals where data are missing in a table
* To use with Mysql 8.0+ or MariaDB 10.2+
* Gaps are expressed as intervals like [X; Y[
* X is the 1st missing row, and Y is the first existing row which ends the gap
*
* @see https://bertwagner.com/posts/gaps-and-islands/
*
* @param string $table Table to search for gaps
* @param DateTimeInterface $start Start date to search
* @param DateInterval $interval Interval between each data sample (do not use intervals in months or years)
* @param DateTimeInterface|null $stop Stop date to search
* @param array $criteria Criterias for the SQL query
* @return array list of gaps
* @param string $table Table to search for gaps
* @param DateTimeInterface $start Start date to search
* @param DateInterval $interval Interval between each data sample (do not use intervals in months or years)
* @param DateTimeInterface|null $stop Stop date to search
* @param array $criteria Criterias for the SQL query
* @return DBmysqlIterator list of gaps
*/
public static function findTemporalGapsInTable(string $table, DateTimeInterface $start, DateInterval $interval, ?DateTimeInterface $stop = null, array $criteria = [])
public static function findTemporalGapsInTable(string $table, DateTimeInterface $start, DateInterval $interval, ?DateTimeInterface $stop = null, array $criteria = []): DBmysqlIterator
{
/** @var DBmysql $DB */
global $DB;

if ($interval->m !== 0 || $interval->y !== 0) {
throw new \InvalidArgumentException('Interval must be in days, hours, minutes or seconds');
}
// $interval_in_seconds = $interval->s + $interval->i * 60 + $interval->h * 3600 + $interval->d * 86400;
if ($stop === null) {
// Assume stop date is yesterday at midnight
$stop = new DateTime('yesterday midnight');
}
$sql_interval = self::dateIntervalToMySQLInterval($interval);

$start_string = $start->format('Y-m-d H:i:s');
$stop_string = $stop->format('Y-m-d H:i:s');
// Get start date as unix timestamp
$boundaries[] = new QueryExpression('`date` >= "' . $start->format('Y-m-d H:i:s') . '"');
$boundaries[] = new QueryExpression('`date` >= "' . $start_string . '"');
$boundaries[] = new QueryExpression('`date` < "' . $stop_string . '"');

// get stop date as unix timestamp
if ($stop === null) {
// Assume stop date is yesterday at midnight
$stop = new DateTime();
$stop->setTime(0, 0, 0);
$stop->sub(new DateInterval('P1D'));
}
$boundaries[] = new QueryExpression('`date` <= "' . $stop->format('Y-m-d H:i:s') . '"');
$common_criterias = array_merge($boundaries, $criteria);

// prepare sub query to get start and end date of an atomic date range
// An atomic date range is set to 1 hour
// To reduce problems with DST, we use the unix timestamp of the date
$atomic_ranges_subquery = new QuerySubQuery([
$records_query = new QuerySubQuery([
'SELECT' => [
new QueryExpression('`date` as `start_date`'),
new QueryExpression("DATE_ADD(`date`, $sql_interval) as `end_date`"),
'date',
new QueryExpression('LAG(`date`) OVER (ORDER BY `date`) AS `prev_date`')
],
'FROM' => $table,
'WHERE' => $criteria + $boundaries,
], 'atomic_ranges');

// For each atomic date range, find the end date of previous atomic date range
$groups_subquery = new QuerySubQuery([
'SELECT' => [
new QueryExpression('ROW_NUMBER() OVER (ORDER BY `start_date`, `end_date`) AS `row_number`'),
'start_date',
'end_date',
new QueryExpression('LAG(`end_date`, 1) OVER (ORDER BY `start_date`, `end_date`) AS `previous_end_date`')
'FROM' => $table,
'WHERE' => $common_criterias,
], 'records');

$request = new QueryUnion([
// Internal gaps (between existing records in the requred interval)
[
'SELECT' => [
new QueryExpression('`prev_date` + ' . $sql_interval . ' AS `start`'),
'date AS `end`'
],
'FROM' => $records_query,
'WHERE' => array_merge($boundaries, [
'NOT' => ['prev_date' => null],
// new QueryExpression('TIMESTAMPDIFF(SECOND, `records`.`prev_date`, `records`.`date`) > ' . $interval_in_seconds)
new QueryExpression('DATE_ADD(`records`.`prev_date`, ' . $sql_interval . ') < `date`')
]),
],
'FROM' => $atomic_ranges_subquery
], 'groups');

// For each atomic date range, find if it is the start of an island
$islands_subquery = new QuerySubQuery([
'SELECT' => [
'*',
new QueryExpression('SUM(CASE WHEN `groups`.`previous_end_date` >= `start_date` THEN 0 ELSE 1 END) OVER (ORDER BY `groups`.`row_number`) AS `ìsland_id`')
// Gap before the beginning of the serie
[
'SELECT' => [
new queryExpression('\'' . $start_string . '\' AS start'),
new queryExpression('MIN(`date`) AS `end`')
],
'FROM' => $table,
'WHERE' => $common_criterias,
'HAVING' => [
new QueryExpression("'" . $start_string . "' < MIN(`date`)")
],
],
'FROM' => $groups_subquery
], 'islands');

$request = [
'SELECT' => [
'MIN' => 'start_date as island_start_date',
'MAX' => 'end_date as island_end_date',
// Gap after the end of the serie
[
'SELECT' => [
new queryExpression('MAX(`date`) + ' . $sql_interval . ' AS `start`'),
new queryExpression('\'' . $stop_string . '\' AS `end`'),
],
'FROM' => $table,
'WHERE' => $common_criterias,
'HAVING' => [
new QueryExpression("DATE_SUB('" . $stop_string . "', " . $sql_interval . ") > MAX(`date`)")
],
],
'FROM' => $islands_subquery,
'GROUPBY' => ['ìsland_id'],
'ORDER' => ['island_start_date']
];

$result = $DB->request($request);
if ($result->count() === 0) {
// No island at all, the whole range is a gap
return [
[
'start' => $start->format('Y-m-d H:i:s'),
'end' => $stop->format('Y-m-d H:i:s'),
]
];
}

// Find gaps from islands
$expected_start_date = $start;
$gaps = [];
foreach ($result as $row) {
if ($expected_start_date < new DateTimeImmutable($row['island_start_date'])) {
// The current island starts after the expected start date
// Then there is a gap
$gaps[] = [
'start' => $expected_start_date->format('Y-m-d H:i:s'),
'end' => $row['island_start_date'],
];
}
$expected_start_date = new DateTimeImmutable($row['island_end_date']);
}
if ($expected_start_date < $stop) {
// The last island ends before the stop date
// Then there is a gap
$gaps[] = [
'start' => $expected_start_date->format('Y-m-d H:i:s'),
'end' => $stop->format('Y-m-d H:i:s'),
];
}

return $gaps;
// No record between the boundaries
[
'SELECT' => [
new QueryExpression('\'' . $start_string . '\' AS `start`'),
new QueryExpression('\'' . $stop_string . '\' AS `end`'),
],
'FROM' => $table,
'WHERE' => $common_criterias,
'HAVING' => [
new QueryExpression('COUNT(*) = 0'),
],
]
], true);

return $DB->request([
'FROM' => $request,
'ORDER' => 'start'
]);
}

/**
Expand Down
15 changes: 9 additions & 6 deletions tests/units/CarbonEmissionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ public function testFindGaps($timezone)

$start_date = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $start_date);
$stop_date = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $stop_date);
$gaps = $instance->findGaps($itemtype, $asset->getID(), $start_date, $stop_date);
$result = $instance->findGaps($itemtype, $asset->getID(), $start_date, $stop_date);
$result = iterator_to_array($result);
$expected = [
[
'start' => '2019-01-01 00:00:00',
Expand All @@ -95,7 +96,7 @@ public function testFindGaps($timezone)
'end' => '2023-12-31 00:00:00',
],
];
$this->assertEquals($expected, $gaps);
$this->assertEquals($expected, $result);

// Create a gap
$gap_start = '2020-04-05 00:00:00';
Expand All @@ -106,7 +107,8 @@ public function testFindGaps($timezone)
['date' => ['<=', $gap_end]],
]);

$gaps = $instance->findGaps($itemtype, $asset->getID(), $start_date, $stop_date);
$result = $instance->findGaps($itemtype, $asset->getID(), $start_date, $stop_date);
$result = iterator_to_array($result);
$expected = [
[
'start' => '2019-01-01 00:00:00',
Expand All @@ -121,7 +123,7 @@ public function testFindGaps($timezone)
'end' => '2023-12-31 00:00:00',
],
];
$this->assertEquals($expected, $gaps);
$this->assertEquals($expected, $result);

// Create an other gap
$gap_start_2 = '2020-06-20 00:00:00';
Expand All @@ -131,7 +133,8 @@ public function testFindGaps($timezone)
['date' => ['>=', $gap_start_2]],
['date' => ['<=', $gap_end_2]],
]);
$gaps = $instance->findGaps($itemtype, $asset->getID(), $start_date, $stop_date);
$result = $instance->findGaps($itemtype, $asset->getID(), $start_date, $stop_date);
$result = iterator_to_array($result);
$expected = [
[
'start' => '2019-01-01 00:00:00',
Expand All @@ -150,6 +153,6 @@ public function testFindGaps($timezone)
'end' => '2023-12-31 00:00:00',
],
];
$this->assertEquals($expected, $gaps);
$this->assertEquals($expected, $result);
}
}
Loading