Skip to content

Commit 1034b29

Browse files
authored
Fill in gaps when events are split. (#32)
1 parent de2a9fe commit 1034b29

8 files changed

+153
-19
lines changed

CHANGELOG.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ All Notable changes to BAT will be documented in this file
1111
- Nothing
1212

1313
### Fixed
14-
- Nothing
14+
- The event stores now ensure new events splitting existing events do not leave empty gaps between day boundaries and the new event.
1515

1616
### Removed
1717
- Nothing
1818

1919
### Security
20-
- Nothing
20+
- Nothing

src/Calendar/AbstractCalendar.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ abstract class AbstractCalendar implements CalendarInterface {
3030
/**
3131
* The class that will access the actual event store where event data is held.
3232
*
33-
* @var
33+
* @var \Roomify\Bat\Store\StoreInterface
3434
*/
3535
protected $store;
3636

src/Calendar/CalendarInterface.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,18 @@ interface CalendarInterface {
2323
* @param $end_date
2424
* The end date of our range
2525
*
26-
* @return EventInterface[]
26+
* @return \Roomify\Bat\Event\EventInterface[]
2727
* An array of Event objects
2828
*/
2929
public function getEvents(\DateTime $start_date, \DateTime $end_date);
3030

3131
/**
3232
* Given an array of Events the calendar is updated with the relevant data.
3333
*
34-
* @param EventInterface[] $events
34+
* @param \Roomify\Bat\Event\EventInterface[] $events
3535
* An array of events to update the calendar with
3636
*
37-
* @param granularity
37+
* @param $granularity
3838
* The leverl of detail (one of HOURLY, DAILY) at which to store the event
3939
*/
4040
public function addEvents($events, $granularity);

src/Event/AbstractEvent.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Roomify\Bat\Event\EventInterface;
1111
use Roomify\Bat\Store\Store;
1212
use Roomify\Bat\EventFormatter\EventFormatter;
13+
use Roomify\Bat\Store\StoreInterface;
1314

1415
abstract class AbstractEvent implements EventInterface {
1516

@@ -466,12 +467,12 @@ public function itemize($itemizer) {
466467
/**
467468
* Saves an event using the Store object
468469
*
469-
* @param Store $store
470+
* @param StoreInterface $store
470471
* @param string $granularity
471472
*
472473
* @return boolean
473474
*/
474-
public function saveEvent(Store $store, $granularity = AbstractEvent::BAT_HOURLY) {
475+
public function saveEvent(StoreInterface $store, $granularity = AbstractEvent::BAT_HOURLY) {
475476
return $store->storeEvent($this, $granularity);
476477
}
477478

src/Store/DrupalDBStore.php

+11
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,21 @@ public function storeEvent(EventInterface $event, $granularity = Event::BAT_HOUR
8888
$transaction = db_transaction();
8989
}
9090

91+
// Get existing event data from db
92+
$existing_events = $this->getEventData($event->getStartDate(), $event->getEndDate(), array($event->getUnitId()));
93+
9194
try {
9295
// Itemize an event so we can save it
9396
$itemized = $event->itemize(new EventItemizer($event, $granularity));
9497

9598
//Write days
9699
foreach ($itemized[Event::BAT_DAY] as $year => $months) {
97100
foreach ($months as $month => $days) {
101+
if ($granularity === Event::BAT_HOURLY) {
102+
foreach ($days as $day => $value) {
103+
$this->itemizeSplitDay($existing_events, $itemized, $value, $event->getUnitId(), $year, $month, $day);
104+
}
105+
}
98106
if (class_exists('Drupal') && floatval(\Drupal::VERSION) >= 9) {
99107
\Drupal\Core\Database\Database::getConnection()->merge($this->day_table_no_prefix)
100108
->key(array(
@@ -124,6 +132,9 @@ public function storeEvent(EventInterface $event, $granularity = Event::BAT_HOUR
124132
foreach ($days as $day => $hours) {
125133
// Count required as we may receive empty hours for granular events that start and end on midnight
126134
if (count($hours) > 0) {
135+
foreach ($hours as $hour => $value){
136+
$this->itemizeSplitHour($existing_events, $itemized, $value, $event->getUnitId(), $year, $month, $day, $hour);
137+
}
127138
if (class_exists('Drupal') && floatval(\Drupal::VERSION) >= 9) {
128139
\Drupal\Core\Database\Database::getConnection()->merge($this->hour_table_no_prefix)
129140
->key(array(

src/Store/SqlLiteDBStore.php

+20-10
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,10 @@ public function getEventData(\DateTime $start_date, \DateTime $end_date, $unit_i
8686
*/
8787
public function storeEvent(EventInterface $event, $granularity = Event::BAT_HOURLY) {
8888
$stored = TRUE;
89+
$unit_id = $event->getUnitId();
8990

9091
// Get existing event data from db
91-
$existing_events = $this->getEventData($event->getStartDate(), $event->getEndDate(), array($event->getUnitId()));
92+
$existing_events = $this->getEventData($event->getStartDate(), $event->getEndDate(), array($unit_id));
9293

9394
try {
9495
// Itemize an event so we can save it
@@ -100,17 +101,20 @@ public function storeEvent(EventInterface $event, $granularity = Event::BAT_HOUR
100101
$values = array_values($days);
101102
$keys = array_keys($days);
102103
// Because SQLite does not have a nice merge first we have to check if a row exists to determine whether to do an insert or an update
103-
if (isset($existing_events[$event->getUnitId()][EVENT::BAT_DAY][$year][$month])) {
104+
if (isset($existing_events[$unit_id][EVENT::BAT_DAY][$year][$month])) {
104105
$command = "UPDATE $this->day_table SET ";
105106
foreach ($days as $day => $value) {
107+
if ($granularity === Event::BAT_HOURLY) {
108+
$this->itemizeSplitDay($existing_events, $itemized, $value, $unit_id, $year, $month, $day);
109+
}
106110
$command .= "$day = $value,";
107111
}
108112
$command = rtrim($command, ',');
109-
$command .= " WHERE unit_id = " . $event->getUnitId() . " AND year = $year AND month = $month";
113+
$command .= " WHERE unit_id = " . $unit_id . " AND year = $year AND month = $month";
110114
$this->pdo->exec($command);
111115
}
112116
else {
113-
$this->pdo->exec("INSERT INTO $this->day_table (unit_id, year, month, " . implode(', ', $keys) . ") VALUES (" . $event->getUnitId() . ", $year, $month, " . implode(', ', $values) . ")");
117+
$this->pdo->exec("INSERT INTO $this->day_table (unit_id, year, month, " . implode(', ', $keys) . ") VALUES (" . $unit_id . ", $year, $month, " . implode(', ', $values) . ")");
114118
}
115119
}
116120
}
@@ -124,16 +128,22 @@ public function storeEvent(EventInterface $event, $granularity = Event::BAT_HOUR
124128
if (count($hours) > 0) {
125129
$values = array_values($hours);
126130
$keys = array_keys($hours);
127-
if (isset($existing_events[$event->getUnitId()][EVENT::BAT_HOUR][$year][$month][$day])) {
131+
if (isset($existing_events[$unit_id][EVENT::BAT_HOUR][$year][$month][$day])) {
128132
$command = "UPDATE $this->hour_table SET ";
129133
foreach ($hours as $hour => $value){
134+
$this->itemizeSplitHour($existing_events, $itemized, $value, $unit_id, $year, $month, $day, $hour);
130135
$command .= "$hour = $value,";
131136
}
132137
$command = rtrim($command, ',');
133-
$command .= " WHERE unit_id = " . $event->getUnitId() . " AND year = $year AND month = $month AND day = " . substr($day,1);
138+
$command .= " WHERE unit_id = " . $unit_id . " AND year = $year AND month = $month AND day = " . substr($day,1);
134139
$this->pdo->exec($command);
135140
} else {
136-
$this->pdo->exec("INSERT INTO $this->hour_table (unit_id, year, month, day, " . implode(', ', $keys) . ") VALUES (" . $event->getUnitId() . ", $year, $month, " . substr($day, 1) . ", " . implode(', ', $values) . ")");
141+
if (isset($existing_events[$unit_id][EVENT::BAT_DAY][$year][$month][$day])) {
142+
foreach ($hours as $hour => $value) {
143+
$this->itemizeSplitHour($existing_events, $itemized, $value, $unit_id, $year, $month, $day, $hour);
144+
}
145+
}
146+
$this->pdo->exec("INSERT INTO $this->hour_table (unit_id, year, month, day, " . implode(', ', $keys) . ") VALUES (" . $unit_id . ", $year, $month, " . substr($day, 1) . ", " . implode(', ', $values) . ")");
137147
}
138148
}
139149
}
@@ -147,16 +157,16 @@ public function storeEvent(EventInterface $event, $granularity = Event::BAT_HOUR
147157
foreach ($hours as $hour => $minutes) {
148158
$values = array_values($minutes);
149159
$keys = array_keys($minutes);
150-
if (isset($existing_events[$event->getUnitId()][EVENT::BAT_MINUTE][$year][$month][$day][$hour])) {
160+
if (isset($existing_events[$unit_id][EVENT::BAT_MINUTE][$year][$month][$day][$hour])) {
151161
$command = "UPDATE $this->minute_table SET ";
152162
foreach ($minutes as $minute => $value){
153163
$command .= "$minute = $value,";
154164
}
155165
$command = rtrim($command, ',');
156-
$command .= " WHERE unit_id = " . $event->getUnitId() . " AND year = $year AND month = $month AND day = " . substr($day,1) . " AND hour = " . substr($hour,1);
166+
$command .= " WHERE unit_id = " . $unit_id . " AND year = $year AND month = $month AND day = " . substr($day,1) . " AND hour = " . substr($hour,1);
157167
$this->pdo->exec($command);
158168
} else {
159-
$this->pdo->exec("INSERT INTO $this->minute_table (unit_id, year, month, day, hour, " . implode(', ', $keys) . ") VALUES (" . $event->getUnitId() . ", $year, $month, " . substr($day, 1) . ", " . substr($hour, 1) . ", " . implode(', ', $values) . ")");
169+
$this->pdo->exec("INSERT INTO $this->minute_table (unit_id, year, month, day, hour, " . implode(', ', $keys) . ") VALUES (" . $unit_id . ", $year, $month, " . substr($day, 1) . ", " . substr($hour, 1) . ", " . implode(', ', $values) . ")");
160170
}
161171
}
162172
}

src/Store/Store.php

+76-1
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,86 @@
77

88
namespace Roomify\Bat\Store;
99

10-
use Roomify\Bat\Store\StoreInterface;
10+
use Roomify\Bat\Event\Event;
1111

1212
/**
1313
* The basic Store class
1414
*/
1515
abstract class Store implements StoreInterface {
1616

17+
/**
18+
* Fill in hourly values from existing events when a day is being split.
19+
*
20+
* $existing_events must contain an existing event for the unit the same day.
21+
* This only needs to be called for hourly granularity.
22+
*
23+
* @param array $existing_events
24+
* Existing event data from ::getEventData().
25+
* @param array $itemized
26+
* The new event itemized. Values from existing overlapping events will be
27+
* inserted into it.
28+
* @param int $value
29+
* The value of the event being added.
30+
* @param int $unit_id
31+
* The unit the event being added.
32+
* @param int $year
33+
* Year of the event.
34+
* @param int $month
35+
* A month of the event.
36+
* @param int $day
37+
* Day of the event.
38+
*/
39+
protected function itemizeSplitDay(array &$existing_events, array &$itemized, $value, $unit_id, $year, $month, $day) {
40+
$existing_value = $existing_events[$unit_id][EVENT::BAT_DAY][$year][$month][$day];
41+
if ($value === -1 && $existing_value > 0) {
42+
$itemized_day = &$itemized[Event::BAT_HOUR][$year][$month][$day];
43+
for ($hour = 0; $hour < 24; $hour++) {
44+
$hour_key = 'h' . $hour;
45+
$var = &$itemized_day[$hour_key];
46+
$var = isset($var) && $var != 0 ? $var : $existing_value;
47+
}
48+
}
49+
}
50+
51+
/**
52+
* Fill in minute values from existing events when an hour is being split.
53+
*
54+
* $existing_events must contain an existing event for the unit during either
55+
* the same hour or day.
56+
*
57+
* @param array $existing_events
58+
* Existing event data from ::getEventData().
59+
* @param array $itemized
60+
* The new event itemized. Values from existing overlapping events will be
61+
* inserted into it.
62+
* @param int $value
63+
* The value of the event being added.
64+
* @param int $unit_id
65+
* The unit the event being added.
66+
* @param int $year
67+
* Year of the event.
68+
* @param int $month
69+
* A month of the event.
70+
* @param int $day
71+
* A day of the event.
72+
* @param int $hour
73+
* An hour in which an existing event overlaps.
74+
*/
75+
protected function itemizeSplitHour(array $existing_events, array &$itemized, $value, $unit_id, $year, $month, $day, $hour) {
76+
if (isset($existing_events[$unit_id][EVENT::BAT_HOUR][$year][$month][$day][$hour])) {
77+
$existing_value = $existing_events[$unit_id][EVENT::BAT_HOUR][$year][$month][$day][$hour];
78+
}
79+
else {
80+
$existing_value = $existing_events[$unit_id][EVENT::BAT_DAY][$year][$month][$day];
81+
}
82+
if ($value === -1 && $existing_value > 0) {
83+
$itemized_hour = &$itemized[Event::BAT_MINUTE][$year][$month][$day][$hour];
84+
for ($minute = 0; $minute < 60; $minute++) {
85+
$minute_key = 'm' . str_pad($minute, 2, '0', STR_PAD_LEFT);
86+
$var = &$itemized_hour[$minute_key];
87+
$var = isset($var) && $var != 0 ? $var : $existing_value;
88+
}
89+
}
90+
}
91+
1792
}

tests/CalendarTest.php

+37
Original file line numberDiff line numberDiff line change
@@ -891,4 +891,41 @@ public function testDstTransition2() {
891891
$this->assertEquals($events[1][1]->getValue(), 11);
892892
}
893893

894+
public function testPartialOverlap() {
895+
$u1 = new Unit(1, 0, array());
896+
$units = array($u1);
897+
$store = new SqlLiteDBStore($this->pdo, 'availability_event', SqlDBStore::BAT_STATE);
898+
$calendar = new Calendar($units, $store);
899+
900+
$sd = new \DateTime('2020-07-12 00:00');
901+
$ed = new \DateTime('2020-07-13 23:59');
902+
// Base event to be split.
903+
$sd1 = new \DateTime('2020-07-12 00:00');
904+
$ed1 = new \DateTime('2020-07-13 23:59');
905+
$e1s11 = new Event($sd1, $ed1, $u1, 11);
906+
// Splits an existing day.
907+
$sd2 = new \DateTime('2020-07-12 01:00');
908+
$ed2 = new \DateTime('2020-07-12 03:00');
909+
$e2s22 = new Event($sd2, $ed2, $u1, 22);
910+
// Splits an existing hour when day is already split.
911+
$sd3 = new \DateTime('2020-07-12 04:20');
912+
$ed3 = new \DateTime('2020-07-12 04:25');
913+
$e3s33 = new Event($sd3, $ed3, $u1, 33);
914+
// Splits an hour in an existing day not previously split.
915+
$sd4 = new \DateTime('2020-07-13 04:20');
916+
$ed4 = new \DateTime('2020-07-13 04:25');
917+
$e4s44 = new Event($sd4, $ed4, $u1, 44);
918+
919+
$calendar->addEvents(array($e1s11, $e2s22, $e3s33, $e4s44), Event::BAT_HOURLY);
920+
$events = $calendar->getEvents($sd, $ed);
921+
922+
$this->assertEquals(11, $events[1][0]->getValue());
923+
$this->assertEquals(22, $events[1][1]->getValue());
924+
$this->assertEquals(11, $events[1][2]->getValue());
925+
$this->assertEquals(33, $events[1][3]->getValue());
926+
$this->assertEquals(11, $events[1][4]->getValue());
927+
$this->assertEquals(44, $events[1][5]->getValue());
928+
$this->assertEquals(11, $events[1][4]->getValue());
929+
}
930+
894931
}

0 commit comments

Comments
 (0)