area_id = $area_id; if (isset($area_id)) { $this->tzid = get_area_timezone($area_id); } } /** * Get the periods for a given area from the database. */ public static function getForArea(int $area_id) : self { static $result = []; // Cache for performance if (!isset($result[$area_id])) { $sql = "SELECT periods FROM " . _tbl('area') . " WHERE id=:id LIMIT 1"; $res = db()->query($sql, [':id' => $area_id]); if (false !== ($row = $res->next_row_keyed())) { $result[$area_id] = self::fromDbValue($area_id, $row['periods']); } } return clone $result[$area_id]; } /** * Convert a periods value as stored in the database to an instance of this class. */ public static function fromDbValue(int $area_id, string $value) : self { $result = new self($area_id); $array = json_decode($value, true); // The periods are stored in the database as either: // (a) a simple array of period names (the old way of storing periods // which we handle for backwards compatibility); or // (b) an associative array of period names and start/end times. if (is_assoc($array)) { foreach ($array as $period_name => $times) { $result->add(new Period($period_name, $times[0], $times[1])); } } else { foreach ($array as $period_name) { $result->add(new Period($period_name)); } } return $result; } /** * Convert the object to a value suitable for storing in the database. */ public function toDbValue() : string { $result = []; foreach ($this->data as $period) { $result[$period->name] = [$period->start, $period->end]; } return json_encode($result); } /** * Validate the periods, checking that the start and end times are valid and in ascending order. * * @return true|string */ public function validate() { foreach ($this->data as $i => $period) { // If we're not using times, everything is OK. if (($i === 0) && (!isset($period->start))) { return true; } // Otherwise check that the start and end times are valid and are in ascending order. try { if (false === ($start = DateTime::createFromFormat('H:i', $period->start))) { return get_vocab('invalid_period_start_time', $period->start, $period->name); } if (false === ($end = DateTime::createFromFormat('H:i', $period->end))) { return get_vocab('invalid_period_end_time', $period->end, $period->name); } if (isset($last_end_time) && ($start < $last_end_time)) { return get_vocab('period_start_before_last_end', $period->name); } if ($start >= $end) { return get_vocab('period_must_have_positive_duration', $period->name); } $last_end_time = $end; } catch (\Exception $e) { return get_vocab('invalid_period_time', $period->name); } } return true; } public function add(Period $period) : void { $this->data[] = $period; } public function current() : Period { return $this->data[$this->index]; } public function next() : void { $this->index++; } public function key(): int { return $this->index; } public function valid(): bool { return isset($this->data[$this->index]); } public function rewind() : void { $this->index = 0; } public function count() : int { return count($this->data); } public function seek($offset) : void { if ($offset < 0 || $offset >= $this->count()) { throw new \OutOfBoundsException("Invalid offset $offset"); } $this->index = $offset; } /** * Check whether the object has start and end times defined. */ public function hasTimes() : bool { return ($this->count() > 0) && ($this->data[0]->start !== null); } /** * Get a period by its index. Note that the iterator key is unchanged. */ public function offsetGet($offset) : Period { if ($offset < 0 || $offset >= $this->count()) { throw new \OutOfBoundsException("Invalid offset $offset"); } return $this->data[$offset]; } /** * Get a period by its nominal start time in seconds. Note that the * iterator key is unchanged. */ public function offsetGetByNominalSeconds(int $seconds) : Period { return $this->offsetGet(self::nominalSecondsToIndex($seconds)); } /** * Get a period by its timestamp. Note that the iterator key is unchanged. * * @param bool $previous If true, return the previous period, otherwise return this period. */ public function offsetGetByTimestamp(int $timestamp, bool $previous=false) : Period { $index = $this->timestampToIndex($timestamp); if ($previous) { $index--; } // Make sure we're within bounds return $this->offsetGet(max(0, min($index, $this->count()-1))); } public static function getHourByOffset(int $offset) : int { return 12 + intval($offset/60); } public static function getMinuteByOffset(int $offset) : int { return $offset % 60; } /** * Get the timestamp, as held in the entry table, of the start of the period at the given index on the given date. */ public function getStartTimestamp(int $index, DateTime $date) : int { return $date->setTime(12, 0)->getTimestamp() + ($index * 60); } /** * Get the timestamp, as held in the entry table, of the end of the period at the given index on the given date. */ public function getEndTimestamp(int $index, DateTime $date) : int { return $this->getStartTimestamp($index, $date) + 60; } /** * Convert a period to a real end time. * * @param int $timestamp The timestamp of the period. * * @return false|int False if the time is invalid, otherwise the real end time as a Unix timestamp. */ public function timestampToRealEnd(int $timestamp) : int { return $this->getRealTime($timestamp, $this->offsetGetByTimestamp($timestamp)->end); } /** * Convert a period to a real start time. * * @param int $timestamp The timestamp of the period. * * @return false|int False if the time is invalid, otherwise the real start time as a Unix timestamp. */ public function timestampToRealStart(int $timestamp) : int { return $this->getRealTime($timestamp, $this->offsetGetByTimestamp($timestamp)->start); } /** * Convert a period start or end time to a real time. * * @param int $timestamp The timestamp of the period * @param string $hhmm The time in the format HH:MM */ private function getRealTime(int $timestamp, string $hhmm) : int { if (!isset($this->tzid)) { throw new \Exception('No timezone set'); } return timestamp_set_time($timestamp, $hhmm, $this->tzid); } /** * Convert a period nominal start time in seconds (ie nominal seconds since * midnight, ignoring DST transitions) to an index into the periods array. */ private static function nominalSecondsToIndex(int $seconds) : int { // Periods are counted as minutes from noon, ie 1200 is $period[0], // 1201 $period[1], etc. return intval($seconds/60) - (12*60); } /** * Convert a timestamp to an index into the periods array. */ private function timestampToIndex(int $timestamp) : int { if (!isset($this->tzid)) { throw new \Exception('No timezone set'); } $noon = new DateTime('now', new DateTimeZone($this->tzid)); $noon->setTimestamp($timestamp); $noon->setTime(12, 0); return intval(($timestamp - $noon->getTimestamp())/60); } }