124 lines
4.1 KiB
C++
Executable File
124 lines
4.1 KiB
C++
Executable File
#include "Scheduler.h"
|
|
|
|
#include "ScheduleValidator.h"
|
|
|
|
#include <limits>
|
|
|
|
namespace timbre_core {
|
|
|
|
void Scheduler::reset() noexcept {
|
|
hasLastKey_ = false;
|
|
lastKey_ = LocalMinuteKey{};
|
|
}
|
|
|
|
bool Scheduler::isHoliday(const AppState& state, std::uint8_t month,
|
|
std::uint8_t day) noexcept {
|
|
for (std::size_t i = 0; i < state.holidays.size(); ++i) {
|
|
const Holiday& holiday = state.holidays[i];
|
|
if (holiday.enabled && holiday.month == month && holiday.day == day) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Scheduler::scheduleMatches(const Schedule& schedule, const AppState& state,
|
|
const CivilDateTime& localTime) noexcept {
|
|
if (!schedule.enabled || schedule.minuteOfDay != localTime.minuteOfDay()) {
|
|
return false;
|
|
}
|
|
const Profile* activeProfile = state.profile(state.activeProfileId);
|
|
if (activeProfile == nullptr || !activeProfile->enabled ||
|
|
schedule.profileId != state.activeProfileId) {
|
|
return false;
|
|
}
|
|
const std::uint8_t weekday = CivilTime::weekdayForDate(localTime.year, localTime.month,
|
|
localTime.day);
|
|
if ((schedule.weekdayMask & static_cast<std::uint8_t>(1u << weekday)) == 0) {
|
|
return false;
|
|
}
|
|
if (isHoliday(state, localTime.month, localTime.day)) {
|
|
return false;
|
|
}
|
|
const Pattern* pattern = state.pattern(schedule.patternId);
|
|
return pattern != nullptr && isPatternId(schedule.patternId) &&
|
|
ScheduleValidator::validatePattern(*pattern).valid;
|
|
}
|
|
|
|
ScheduleDecision Scheduler::poll(const AppState& state,
|
|
const CivilDateTime& localTime,
|
|
bool firstObservation) {
|
|
ScheduleDecision decision;
|
|
decision.firstObservation = firstObservation;
|
|
|
|
if (!localTime.valid()) {
|
|
decision.invalidTime = true;
|
|
return decision;
|
|
}
|
|
|
|
const LocalMinuteKey currentKey = LocalMinuteKey::from(localTime);
|
|
const std::int64_t currentSerial = currentKey.serial();
|
|
if (currentSerial == std::numeric_limits<std::int64_t>::min()) {
|
|
decision.invalidTime = true;
|
|
return decision;
|
|
}
|
|
|
|
if (hasLastKey_) {
|
|
const std::int64_t previousSerial = lastKey_.serial();
|
|
if (currentSerial <= previousSerial) {
|
|
decision.alreadyProcessed = true;
|
|
return decision;
|
|
}
|
|
const std::int64_t distance = currentSerial - previousSerial;
|
|
decision.missedMinutes = distance > 1 ? static_cast<std::uint64_t>(distance - 1) : 0;
|
|
}
|
|
|
|
// Consume the current key before evaluating matches. Therefore a failed
|
|
// lookup (holiday, disabled profile, etc.) cannot be replayed later.
|
|
lastKey_ = currentKey;
|
|
hasLastKey_ = true;
|
|
|
|
if (firstObservation) {
|
|
return decision;
|
|
}
|
|
|
|
if (isHoliday(state, localTime.month, localTime.day)) {
|
|
decision.blockedByHoliday = true;
|
|
return decision;
|
|
}
|
|
|
|
const Schedule* selected = nullptr;
|
|
for (std::size_t i = 0; i < state.schedules.size(); ++i) {
|
|
const Schedule& candidate = state.schedules[i];
|
|
if (!scheduleMatches(candidate, state, localTime)) {
|
|
continue;
|
|
}
|
|
// Stable IDs, rather than array positions, define deterministic
|
|
// precedence when two schedules intentionally share a minute.
|
|
if (selected == nullptr || candidate.id < selected->id) {
|
|
selected = &candidate;
|
|
}
|
|
}
|
|
|
|
if (selected != nullptr) {
|
|
decision.due = true;
|
|
decision.scheduleId = selected->id;
|
|
decision.patternId = selected->patternId;
|
|
}
|
|
return decision;
|
|
}
|
|
|
|
ScheduleDecision Scheduler::poll(const AppState& state,
|
|
SessionClock& clock,
|
|
std::int64_t utcEpochSeconds) {
|
|
const SessionClock::Observation observation = clock.observe(utcEpochSeconds);
|
|
if (!observation.valid) {
|
|
ScheduleDecision decision;
|
|
decision.invalidTime = true;
|
|
return decision;
|
|
}
|
|
return poll(state, observation.local, observation.firstObservation);
|
|
}
|
|
|
|
} // namespace timbre_core
|