Files
TimbreESP32/lib/timbre_core/ScheduleValidator.cpp
Angel Ivan 2a9eabce14 Fist commit
2026-10-02 12:08:21 -06:00

215 lines
8.1 KiB
C++
Executable File

#include "ScheduleValidator.h"
namespace timbre_core {
namespace {
ValidationResult failure(ValidationCode code, std::size_t index = 0) noexcept {
return ValidationResult(code, index);
}
} // namespace
const char* ValidationResult::message() const noexcept {
switch (code) {
case ValidationCode::Ok:
return "ok";
case ValidationCode::TooManyProfiles:
return "too many profiles";
case ValidationCode::TooManyPatterns:
return "too many patterns";
case ValidationCode::TooManySchedules:
return "too many schedules";
case ValidationCode::TooManyHolidays:
return "too many holidays";
case ValidationCode::InvalidProfileId:
return "invalid profile id";
case ValidationCode::DuplicateProfileId:
return "duplicate profile id";
case ValidationCode::InvalidPatternId:
return "invalid pattern id";
case ValidationCode::DuplicatePatternId:
return "duplicate pattern id";
case ValidationCode::InvalidPhaseCount:
return "invalid phase count";
case ValidationCode::InvalidPhaseDuration:
return "invalid phase duration";
case ValidationCode::PatternTotalTooLarge:
return "pattern total is too large";
case ValidationCode::InvalidHolidayId:
return "invalid holiday id";
case ValidationCode::DuplicateHolidayId:
return "duplicate holiday id";
case ValidationCode::InvalidHolidayDate:
return "invalid holiday date";
case ValidationCode::InvalidScheduleId:
return "invalid schedule id";
case ValidationCode::DuplicateScheduleId:
return "duplicate schedule id";
case ValidationCode::InvalidMinute:
return "invalid minute";
case ValidationCode::InvalidWeekdayMask:
return "invalid weekday mask";
case ValidationCode::UnknownProfile:
return "schedule references an unknown profile";
case ValidationCode::UnknownPattern:
return "schedule references an unknown pattern";
case ValidationCode::InvalidActiveProfile:
return "active profile does not exist";
}
return "validation error";
}
ValidationResult ScheduleValidator::validateProfile(const Profile& profile,
std::size_t index) noexcept {
if (profile.id == kInvalidId) {
return failure(ValidationCode::InvalidProfileId, index);
}
return ValidationResult{};
}
ValidationResult ScheduleValidator::validatePattern(const Pattern& pattern,
std::size_t index) noexcept {
if (!isPatternId(pattern.id)) {
return failure(ValidationCode::InvalidPatternId, index);
}
if (pattern.phaseCount > kMaxPatternPhases) {
return failure(ValidationCode::InvalidPhaseCount, index);
}
std::uint16_t total = 0;
for (std::size_t i = 0; i < pattern.phaseCount; ++i) {
if (!validPhaseDuration(pattern.phases[i])) {
return failure(ValidationCode::InvalidPhaseDuration, index);
}
total = static_cast<std::uint16_t>(total + pattern.phases[i]);
if (!validPatternTotal(total)) {
return failure(ValidationCode::PatternTotalTooLarge, index);
}
}
return ValidationResult{};
}
ValidationResult ScheduleValidator::validateHoliday(const Holiday& holiday,
std::size_t index) noexcept {
if (holiday.id == kInvalidId) {
return failure(ValidationCode::InvalidHolidayId, index);
}
if (!validHolidayDate(holiday.month, holiday.day)) {
return failure(ValidationCode::InvalidHolidayDate, index);
}
return ValidationResult{};
}
bool ScheduleValidator::validHolidayDate(std::uint8_t month, std::uint8_t day) noexcept {
// Holidays are recurring month/day values. February 29 is valid and is
// naturally inert in non-leap years.
if (month < 1 || month > 12 || day < 1) {
return false;
}
static constexpr std::uint8_t maximumDays[] = {
0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
};
return day <= maximumDays[month];
}
ValidationResult ScheduleValidator::validateSchedule(const Schedule& schedule,
const AppState& state,
std::size_t index) noexcept {
if (schedule.id == kInvalidId) {
return failure(ValidationCode::InvalidScheduleId, index);
}
if (!validMinute(schedule.minuteOfDay)) {
return failure(ValidationCode::InvalidMinute, index);
}
if (!validWeekdayMask(schedule.weekdayMask)) {
return failure(ValidationCode::InvalidWeekdayMask, index);
}
if (state.profile(schedule.profileId) == nullptr) {
return failure(ValidationCode::UnknownProfile, index);
}
if (state.pattern(schedule.patternId) == nullptr ||
!isPatternId(schedule.patternId)) {
return failure(ValidationCode::UnknownPattern, index);
}
const Pattern* pattern = state.pattern(schedule.patternId);
if (pattern == nullptr || !validatePattern(*pattern).valid) {
return failure(ValidationCode::UnknownPattern, index);
}
return ValidationResult{};
}
ValidationResult ScheduleValidator::validate(const AppState& state) noexcept {
if (state.profiles.size() > kMaxProfiles) {
return failure(ValidationCode::TooManyProfiles, state.profiles.size());
}
if (state.patterns.size() != kMaxPatterns) {
return failure(ValidationCode::TooManyPatterns, state.patterns.size());
}
if (state.schedules.size() > kMaxSchedules) {
return failure(ValidationCode::TooManySchedules, state.schedules.size());
}
if (state.holidays.size() > kMaxHolidays) {
return failure(ValidationCode::TooManyHolidays, state.holidays.size());
}
for (std::size_t i = 0; i < state.profiles.size(); ++i) {
const ValidationResult result = validateProfile(state.profiles[i], i);
if (!result.valid) {
return result;
}
for (std::size_t j = 0; j < i; ++j) {
if (state.profiles[j].id == state.profiles[i].id) {
return failure(ValidationCode::DuplicateProfileId, i);
}
}
}
for (std::size_t i = 0; i < state.patterns.size(); ++i) {
const ValidationResult result = validatePattern(state.patterns[i], i);
if (!result.valid) {
return result;
}
for (std::size_t j = 0; j < i; ++j) {
if (state.patterns[j].id == state.patterns[i].id) {
return failure(ValidationCode::DuplicatePatternId, i);
}
}
}
for (std::size_t i = 0; i < state.holidays.size(); ++i) {
const ValidationResult result = validateHoliday(state.holidays[i], i);
if (!result.valid) {
return result;
}
for (std::size_t j = 0; j < i; ++j) {
if (state.holidays[j].id == state.holidays[i].id) {
return failure(ValidationCode::DuplicateHolidayId, i);
}
if (state.holidays[j].enabled && state.holidays[i].enabled &&
state.holidays[j].month == state.holidays[i].month &&
state.holidays[j].day == state.holidays[i].day) {
return failure(ValidationCode::DuplicateHolidayId, i);
}
}
}
for (std::size_t i = 0; i < state.schedules.size(); ++i) {
const ValidationResult result = validateSchedule(state.schedules[i], state, i);
if (!result.valid) {
return result;
}
for (std::size_t j = 0; j < i; ++j) {
if (state.schedules[j].id == state.schedules[i].id) {
return failure(ValidationCode::DuplicateScheduleId, i);
}
}
}
if (state.profiles.size() != 0 && state.profile(state.activeProfileId) == nullptr) {
return failure(ValidationCode::InvalidActiveProfile, 0);
}
return ValidationResult{};
}
} // namespace timbre_core