From 8edb7b633369cd04151525066ab0f8b512da7702 Mon Sep 17 00:00:00 2001 From: Maxim Prokhorov Date: Thu, 22 Aug 2024 23:29:07 +0300 Subject: [PATCH] sch: relative time spec for events Reference calendar time of other schedules, user-created named events or the sun{rise,set} (when enabled) For example - '15m before sunset' - '30m after cal#0' - '1h15m after "foobar"' Empty time spec is allowed, defaults to '1m' 'before sunrise' is the same as '1m before sunrise' Internals are reworked to handle a more generalized 'Event' type, based on the 'time point' and 'event' base classes fron sunrise and sunset Sunrise and sunset should also track 'last' event as well as 'next (not currently displayed anywhere, though) --- code/espurna/scheduler.cpp | 783 ++++++++++++++++++--- code/espurna/scheduler_common.ipp | 474 +++++++++---- code/espurna/scheduler_time.re | 295 ++++++++ code/espurna/scheduler_time.re.ipp | 658 +++++++++++++++++ code/html/src/template-sch.html | 1 + code/test/unit/src/scheduler/scheduler.cpp | 301 ++++++-- 6 files changed, 2223 insertions(+), 289 deletions(-) diff --git a/code/espurna/scheduler.cpp b/code/espurna/scheduler.cpp index f984966e8d..86a6b01f3e 100644 --- a/code/espurna/scheduler.cpp +++ b/code/espurna/scheduler.cpp @@ -47,11 +47,11 @@ Copyright (C) 2019-2024 by Maxim Prokhorov named_events; + +NamedEvent* find_named(StringView name) { + const auto it = std::find_if( + named_events.begin(), + named_events.end(), + [&](const NamedEvent& entry) { + return entry.name == name; + }); + + if (it != named_events.end()) { + return &(*it); + } + + return nullptr; +} + +bool named_event(String name, datetime::Seconds seconds) { + auto it = find_named(name); + if (it) { + it->time_point = make_time_point(seconds); + return true; + } + + size_t size { 0 }; + + auto last = named_events.begin(); + while (last != named_events.end()) { + ++size; + ++last; + } + + if (size < EventsMax) { + named_events.push_front( + NamedEvent{ + .name = std::move(name), + .time_point = make_time_point(seconds), + }); + + return true; + } + + return false; +} -// scheduler itself has minutes precision, while seconds are used in debug and calculations -struct Event { - datetime::Minutes minutes{ -1 }; - datetime::Seconds seconds{ -1 }; +void cleanup_named_events(const datetime::Context& ctx) { + named_events.remove_if( + [&](const NamedEvent& entry) { + return !event::is_valid(entry.time_point) + || (datetime::Seconds(ctx.timestamp) - to_seconds(entry.time_point)) > EventTtl; + }); +} + +constexpr auto LastTtl = datetime::Days{ 1 }; + +struct Last { + size_t index; + datetime::Minutes minutes; }; -datetime::Seconds event_seconds(const Event& event) { - return std::chrono::duration_cast(event.minutes) + event.seconds; +std::forward_list last_minutes; + +Last* find_last(size_t index) { + auto it = std::find_if( + last_minutes.begin(), + last_minutes.end(), + [&](const Last& entry) { + return entry.index == index; + }); + + if (it != last_minutes.end()) { + return &(*it); + } + + return nullptr; +} + +void action_timestamp(size_t index, datetime::Minutes minutes) { + auto* it = find_last(index); + if (it) { + it->minutes = minutes; + return; + } + + last_minutes.push_front( + Last{ + .index = index, + .minutes = minutes, + }); } -bool event_valid(const Event& event) { - return (event.minutes > datetime::Minutes::zero()) - && (event.seconds > datetime::Seconds::zero()); +void action_timestamp(const datetime::Context& ctx, size_t index) { + action_timestamp( + index, to_minutes(datetime::Seconds(ctx.timestamp))); } -struct EventMatch { +datetime::Minutes action_timestamp(size_t index) { + auto it = find_last(index); + if (it) { + return it->minutes; + } + + return datetime::Minutes{ -1 }; +} + +void cleanup_action_timestamps(const datetime::Context& ctx) { + const auto minutes = to_minutes(ctx); + last_minutes.remove_if( + [&](const Last& last) { + return (minutes - last.minutes) > LastTtl; + }); +} + +#if SCHEDULER_SUN_SUPPORT +namespace sun { + +struct EventMatch : public Event { datetime::Date date; TimeMatch time; - Event last; }; struct Match { @@ -153,11 +258,13 @@ using espurna::settings::options::Enumeration; STRING_VIEW_INLINE(Unknown, "unknown"); STRING_VIEW_INLINE(Disabled, "disabled"); STRING_VIEW_INLINE(Calendar, "calendar"); +STRING_VIEW_INLINE(Relative, "relative"); -static constexpr std::array, 3> Types PROGMEM { - {{Type::Unknown, Unknown}, - {Type::Disabled, Disabled}, - {Type::Calendar, Calendar}} +static constexpr std::array, 4> Types PROGMEM { + {{Type::Unknown, Unknown}, + {Type::Disabled, Disabled}, + {Type::Calendar, Calendar}, + {Type::Relative, Relative}} }; namespace v1 { @@ -324,17 +431,40 @@ Schedule schedule(size_t index) { return parse_schedule(settings::time(index)); } -size_t count() { - size_t out { 0 }; +Relative relative(size_t index) { + return parse_relative(settings::time(index)); +} +template +void foreach_type(T&& callback) { for (size_t index = 0; index < build::max(); ++index) { const auto type = settings::type(index); if (type == Type::Unknown) { break; } - ++out; + callback(type); } +} + +using Types = std::vector; + +Types types() { + Types out; + + foreach_type([&](Type type) { + out.push_back(type); + }); + + return out; +} + +size_t count() { + size_t out { 0 }; + + foreach_type([&](Type type) { + ++out; + }); return out; } @@ -589,49 +719,30 @@ EventMatch* find_event_match(const Schedule& schedule) { return find_event_match(schedule.time); } -tm time_point_from_seconds(datetime::Seconds seconds) { +tm make_utc_date_time(datetime::Seconds seconds) { tm out{}; + time_t timestamp{ seconds.count() }; gmtime_r(×tamp, &out); return out; } -Event make_invalid_event() { - Event out; - - out.seconds = datetime::Seconds{ -1 }; - out.minutes = datetime::Minutes{ -1 }; - - return out; -} - -Event make_event(datetime::Seconds seconds) { - Event out; - - out.seconds = seconds; - out.minutes = - std::chrono::duration_cast(out.seconds); - out.seconds -= out.minutes; - - return out; -} - -datetime::Date date_point(const tm& time_point) { +datetime::Date make_date(const tm& date_time) { datetime::Date out; - out.year = time_point.tm_year + 1900; - out.month = time_point.tm_mon + 1; - out.day = time_point.tm_mday; + out.year = date_time.tm_year + 1900; + out.month = date_time.tm_mon + 1; + out.day = date_time.tm_mday; return out; } -TimeMatch time_match(const tm& time_point) { +TimeMatch make_time_match(const tm& date_time) { TimeMatch out; - out.hour[time_point.tm_hour] = true; - out.minute[time_point.tm_min] = true; + out.hour[date_time.tm_hour] = true; + out.minute[date_time.tm_min] = true; out.flags = FlagUtc; return out; @@ -639,15 +750,20 @@ TimeMatch time_match(const tm& time_point) { void update_event_match(EventMatch& match, datetime::Seconds seconds) { if (seconds <= datetime::Seconds::zero()) { - match.last = make_invalid_event(); + if (event::is_valid(match.next)) { + match.last = match.next; + } + + match.next = TimePoint{}; return; } - const auto time_point = time_point_from_seconds(seconds); - match.date = date_point(time_point); - match.time = time_match(time_point); + const auto date_time = make_utc_date_time(seconds); + match.date = make_date(date_time); + match.time = make_time_match(date_time); - match.last = make_event(seconds); + match.last = match.next; + match.next = make_time_point(seconds); } void update_schedule_from(Schedule& schedule, const EventMatch& match) { @@ -665,7 +781,7 @@ bool update_schedule(Schedule& schedule) { } // in case calculation failed, no use here - if (!event_valid((*selected).last)) { + if (!event::is_valid((*selected).next)) { return false; } @@ -679,8 +795,8 @@ bool update_schedule(Schedule& schedule) { } bool needs_update(datetime::Minutes minutes) { - return ((match.rising.last.minutes < minutes) - || (match.setting.last.minutes < minutes)); + return (match.rising.next.minutes < minutes) + || (match.setting.next.minutes < minutes); } template @@ -729,7 +845,7 @@ void update(time_t timestamp, const tm& today, T&& compare) { String format_match(const EventMatch& match) { return datetime::format_local_tz( - datetime::make_context(event_seconds(match.last))); + datetime::make_context(event::to_seconds(match.next))); } // check() needs current or future events, discard timestamps in the past @@ -758,12 +874,12 @@ void update_after(const datetime::Context& ctx) { update(minutes, ctx.utc, CheckCompare{}); - if (match.rising.last.minutes.count() > 0) { + if (match.rising.next.minutes.count() > 0) { DEBUG_MSG_P(PSTR("[SCH] Sunrise at %s\n"), format_match(match.rising).c_str()); } - if (match.setting.last.minutes.count() > 0) { + if (match.setting.next.minutes.count() > 0) { DEBUG_MSG_P(PSTR("[SCH] Sunset at %s\n"), format_match(match.setting).c_str()); } @@ -781,7 +897,7 @@ namespace terminal { namespace internal { String sunrise_sunset(const sun::EventMatch& match) { - if (match.last.minutes > datetime::Minutes::zero()) { + if (match.next.minutes > datetime::Minutes::zero()) { return sun::format_match(match); } @@ -809,13 +925,11 @@ void dump_sunrise_sunset(::terminal::CommandContext& ctx) { } // namespace internal #endif +// SCHEDULE [] PROGMEM_STRING(Dump, "SCHEDULE"); void dump(::terminal::CommandContext&& ctx) { if (ctx.argv.size() != 2) { -#if SCHEDULER_SUN_SUPPORT - internal::dump_sunrise_sunset(ctx); -#endif settingsDump(ctx, settings::Settings); return; } @@ -826,12 +940,83 @@ void dump(::terminal::CommandContext&& ctx) { return; } + const auto last = find_last(id); + if (last) { + ctx.output.printf_P(PSTR("last action: %s\n"), + datetime::format_local(datetime::Seconds((*last).minutes).count()).c_str()); + } + settingsDump(ctx, settings::IndexedSettings, id); terminalOK(ctx); } +PROGMEM_STRING(Event, "EVENT"); + +// EVENT [] [] +void event(::terminal::CommandContext&& ctx) { + String name; + + if (ctx.argv.size() == 2) { + name = std::move(ctx.argv[1]); + } + + if (ctx.argv.size() != 3) { + bool once { true }; + for (auto& entry : named_events) { + if (name.length() && entry.name != name) { + continue; + } + + if (once) { + ctx.output.print(PSTR("Named events:\n")); + once = false; + } + + const auto seconds = to_seconds(entry.time_point); + ctx.output.printf_P(PSTR("\"%s\" at %s\n"), + entry.name.c_str(), + datetime::format_local_tz(seconds.count()).c_str()); + + if (name.length()) { + terminalOK(ctx); + return; + } + } + + if (name.length()) { + terminalError(ctx, STRING_VIEW("Invalid name")); + return; + } + +#if SCHEDULER_SUN_SUPPORT + ctx.output.print(PSTR("Sun events:\n")); + internal::dump_sunrise_sunset(ctx); +#endif + + terminalOK(ctx); + return; + } + + datetime::DateHhMmSs datetime; + bool utc { false }; + + const auto result = parse_simple_iso8601(datetime, utc, ctx.argv[2]); + if (!result) { + terminalError(ctx, STRING_VIEW("Invalid datetime")); + return; + } + + if (!named_event(std::move(ctx.argv[1]), to_seconds(datetime, utc))) { + terminalError(ctx, STRING_VIEW("Cannot add more events")); + return; + } + + terminalOK(ctx); +} + static constexpr ::terminal::Command Commands[] PROGMEM { {Dump, dump}, + {Event, event}, }; void setup() { @@ -1204,11 +1389,31 @@ void parse_action(String action) { if (!espurna::terminal::api_find_and_call(action, output, error)) { DEBUG_MSG_P(PSTR("[SCH] %s\n"), error.c_str()); + return; } } #endif +Schedule load_schedule(size_t index) { + auto out = settings::schedule(index); + if (!out.ok) { + return out; + } + +#if SCHEDULER_SUN_SUPPORT + if (want_sunrise_sunset(out.time) && !sun::update_schedule(out)) { + out.ok = false; + } +#else + if (want_sunrise_sunset(out.time)) { + out.ok = false; + } +#endif + + return out; +} + namespace restore { [[gnu::used]] @@ -1255,7 +1460,7 @@ void run_delta(Context& ctx) { } for (auto it = ctx.pending.begin(); it != ctx.pending.end();) { - if (handle_delta(ctx, *it)) { + if (handle_pending(ctx, *it)) { it = ctx.pending.erase(it); } else { it = std::next(it); @@ -1272,6 +1477,7 @@ void run_today(Context& ctx) { return; case Type::Disabled: + case Type::Relative: continue; case Type::Calendar: @@ -1289,7 +1495,7 @@ void run_today(Context& ctx) { #if SCHEDULER_SUN_SUPPORT if (!sun::update_schedule(schedule)) { - context_pending(ctx, index, schedule); + ctx.push_pending(index, schedule); continue; } #else @@ -1302,21 +1508,13 @@ void run_today(Context& ctx) { } } -void sort(Context& ctx) { - std::sort( - ctx.results.begin(), - ctx.results.end(), - [](const Result& lhs, const Result& rhs) { - return lhs.offset < rhs.offset; - }); -} - void run(const datetime::Context& base) { Context ctx{ base }; run_today(ctx); run_delta(ctx); - sort(ctx); + + ctx.sort(); for (auto& result : ctx.results) { const auto action = settings::action(result.index); @@ -1329,60 +1527,425 @@ void run(const datetime::Context& base) { } // namespace restore -void check(const datetime::Context& ctx) { - for (size_t index = 0; index < build::max(); ++index) { - switch (settings::type(index)) { - case Type::Unknown: - return; +namespace expect { - case Type::Disabled: +} // namespace expect + +namespace relative { + +struct Source { + constexpr static datetime::Minutes DefaultMinutes{ -1 }; + + virtual ~Source(); + + virtual const datetime::Minutes& minutes() const = 0; + + virtual bool before(const datetime::Context&) { + return true; + } + + virtual bool after(const datetime::Context&) { + return true; + } +}; + +constexpr datetime::Minutes Source::DefaultMinutes; + +Source::~Source() = default; + +struct Calendar : public Source { + Calendar(size_t index, std::shared_ptr expect) : + _expect(expect), + _index(index) + {} + + const datetime::Minutes& minutes() const override { + return _minutes; + } + + bool before(const datetime::Context&) override; + bool after(const datetime::Context&) override; + +private: + void _reset_minutes(const datetime::Context& ctx, datetime::Minutes offset) { + _minutes = to_minutes(ctx) + offset; + } + + void _reset_minutes(const datetime::Context& ctx, const expect::Context& expect) { + _reset_minutes(ctx, expect.results.back().offset); + } + + std::shared_ptr _expect; + size_t _index; + + datetime::Minutes _minutes{ DefaultMinutes }; +}; + +bool Calendar::before(const datetime::Context& ctx) { + if (event::is_valid(_minutes)) { + return true; + } + + const auto it = std::find_if( + _expect->results.begin(), + _expect->results.end(), + [&](const Offset& offset) { + return offset.index == _index; + }); + + if (it != _expect->results.end()) { + _reset_minutes(ctx, (*it).offset); + return true; + } + + const auto schedule = load_schedule(_index); + if (!schedule.ok) { + return false; + } + + if ((_expect->days == _expect->days.zero()) && handle_today(*_expect, _index, schedule)) { + _reset_minutes(ctx, *_expect); + return true; + } + + const auto pending = std::find_if( + _expect->pending.begin(), + _expect->pending.end(), + [&](const Pending& pending) { + return pending.index == _index; + }); + + if (pending == _expect->pending.end()) { + return false; + } + + if (handle_pending(*_expect, *pending)) { + _reset_minutes(ctx, *_expect); + return true; + } + + // assuming this only happen once, after +1day shift + _expect->pending.erase(pending); + + return false; +} + +bool Calendar::after(const datetime::Context& ctx) { + _minutes = action_timestamp(_index); + return event::is_valid(_minutes); +} + +struct Named : public Source { + explicit Named(String&& name) : + _name(std::move(name)) + {} + + const datetime::Minutes& minutes() const override { + return _minutes; + } + + bool before(const datetime::Context&) override; + bool after(const datetime::Context&) override; + +private: + bool _reset_minutes(StringView name) { + const auto it = find_named(name); + if (it) { + _minutes = it->time_point.minutes; + } + + return event::is_valid(_minutes); + } + + String _name; + datetime::Minutes _minutes{ -1 }; +}; + +bool Named::before(const datetime::Context&) { + return event::is_valid(_minutes) || _reset_minutes(_name); +} + +bool Named::after(const datetime::Context&) { + return event::is_valid(_minutes) || _reset_minutes(_name); +} + +#if SCHEDULER_SUN_SUPPORT +struct Sun : public Source { + explicit Sun(const Event* event) : + _event(event) + {} + + bool before(const datetime::Context&) override { + return _reset_minutes(_event->next); + } + + bool after(const datetime::Context&) override { + return _reset_minutes(_event->last); + } + + const datetime::Minutes& minutes() const override { + return *_minutes; + } + +private: + bool _reset_minutes(const TimePoint& time_point) { + if (event::is_valid(time_point)) { + _minutes = &(time_point.minutes); + } else { + _minutes = &DefaultMinutes; + } + + return event::is_valid(*_minutes); + } + + const Event* _event; + const datetime::Minutes* _minutes { &DefaultMinutes }; +}; + +struct Sunset : public Sun { + Sunset() : + Sun(&sun::match.setting) + {} +}; + +struct Sunrise : public Sun { + Sunrise() : + Sun(&sun::match.rising) + {} +}; +#endif + +struct EventOffset : public Offset { + std::unique_ptr source; + relative::Order order; +}; + +using EventOffsets = std::vector; + +void process_valid_event_offsets(const datetime::Context& ctx, EventOffsets& pending, relative::Order order) { + std::vector matched; + + auto it = pending.begin(); + while (it != pending.end()) { + if ((*it).order != order) { + goto next; + } + + // expect required event time point ('next' or 'last') to exist for the requested 'order' + { + const auto& minutes = (*it).source->minutes(); + if (!event::is_valid(minutes)) { + goto next; + } + + const auto diff = event::difference(ctx, minutes); + if (diff == (*it).offset) { + matched.push_back((*it).index); + } + } + + // always fall-through and erase + it = pending.erase(it); + continue; + + // only move where when explicitly told to +next: + it = std::next(it); + continue; + } + + for (const auto& match : matched) { + parse_action(settings::action(match)); + } +} + +struct Prepared { + Span types; + EventOffsets event_offsets; + std::shared_ptr expect; + + explicit operator bool() const { + return event_offsets.size() > 0; + } + + bool next() { + return expect.get() != nullptr + && expect.use_count() > 1 + && expect->pending.size() + && expect->next(); + } +}; + +Prepared prepare_event_offsets(const datetime::Context& ctx, Span types) { + Prepared out{ + .types = types, + .event_offsets = {}, + .expect = {}, + }; + + for (size_t index = 0; index < types.size(); ++index) { + if (scheduler::Type::Relative != types[index]) { continue; + } - case Type::Calendar: - break; + auto relative = settings::relative(index); + if (Type::None == relative.type) { + continue; } - auto schedule = settings::schedule(index); - if (!schedule.ok) { + if (Order::None == relative.order) { continue; } + EventOffset tmp; + tmp.index = index; + tmp.order = relative.order; + tmp.offset = relative.offset; + + if (Order::Before == relative.order) { + tmp.offset = -tmp.offset; + } + + switch (relative.type) { + case Type::None: + continue; + + case Type::Calendar: + if (!out.expect) { + out.expect = std::make_unique(ctx); + } + tmp.source = + std::make_unique(relative.data, out.expect); + break; + + case Type::Named: + tmp.source = + std::make_unique(std::move(relative.name)); + break; + + case Type::Sunrise: #if SCHEDULER_SUN_SUPPORT - if (!sun::update_schedule(schedule)) { + tmp.source = std::make_unique(); + break; +#else continue; - } +#endif + + case Type::Sunset: +#if SCHEDULER_SUN_SUPPORT + tmp.source = std::make_unique(); + break; #else - if (want_sunrise_sunset(schedule.time)) { continue; - } #endif + } - const auto& time = select_time(ctx, schedule); + if (tmp.source) { + out.event_offsets.push_back(std::move(tmp)); + } + } - if (!match(schedule.date, time)) { - continue; + return out; +} + +void handle_ordered(const datetime::Context& ctx, Prepared& prepared, Order order) { + auto it = prepared.event_offsets.begin(); + while (it != prepared.event_offsets.end()) { + if ((*it).order != order) { + goto next; } - if (!match(schedule.weekdays, time)) { - continue; + switch (order) { + case Order::None: + goto next; + + case Order::Before: + if (!(*it).source->before(ctx)) { + goto erase; + } + goto next; + + case Order::After: + if (!(*it).source->after(ctx)) { + goto erase; + } + goto next; + } - if (!match(schedule.time, time)) { +erase: + it = prepared.event_offsets.erase(it); + continue; + +next: + it = std::next(it); + continue; + } +} + +void handle_before(const datetime::Context& ctx, Prepared& prepared) { + handle_ordered(ctx, prepared, Order::Before); + if (prepared.next()) { + handle_ordered(ctx, prepared, Order::Before); + } +} + +void handle_after(const datetime::Context& ctx, Prepared& prepared) { + handle_ordered(ctx, prepared, Order::After); +} + +} // namespace relative + +bool match_schedule(const Schedule& schedule, const tm& time_point) { + if (!match(schedule.date, time_point)) { + return false; + } + + if (!match(schedule.weekdays, time_point)) { + return false; + } + + return match(schedule.time, time_point); +} + +bool check_calendar(const datetime::Context& ctx, size_t index) { + const auto schedule = load_schedule(index); + return schedule.ok + && match_schedule(schedule, select_time(ctx, schedule)); +} + +void handle_calendar(const datetime::Context& ctx, Span types) { + for (size_t index = 0; index < types.size(); ++index) { + bool ok = false; + + switch (types[index]) { + case Type::Unknown: + return; + + case Type::Disabled: + case Type::Relative: continue; + + case Type::Calendar: + ok = check_calendar(ctx, index); + break; } - DEBUG_MSG_P(PSTR("[SCH] Action #%zu triggered\n"), index); - parse_action(settings::action(index)); + if (ok) { + action_timestamp(ctx, index); + parse_action(settings::action(index)); + } } } void tick(NtpTick tick) { - if (tick != NtpTick::EveryMinute) { + auto ctx = datetime::make_context(now()); + if (tick == NtpTick::EveryHour) { + cleanup_action_timestamps(ctx); + cleanup_named_events(ctx); return; } - auto ctx = datetime::make_context(now()); - if (initial) { initial = false; settings::gc(settings::count()); @@ -1393,7 +1956,23 @@ void tick(NtpTick tick) { sun::update_after(ctx); #endif - check(ctx); + const auto types = settings::types(); + auto prepared = + relative::prepare_event_offsets(ctx, make_span(types)); + + if (prepared) { + relative::handle_before(ctx, prepared); + relative::process_valid_event_offsets( + ctx, prepared.event_offsets, relative::Order::Before); + } + + handle_calendar(ctx, prepared.types); + + if (prepared) { + relative::handle_after(ctx, prepared); + relative::process_valid_event_offsets( + ctx, prepared.event_offsets, relative::Order::After); + } } void setup() { diff --git a/code/espurna/scheduler_common.ipp b/code/espurna/scheduler_common.ipp index 0bb818598f..5f67a9407c 100644 --- a/code/espurna/scheduler_common.ipp +++ b/code/espurna/scheduler_common.ipp @@ -312,12 +312,12 @@ constexpr int first_set_u64(uint64_t value) { return __builtin_ffsll(value); } -// Returns the number of leading 0-bits in x, starting at the most significant bit position. If x is 0, the result is undefined. +// Returns one plus the index of the most significant 1-bit of x, or if x is zero, returns zero. constexpr int last_set_u32(uint32_t value) { return value ? (32 - __builtin_clz(value)) : 0; } -// Returns the number of leading 0-bits in x, starting at the most significant bit position. If x is 0, the result is undefined. +// Returns one plus the index of the most significant 1-bit of x, or if x is zero, returns zero. constexpr int last_set_u64(uint64_t value) { return value ? (64 - __builtin_clzll(value)) : 0; } @@ -355,17 +355,15 @@ const tm& select_time(const datetime::Context& ctx, const Schedule& schedule) { : ctx.local; } -datetime::Minutes to_minutes(int hour, int minute) { - return datetime::Hours{ hour } + datetime::Minutes{ minute }; +constexpr datetime::Minutes to_minutes(datetime::Seconds seconds) { + return std::chrono::duration_cast(seconds); } -datetime::Minutes to_minutes(const tm& t) { - return to_minutes(t.tm_hour, t.tm_min); +constexpr datetime::Minutes to_minutes(const datetime::Context& ctx) { + return to_minutes(datetime::Seconds(ctx.timestamp)); } -namespace restore { - -struct Result { +struct Offset { size_t index; datetime::Minutes offset; }; @@ -375,54 +373,58 @@ struct Pending { Schedule schedule; }; -struct Context { - Context() = delete; - - explicit Context(const datetime::Context& ctx) : - base(ctx), - current(ctx) - { - init(); - } +namespace search { - ~Context() { - destroy(); - } +bool is_same_day(const tm& lhs, const tm& rhs) { + return lhs.tm_year == rhs.tm_year + && lhs.tm_mon == rhs.tm_mon + && lhs.tm_yday == rhs.tm_yday + && lhs.tm_wday == rhs.tm_wday + && lhs.tm_mday == rhs.tm_mday; +} - bool next_delta(const datetime::Days&); - bool next(); +struct Closest { + using Mask = TimeMatch(*)(const TimeMatch&, const tm&); + Mask mask; - const datetime::Context& base; + using FindU32 = int(*)(uint32_t); + FindU32 find_u32; - datetime::Context current{}; - datetime::Days days{}; + using FindU64 = int(*)(uint64_t); + FindU64 find_u64; +}; - std::vector pending{}; - std::vector results{}; +// generalized code to find out tm either in the past or the future matching lhs, starting from the rhs +bool closest(const Closest& impl, tm& out, const TimeMatch& lhs, const tm& rhs) { + auto masked = impl.mask(lhs, rhs); + if (lhs.hour[rhs.tm_hour]) { + auto minute = impl.find_u64(masked.minute.to_ullong()); + if (minute != 0) { + --minute; + out.tm_min = minute; + return true; + } -private: - void destroy(); - void init(); - void init_delta(); -}; + masked.hour[rhs.tm_hour] = false; + } -bool Context::next_delta(const datetime::Days& days) { - if (days.count() == 0) { + auto hour = impl.find_u32(masked.hour.to_ulong()); + if (hour == 0) { return false; } - this->days += days; - this->current = datetime::delta(this->current, days); - if (this->current.timestamp < 0) { + auto minute = impl.find_u64(lhs.minute.to_ullong()); + if (minute == 0) { return false; } - this->init_delta(); - return true; -} + --hour; + --minute; -bool Context::next() { - return next_delta(datetime::Days{ -1 }); + out.tm_hour = hour; + out.tm_min = minute; + + return true; } std::bitset<24> mask_past_hours(const std::bitset<24>& lhs, int rhs) { @@ -442,38 +444,17 @@ TimeMatch mask_past(const TimeMatch& lhs, const tm& rhs) { return out; } -bool closest_delta(datetime::Minutes& out, const TimeMatch& lhs, const tm& rhs) { - auto past = mask_past(lhs, rhs); - if (lhs.hour[rhs.tm_hour]) { - auto minute = bits::last_set_u64(past.minute.to_ullong()); - if (minute != 0) { - --minute; - out = datetime::Minutes{ minute - rhs.tm_min }; - return true; - } - - past.hour[rhs.tm_hour] = false; - } - - auto hour = bits::last_set_u32(past.hour.to_ulong()); - if (hour == 0) { - return false; - } - - auto minute = bits::last_set_u64(lhs.minute.to_ullong()); - if (minute == 0) { - return false; - } - - --hour; - --minute; - - out -= to_minutes(rhs) - to_minutes(hour, minute); +constexpr auto ClosestPast = Closest{ + .mask = mask_past, + .find_u32 = bits::last_set_u32, + .find_u64 = bits::last_set_u64, +}; - return true; +bool closest_past(tm& out, const TimeMatch& lhs, const tm& rhs) { + return closest(ClosestPast, out, lhs, rhs); } -bool closest_delta_end_of_day(datetime::Minutes& out, const TimeMatch& lhs, const tm& rhs) { +bool closest_end_of_day(tm& out, const TimeMatch& lhs, const tm& rhs) { tm tmp; std::memcpy(&tmp, &rhs, sizeof(tm)); @@ -481,112 +462,339 @@ bool closest_delta_end_of_day(datetime::Minutes& out, const TimeMatch& lhs, cons tmp.tm_min = 59; tmp.tm_sec = 00; - const auto result = closest_delta(out, lhs, tmp); - if (result) { - out -= datetime::Minutes{ 1 }; - return true; - } + return closest_past(out, lhs, tmp); +} - return false; +std::bitset<24> mask_future_hours(const std::bitset<24>& lhs, int rhs) { + return lhs.to_ulong() & bits::fill_u32(rhs, 24); +} + +std::bitset<60> mask_future_minutes(const std::bitset<60>& lhs, int rhs) { + return lhs.to_ullong() & bits::fill_u64(rhs, 60); +} + +TimeMatch mask_future(const TimeMatch& lhs, const tm& rhs) { + TimeMatch out; + out.hour = mask_future_hours(lhs.hour, rhs.tm_hour); + out.minute = mask_future_minutes(lhs.minute, rhs.tm_min); + out.flags = lhs.flags; + + return out; } -void context_pending(Context& ctx, size_t index, const Schedule& schedule) { - ctx.pending.push_back({.index = index, .schedule = schedule}); +constexpr auto Future = Closest{ + .mask = mask_future, + .find_u32 = bits::first_set_u32, + .find_u64 = bits::first_set_u64, +}; + +bool closest_future(tm& out, const TimeMatch& lhs, const tm& rhs) { + return closest(Future, out, lhs, rhs); } -void context_result(Context& ctx, size_t index, datetime::Minutes offset) { - ctx.results.push_back({.index = index, .offset = offset}); +bool is_utc(const datetime::Context& ctx, const tm& time_point) { + return &ctx.utc == &time_point; } -bool handle_today(Context& ctx, size_t index, const Schedule& schedule) { - const auto& time_point = select_time(ctx.base, schedule); +bool is_local(const datetime::Context& ctx, const tm& time_point) { + return &ctx.local == &time_point; +} - if (match(schedule.date, time_point) && match(schedule.weekdays, time_point)) { - datetime::Minutes offset{}; - if (closest_delta(offset, schedule.time, time_point)) { - context_result(ctx, index, offset); - return true; - } +struct Context { + explicit Context(const datetime::Context& ctx) : + base(ctx), + current(ctx) + {} + + const datetime::Context& base; + + datetime::Context current{}; + datetime::Days days{}; + + // most of the time, schedules are processed in bulk + // push 'not-yet-handled' ones to internal storage to be processed later + // caller is expected to have full access to both, internals should only use 'push' + + std::vector pending{}; + std::vector results{}; + + void push_pending(size_t index, const Schedule& schedule) { + pending.push_back({.index = index, .schedule = schedule}); + } + + void push_result(size_t index, datetime::Minutes offset) { + results.push_back({.index = index, .offset = offset}); + } + + void sort() { + std::sort( + results.begin(), + results.end(), + [](const Offset& lhs, const Offset& rhs) { + return lhs.offset < rhs.offset; + }); + } +}; + +bool closest_offset_result(tm& out, Context& ctx, size_t index, const tm& time_point) { + datetime::Seconds end{ -1 }; + if (is_utc(ctx.current, time_point)) { + end = datetime::to_seconds(out); + } else if (is_local(ctx.current, time_point)) { + end = datetime::Seconds{ mktime(&out) }; + } + + if (end > end.zero()) { + // cast Seconds -> Minutes right now, extra seconds from + // subtraction may cause unexpected result after rounding + const auto begin = datetime::Seconds{ ctx.base.timestamp }; + const auto offset = to_minutes(end) - to_minutes(begin); + + ctx.push_result(index, offset); + + return true; + } + + return false; +} + +using Handler = bool(*)(tm&, const TimeMatch&, const tm&); + +bool handle_today(const Handler& handler, Context& ctx, size_t index, const Schedule& schedule) { + const auto& time_point = select_time(ctx.current, schedule); + + // in case mktime is used, make sure dst <-> std does not happen and as + // it tries to adjust tm_hour and tm_min. both stay exactly as specified + tm tmp; + tmp = time_point; + tmp.tm_isdst = -1; + + // handler() is expected to adjust `tmp` to either past or future, + // but staying within 00:00..23:59 boundaries of the `time_point` + if (match(schedule.date, time_point) + && match(schedule.weekdays, time_point) + && handler(tmp, schedule.time, time_point) + && is_same_day(tmp, time_point) + && match(schedule.time, tmp) + && closest_offset_result(tmp, ctx, index, time_point)) + { + return true; } - context_pending(ctx, index, schedule); + ctx.push_pending(index, schedule); return false; } -bool handle_delta(Context& ctx, const Pending& pending) { +bool handle_pending(const Handler& handler, Context& ctx, const Pending& pending) { if (!pending.schedule.ok) { return false; } - const auto& time_point = select_time(ctx.current, pending.schedule); + const auto& time_point = + select_time(ctx.current, pending.schedule); - if (match(pending.schedule.date, time_point) && match(pending.schedule.weekdays, time_point)) { - datetime::Minutes offset{ ctx.days - datetime::Days{ -1 }}; - if (closest_delta_end_of_day(offset, pending.schedule.time, time_point)) { - offset -= to_minutes(select_time(ctx.base, pending.schedule)); - context_result(ctx, pending.index, offset); - return true; - } - } + tm tmp; + tmp = time_point; + tmp.tm_isdst = -1; - return false; + return match(pending.schedule.date, time_point) + && match(pending.schedule.weekdays, time_point) + && handler(tmp, pending.schedule.time, time_point) + && is_same_day(tmp, time_point) + && match(pending.schedule.time, tmp) + && closest_offset_result(tmp, ctx, pending.index, time_point); } -} // namespace restore +} // namespace search -namespace expect { +namespace restore { -std::bitset<24> mask_future_hours(const std::bitset<24>& lhs, int rhs) { - return lhs.to_ulong() & bits::fill_u32(rhs, 24); +struct Context : public search::Context { + explicit Context(const datetime::Context& ctx) : + search::Context(ctx) + { + init(); + } + + ~Context() { + destroy(); + } + + bool next_delta(const datetime::Days&); + bool next(); + +private: + // defined externally, warnings should be safe to ignore + + void destroy(); + void init(); + void init_delta(); +}; + +bool Context::next_delta(const datetime::Days& days) { + if (days.count() == 0) { + return false; + } + + this->days += days; + this->current = datetime::delta(this->current, days); + if (this->current.timestamp < 0) { + return false; + } + + this->init_delta(); + return true; } -std::bitset<60> mask_future_minutes(const std::bitset<60>& lhs, int rhs) { - return lhs.to_ullong() & bits::fill_u64(rhs, 60); +bool Context::next() { + return next_delta(datetime::Days{ -1 }); } -TimeMatch mask_future(const TimeMatch& lhs, const tm& rhs) { - TimeMatch out; - out.hour = mask_future_hours(lhs.hour, rhs.tm_hour); - out.minute = mask_future_minutes(lhs.minute, rhs.tm_min); - out.flags = lhs.flags; +bool handle_today(Context& ctx, size_t index, const Schedule& schedule) { + return search::handle_today(search::closest_past, ctx, index, schedule); +} - return out; +bool handle_pending(Context& ctx, const Pending& pending) { + return search::handle_pending(search::closest_end_of_day, ctx, pending); } -bool closest_delta(datetime::Minutes& out, const TimeMatch& lhs, const tm& rhs) { - auto future = mask_future(lhs, rhs); - if (lhs.hour[rhs.tm_hour]) { - auto minute = bits::first_set_u64(future.minute.to_ullong()); - if (minute != 0) { - --minute; - out = datetime::Minutes{ minute - rhs.tm_min }; - return true; - } +} // namespace restore - future.hour[rhs.tm_hour] = false; - } +namespace expect { - auto hour = bits::first_set_u32(future.hour.to_ulong()); - if (hour == 0) { +struct Context : public search::Context { + explicit Context(const datetime::Context& ctx) : + search::Context(ctx) + {} + + bool next_delta(const datetime::Days&); + bool next(); +}; + +bool Context::next_delta(const datetime::Days& days) { + if (days.count() == 0) { return false; } - auto minute = bits::first_set_u64(lhs.minute.to_ullong()); - if (minute == 0) { + this->days += days; + this->current = datetime::delta(this->current, days); + if (this->current.timestamp < 0) { return false; } - --hour; - --minute; + return true; +} + +bool Context::next() { + return next_delta(datetime::Days{ 1 }); +} - out += to_minutes(hour, minute) - to_minutes(rhs); +bool handle_today(Context& ctx, size_t index, const Schedule& schedule) { + return search::handle_today(search::closest_future, ctx, index, schedule); +} - return true; +bool handle_pending(Context& ctx, const Pending& pending) { + return search::handle_pending(search::closest_future, ctx, pending); } } // namespace expect +namespace relative { + +enum class Type { + None, + Calendar, + Named, + Sunrise, + Sunset, +}; + +enum class Order { + None, + Before, + After, +}; + +struct Relative { + Type type { Type::None }; + Order order { Order::None }; + + String name; + uint8_t data { 0 }; + + datetime::Minutes offset{}; +}; + +} // namespace relative + +using relative::Relative; + +namespace event { + +struct TimePoint { + datetime::Minutes minutes{ -1 }; + datetime::Seconds seconds{ -1 }; +}; + +TimePoint make_time_point(datetime::Seconds seconds) { + TimePoint out; + + out.seconds = seconds; + out.minutes = + std::chrono::duration_cast(out.seconds); + out.seconds -= out.minutes; + + return out; +} + +struct Event { + TimePoint next; + TimePoint last; +}; + +constexpr bool is_valid(const datetime::Minutes& minutes) { + return minutes >= datetime::Minutes::zero(); +} + +constexpr bool is_valid(const datetime::Seconds& seconds) { + return seconds >= datetime::Seconds::zero(); +} + +constexpr bool is_valid(const TimePoint& time_point) { + return is_valid(time_point.minutes) + && is_valid(time_point.seconds); +} + +constexpr bool is_valid(const Event& event) { + return is_valid(event.next) && is_valid(event.last); +} + +constexpr bool maybe_valid(const Event& event) { + return is_valid(event.next) || is_valid(event.last); +} + +constexpr datetime::Seconds to_seconds(const TimePoint& time_point) { + return std::chrono::duration_cast(time_point.minutes) + + time_point.seconds; +} + +constexpr datetime::Minutes difference(const datetime::Minutes& lhs, const datetime::Minutes& rhs) { + return lhs - rhs; +} + +constexpr datetime::Minutes difference(const datetime::Context& ctx, const datetime::Minutes& rhs) { + return difference(to_minutes(ctx), rhs); +} + +} // namespace event + +using event::TimePoint; +using event::to_seconds; +using event::make_time_point; + +using event::Event; + } // namespace } // namespace scheduler } // namespace espurna diff --git a/code/espurna/scheduler_time.re b/code/espurna/scheduler_time.re index d8a63b02fb..eb001fd10e 100644 --- a/code/espurna/scheduler_time.re +++ b/code/espurna/scheduler_time.re @@ -681,12 +681,262 @@ return_out: return out && (YYCURSOR == YYLIMIT); } +// Relative time triggers, "before" or "after" specific 'target' event happens. +// Offset can be 0, causing action to immediately trigger. +// Can depend on either fixed-time events like sunrise & sunset, or other calendar schedules. + +bool parse_relative_offset(datetime::Minutes& out, StringView view) { + if (!view.length()) { + return false; + } + + if (!view.length()) { + return false; + } + + auto result = duration::parse(view, datetime::Minutes::period{}); + if (result.ok) { + out = duration::to_chrono(result.value); + return true; + } + + return false; +} + +bool parse_relative_keyword(Relative& out, StringView view) { + bool result { false }; + + if (STRING_VIEW("before").equalsIgnoreCase(view)) { + out.order = relative::Order::Before; + result = true; + } else if (STRING_VIEW("after").equalsIgnoreCase(view)) { + out.order = relative::Order::After; + result = true; + } + + return result; +} + +bool update_relative_id(Relative& out, StringView view) { + auto result = parseUnsigned(view, 10); + + if (result.ok && result.value <= 255) { + out.data = result.value; + return true; + } + + return false; +} + +bool parse_relative_target(Relative& value, StringView view) { + const char* YYCURSOR { view.begin() }; + const char* YYLIMIT { view.end() }; + const char* YYMARKER; + + StringView tmp; + bool out { false }; + + const char *p; + + /*!stags:re2c:relative_target format = 'const char *@@;'; */ + + /*!local:re2c:relative_target + + re2c:api:style = free-form; + re2c:define:YYCTYPE = char; + re2c:flags:case-insensitive = 1; + re2c:flags:tags = 1; + re2c:yyfill:enable = 0; + re2c:eof = 0; + + calendar = "Cal" | "Calendar"; + + sunrise = "Sunrise"; + sunset = "Sunset"; + + calendar [#] @p [0-9]+ { + value.type = relative::Type::Calendar; + + tmp = StringView(p, YYCURSOR - p); + out = update_relative_id(value, tmp); + + goto return_out; + } + + ["] @p [A-Za-z0-9]+ ["] { + value.type = relative::Type::Named; + + tmp = StringView(p, YYCURSOR - p - 1); + value.name = tmp.toString(); + + out = true; + + goto return_out; + } + + + sunrise { + value.type = relative::Type::Sunrise; + out = true; + + goto return_out; + } + + sunset { + value.type = relative::Type::Sunset; + out = true; + + goto return_out; + } + + * { + out = false; + goto return_out; + } + + $ { + goto return_out; + } + */ + +return_out: + return out && (YYCURSOR == YYLIMIT); +} + +bool parse_simple_iso8601(datetime::DateHhMmSs& datetime, bool& utc, StringView view) { + const char* YYCURSOR { view.begin() }; + const char* YYLIMIT { view.end() }; + const char* YYMARKER; + + StringView tmp; + bool out { false }; + + const char *p; + + /*!conditions:re2c:simple_iso8601*/ + int c = yycinit; + + /*!stags:re2c:simple_iso8601 format = 'const char *@@;'; */ + +loop: + /*!local:re2c:simple_iso8601 + + re2c:api:style = free-form; + re2c:define:YYGETCONDITION = "c"; + re2c:define:YYSETCONDITION = "c = @@;"; + re2c:define:YYCTYPE = char; + re2c:flags:case-insensitive = 0; + re2c:flags:tags = 1; + re2c:yyfill:enable = 0; + re2c:eof = 0; + + YYYY = [0-9]{4}; + MM = [0-9]{2}; + DD = [0-9]{2}; + + hh = [0-9]{2}; + mm = [0-9]{2}; + ss = [0-9]{2}; + + zero = "+00:00"; + zulu = "Z"; + + @p YYYY [-] MM [-] DD [T] hh [:] mm [:] ss => tz { + tmp = StringView{p, p + 4}; + const auto year = parseUnsigned(tmp, 10); + if (!year.ok) { + goto return_out; + } + + p += 5; + tmp = StringView{p , p + 2}; + + const auto month = parseUnsigned(tmp, 10); + if (!month.ok || (month.value > 12) || (month.value < 1)) { + goto return_out; + } + + p += 3; + tmp = StringView{p , p + 2}; + + const auto day = parseUnsigned(tmp, 10); + if (!day.ok || (day.value > 31) || (day.value < 1)) { + goto return_out; + } + + p += 3; + tmp = StringView{p , p + 2}; + + const auto hour = parseUnsigned(tmp, 10); + if (!hour.ok || (hour.value > 23)) { + goto return_out; + } + + p += 3; + tmp = StringView{p , p + 2}; + + const auto min = parseUnsigned(tmp, 10); + if (!min.ok || (min.value > 59)) { + goto return_out; + } + + p += 3; + tmp = StringView{p , p + 2}; + + const auto sec = parseUnsigned(tmp, 10); + if (!sec.ok || (sec.value > 59)) { + goto return_out; + } + + datetime.year = year.value; + datetime.month = month.value; + datetime.day = day.value; + + datetime.hours = hour.value; + datetime.minutes = min.value; + datetime.seconds = sec.value; + + goto loop; + } + + zulu | zero { + out = true; + utc = true; + goto return_out; + } + + $ { + out = true; + utc = false; + goto return_out; + } + + <*> * { + out = false; + goto return_out; + } + + <*> $ { + out = false; + goto return_out; + } + */ + +return_out: + return out && (YYCURSOR == YYLIMIT); + +} + } // namespace parse using parse::parse_date; using parse::parse_weekdays; using parse::parse_time; using parse::parse_time_keyword; +using parse::parse_relative_keyword; +using parse::parse_relative_offset; +using parse::parse_relative_target; +using parse::parse_simple_iso8601; Schedule parse_schedule(StringView view) { Schedule out; @@ -757,6 +1007,51 @@ Schedule parse_schedule(StringView view) { return out; } +Relative parse_relative(StringView view) { + Relative out; + + // OFFSET " " KEYWORD " " TARGET + const auto spaces = std::count(view.begin(), view.end(), ' '); + if (spaces > 2) { + return out; + } + + auto split = SplitStringView(view); + + bool parsed_offset { false }; + bool parsed_keyword { false }; + bool parsed_target { false }; + + int found { 0 }; + + while ((found < 3) && split.next()) { + auto elem = split.current(); + ++found; + + if (!parsed_offset && (parsed_offset = parse_relative_offset(out.offset, elem))) { + continue; + } + + if (!parsed_keyword && (parsed_keyword = parse_relative_keyword(out, elem))) { + continue; + } + + if (!parsed_target && (parsed_target = parse_relative_target(out, elem))) { + continue; + } + } + + if (!parsed_keyword || !parsed_target) { + out.type = relative::Type::None; + } + + if (!parsed_offset && out.type != relative::Type::None) { + out.offset = datetime::Minutes{ 1 }; + } + + return out; +} + } // namespace } // namespace scheduler } // namespace espurna diff --git a/code/espurna/scheduler_time.re.ipp b/code/espurna/scheduler_time.re.ipp index 29ef3d8354..5f8f8ee9b4 100644 --- a/code/espurna/scheduler_time.re.ipp +++ b/code/espurna/scheduler_time.re.ipp @@ -1624,12 +1624,625 @@ return_out: return out && (YYCURSOR == YYLIMIT); } +// Relative time triggers, "before" or "after" specific 'target' event happens. +// Offset can be 0, causing action to immediately trigger. +// Can depend on either fixed-time events like sunrise & sunset, or other calendar schedules. + +bool parse_relative_offset(datetime::Minutes& out, StringView view) { + if (!view.length()) { + return false; + } + + if (!view.length()) { + return false; + } + + auto result = duration::parse(view, datetime::Minutes::period{}); + if (result.ok) { + out = duration::to_chrono(result.value); + return true; + } + + return false; +} + +bool parse_relative_keyword(Relative& out, StringView view) { + bool result { false }; + + if (STRING_VIEW("before").equalsIgnoreCase(view)) { + out.order = relative::Order::Before; + result = true; + } else if (STRING_VIEW("after").equalsIgnoreCase(view)) { + out.order = relative::Order::After; + result = true; + } + + return result; +} + +bool update_relative_id(Relative& out, StringView view) { + auto result = parseUnsigned(view, 10); + + if (result.ok && result.value <= 255) { + out.data = result.value; + return true; + } + + return false; +} + +bool parse_relative_target(Relative& value, StringView view) { + const char* YYCURSOR { view.begin() }; + const char* YYLIMIT { view.end() }; + const char* YYMARKER; + + StringView tmp; + bool out { false }; + + const char *p; + + +#line 1686 "espurna/scheduler_time.re.ipp" +const char *yyt1; +#line 741 "espurna/scheduler_time.re" + + + +#line 1692 "espurna/scheduler_time.re.ipp" +{ + char yych; + yych = *YYCURSOR; + switch (yych) { + case '"': goto yy159; + case 'C': + case 'c': goto yy160; + case 'S': + case 's': goto yy161; + default: + if (YYLIMIT <= YYCURSOR) goto yy184; + goto yy157; + } +yy157: + ++YYCURSOR; +yy158: +#line 792 "espurna/scheduler_time.re" + { + out = false; + goto return_out; + } +#line 1714 "espurna/scheduler_time.re.ipp" +yy159: + yych = *(YYMARKER = ++YYCURSOR); + switch (yych) { + case '0' ... '9': + case 'A' ... 'Z': + case 'a' ... 'z': + yyt1 = YYCURSOR; + goto yy162; + default: goto yy158; + } +yy160: + yych = *(YYMARKER = ++YYCURSOR); + switch (yych) { + case 'A': + case 'a': goto yy164; + default: goto yy158; + } +yy161: + yych = *(YYMARKER = ++YYCURSOR); + switch (yych) { + case 'U': + case 'u': goto yy165; + default: goto yy158; + } +yy162: + yych = *++YYCURSOR; + switch (yych) { + case '"': goto yy166; + case '0' ... '9': + case 'A' ... 'Z': + case 'a' ... 'z': goto yy162; + default: goto yy163; + } +yy163: + YYCURSOR = YYMARKER; + goto yy158; +yy164: + yych = *++YYCURSOR; + switch (yych) { + case 'L': + case 'l': goto yy167; + default: goto yy163; + } +yy165: + yych = *++YYCURSOR; + switch (yych) { + case 'N': + case 'n': goto yy168; + default: goto yy163; + } +yy166: + ++YYCURSOR; + p = yyt1; +#line 766 "espurna/scheduler_time.re" + { + value.type = relative::Type::Named; + + tmp = StringView(p, YYCURSOR - p - 1); + value.name = tmp.toString(); + + out = true; + + goto return_out; + } +#line 1779 "espurna/scheduler_time.re.ipp" +yy167: + yych = *++YYCURSOR; + switch (yych) { + case '#': goto yy169; + case 'E': + case 'e': goto yy170; + default: goto yy163; + } +yy168: + yych = *++YYCURSOR; + switch (yych) { + case 'R': + case 'r': goto yy171; + case 'S': + case 's': goto yy172; + default: goto yy163; + } +yy169: + yych = *++YYCURSOR; + switch (yych) { + case '0' ... '9': + yyt1 = YYCURSOR; + goto yy173; + default: goto yy163; + } +yy170: + yych = *++YYCURSOR; + switch (yych) { + case 'N': + case 'n': goto yy175; + default: goto yy163; + } +yy171: + yych = *++YYCURSOR; + switch (yych) { + case 'I': + case 'i': goto yy176; + default: goto yy163; + } +yy172: + yych = *++YYCURSOR; + switch (yych) { + case 'E': + case 'e': goto yy177; + default: goto yy163; + } +yy173: + yych = *++YYCURSOR; + switch (yych) { + case '0' ... '9': goto yy173; + default: goto yy174; + } +yy174: + p = yyt1; +#line 757 "espurna/scheduler_time.re" + { + value.type = relative::Type::Calendar; + + tmp = StringView(p, YYCURSOR - p); + out = update_relative_id(value, tmp); + + goto return_out; + } +#line 1843 "espurna/scheduler_time.re.ipp" +yy175: + yych = *++YYCURSOR; + switch (yych) { + case 'D': + case 'd': goto yy178; + default: goto yy163; + } +yy176: + yych = *++YYCURSOR; + switch (yych) { + case 'S': + case 's': goto yy179; + default: goto yy163; + } +yy177: + yych = *++YYCURSOR; + switch (yych) { + case 'T': + case 't': goto yy180; + default: goto yy163; + } +yy178: + yych = *++YYCURSOR; + switch (yych) { + case 'A': + case 'a': goto yy181; + default: goto yy163; + } +yy179: + yych = *++YYCURSOR; + switch (yych) { + case 'E': + case 'e': goto yy182; + default: goto yy163; + } +yy180: + ++YYCURSOR; +#line 785 "espurna/scheduler_time.re" + { + value.type = relative::Type::Sunset; + out = true; + + goto return_out; + } +#line 1888 "espurna/scheduler_time.re.ipp" +yy181: + yych = *++YYCURSOR; + switch (yych) { + case 'R': + case 'r': goto yy183; + default: goto yy163; + } +yy182: + ++YYCURSOR; +#line 778 "espurna/scheduler_time.re" + { + value.type = relative::Type::Sunrise; + out = true; + + goto return_out; + } +#line 1905 "espurna/scheduler_time.re.ipp" +yy183: + yych = *++YYCURSOR; + switch (yych) { + case '#': goto yy169; + default: goto yy163; + } +yy184: +#line 797 "espurna/scheduler_time.re" + { + goto return_out; + } +#line 1917 "espurna/scheduler_time.re.ipp" +} +#line 800 "espurna/scheduler_time.re" + + +return_out: + return out && (YYCURSOR == YYLIMIT); +} + +bool parse_simple_iso8601(datetime::DateHhMmSs& datetime, bool& utc, StringView view) { + const char* YYCURSOR { view.begin() }; + const char* YYLIMIT { view.end() }; + const char* YYMARKER; + + StringView tmp; + bool out { false }; + + const char *p; + + +#line 1937 "espurna/scheduler_time.re.ipp" +enum YYCONDTYPE { + yycinit, + yyctz +}; +#line 816 "espurna/scheduler_time.re" + + int c = yycinit; + + +#line 1947 "espurna/scheduler_time.re.ipp" +#line 819 "espurna/scheduler_time.re" + + +loop: + +#line 1953 "espurna/scheduler_time.re.ipp" +{ + char yych; + switch (c) { + case yycinit: goto yyc_init; + case yyctz: goto yyc_tz; + } +/* *********************************** */ +yyc_init: + yych = *YYCURSOR; + switch (yych) { + case '0' ... '9': goto yy188; + default: + if (YYLIMIT <= YYCURSOR) goto yy208; + goto yy186; + } +yy186: + ++YYCURSOR; +yy187: +#line 914 "espurna/scheduler_time.re" + { + out = false; + goto return_out; + } +#line 1977 "espurna/scheduler_time.re.ipp" +yy188: + yych = *(YYMARKER = ++YYCURSOR); + switch (yych) { + case '0' ... '9': goto yy189; + default: goto yy187; + } +yy189: + yych = *++YYCURSOR; + switch (yych) { + case '0' ... '9': goto yy191; + default: goto yy190; + } +yy190: + YYCURSOR = YYMARKER; + goto yy187; +yy191: + yych = *++YYCURSOR; + switch (yych) { + case '0' ... '9': goto yy192; + default: goto yy190; + } +yy192: + yych = *++YYCURSOR; + switch (yych) { + case '-': goto yy193; + default: goto yy190; + } +yy193: + yych = *++YYCURSOR; + switch (yych) { + case '0' ... '9': goto yy194; + default: goto yy190; + } +yy194: + yych = *++YYCURSOR; + switch (yych) { + case '0' ... '9': goto yy195; + default: goto yy190; + } +yy195: + yych = *++YYCURSOR; + switch (yych) { + case '-': goto yy196; + default: goto yy190; + } +yy196: + yych = *++YYCURSOR; + switch (yych) { + case '0' ... '9': goto yy197; + default: goto yy190; + } +yy197: + yych = *++YYCURSOR; + switch (yych) { + case '0' ... '9': goto yy198; + default: goto yy190; + } +yy198: + yych = *++YYCURSOR; + switch (yych) { + case 'T': goto yy199; + default: goto yy190; + } +yy199: + yych = *++YYCURSOR; + switch (yych) { + case '0' ... '9': goto yy200; + default: goto yy190; + } +yy200: + yych = *++YYCURSOR; + switch (yych) { + case '0' ... '9': goto yy201; + default: goto yy190; + } +yy201: + yych = *++YYCURSOR; + switch (yych) { + case ':': goto yy202; + default: goto yy190; + } +yy202: + yych = *++YYCURSOR; + switch (yych) { + case '0' ... '9': goto yy203; + default: goto yy190; + } +yy203: + yych = *++YYCURSOR; + switch (yych) { + case '0' ... '9': goto yy204; + default: goto yy190; + } +yy204: + yych = *++YYCURSOR; + switch (yych) { + case ':': goto yy205; + default: goto yy190; + } +yy205: + yych = *++YYCURSOR; + switch (yych) { + case '0' ... '9': goto yy206; + default: goto yy190; + } +yy206: + yych = *++YYCURSOR; + switch (yych) { + case '0' ... '9': goto yy207; + default: goto yy190; + } +yy207: + ++YYCURSOR; + p = YYCURSOR - 19; + c = yyctz; +#line 844 "espurna/scheduler_time.re" + { + tmp = StringView{p, p + 4}; + const auto year = parseUnsigned(tmp, 10); + if (!year.ok) { + goto return_out; + } + + p += 5; + tmp = StringView{p , p + 2}; + + const auto month = parseUnsigned(tmp, 10); + if (!month.ok || (month.value > 12) || (month.value < 1)) { + goto return_out; + } + + p += 3; + tmp = StringView{p , p + 2}; + + const auto day = parseUnsigned(tmp, 10); + if (!day.ok || (day.value > 31) || (day.value < 1)) { + goto return_out; + } + + p += 3; + tmp = StringView{p , p + 2}; + + const auto hour = parseUnsigned(tmp, 10); + if (!hour.ok || (hour.value > 23)) { + goto return_out; + } + + p += 3; + tmp = StringView{p , p + 2}; + + const auto min = parseUnsigned(tmp, 10); + if (!min.ok || (min.value > 59)) { + goto return_out; + } + + p += 3; + tmp = StringView{p , p + 2}; + + const auto sec = parseUnsigned(tmp, 10); + if (!sec.ok || (sec.value > 59)) { + goto return_out; + } + + datetime.year = year.value; + datetime.month = month.value; + datetime.day = day.value; + + datetime.hours = hour.value; + datetime.minutes = min.value; + datetime.seconds = sec.value; + + goto loop; + } +#line 2151 "espurna/scheduler_time.re.ipp" +yy208: +#line 919 "espurna/scheduler_time.re" + { + out = false; + goto return_out; + } +#line 2158 "espurna/scheduler_time.re.ipp" +/* *********************************** */ +yyc_tz: + yych = *YYCURSOR; + switch (yych) { + case '+': goto yy212; + case 'Z': goto yy213; + default: + if (YYLIMIT <= YYCURSOR) goto yy219; + goto yy210; + } +yy210: + ++YYCURSOR; +yy211: +#line 914 "espurna/scheduler_time.re" + { + out = false; + goto return_out; + } +#line 2177 "espurna/scheduler_time.re.ipp" +yy212: + yych = *(YYMARKER = ++YYCURSOR); + switch (yych) { + case '0': goto yy214; + default: goto yy211; + } +yy213: + ++YYCURSOR; +#line 902 "espurna/scheduler_time.re" + { + out = true; + utc = true; + goto return_out; + } +#line 2192 "espurna/scheduler_time.re.ipp" +yy214: + yych = *++YYCURSOR; + switch (yych) { + case '0': goto yy216; + default: goto yy215; + } +yy215: + YYCURSOR = YYMARKER; + goto yy211; +yy216: + yych = *++YYCURSOR; + switch (yych) { + case ':': goto yy217; + default: goto yy215; + } +yy217: + yych = *++YYCURSOR; + switch (yych) { + case '0': goto yy218; + default: goto yy215; + } +yy218: + yych = *++YYCURSOR; + switch (yych) { + case '0': goto yy213; + default: goto yy215; + } +yy219: +#line 908 "espurna/scheduler_time.re" + { + out = true; + utc = false; + goto return_out; + } +#line 2227 "espurna/scheduler_time.re.ipp" +} +#line 923 "espurna/scheduler_time.re" + + +return_out: + return out && (YYCURSOR == YYLIMIT); + +} + } // namespace parse using parse::parse_date; using parse::parse_weekdays; using parse::parse_time; using parse::parse_time_keyword; +using parse::parse_relative_keyword; +using parse::parse_relative_offset; +using parse::parse_relative_target; +using parse::parse_simple_iso8601; Schedule parse_schedule(StringView view) { Schedule out; @@ -1700,6 +2313,51 @@ Schedule parse_schedule(StringView view) { return out; } +Relative parse_relative(StringView view) { + Relative out; + + // OFFSET " " KEYWORD " " TARGET + const auto spaces = std::count(view.begin(), view.end(), ' '); + if (spaces > 2) { + return out; + } + + auto split = SplitStringView(view); + + bool parsed_offset { false }; + bool parsed_keyword { false }; + bool parsed_target { false }; + + int found { 0 }; + + while ((found < 3) && split.next()) { + auto elem = split.current(); + ++found; + + if (!parsed_offset && (parsed_offset = parse_relative_offset(out.offset, elem))) { + continue; + } + + if (!parsed_keyword && (parsed_keyword = parse_relative_keyword(out, elem))) { + continue; + } + + if (!parsed_target && (parsed_target = parse_relative_target(out, elem))) { + continue; + } + } + + if (!parsed_keyword || !parsed_target) { + out.type = relative::Type::None; + } + + if (!parsed_offset && out.type != relative::Type::None) { + out.offset = datetime::Minutes{ 1 }; + } + + return out; +} + } // namespace } // namespace scheduler } // namespace espurna diff --git a/code/html/src/template-sch.html b/code/html/src/template-sch.html index 88def529ad..354a0f690b 100644 --- a/code/html/src/template-sch.html +++ b/code/html/src/template-sch.html @@ -11,6 +11,7 @@ + diff --git a/code/test/unit/src/scheduler/scheduler.cpp b/code/test/unit/src/scheduler/scheduler.cpp index e666da7dc1..fe2c6a3b14 100644 --- a/code/test/unit/src/scheduler/scheduler.cpp +++ b/code/test/unit/src/scheduler/scheduler.cpp @@ -100,6 +100,8 @@ void test_date_impl() { void test_date_parsing_invalid() { TEST_SCHEDULER_INVALID_DATE(""); + TEST_SCHEDULER_INVALID_DATE("0"); + TEST_SCHEDULER_INVALID_DATE("2"); TEST_SCHEDULER_INVALID_DATE("foo bar"); TEST_SCHEDULER_INVALID_DATE("2049-"); TEST_SCHEDULER_INVALID_DATE("-"); @@ -427,7 +429,7 @@ void test_restore_today() { schedule.date.month[0] = true; schedule.date.year = 2006; - TEST_ASSERT_FALSE(restore::handle_today(ctx, 0, schedule)); + TEST_ASSERT_FALSE(handle_today(ctx, 0, schedule)); TEST_ASSERT_EQUAL(1, ctx.pending.size()); TEST_ASSERT_EQUAL(0, ctx.results.size()); @@ -436,7 +438,7 @@ void test_restore_today() { schedule.date = original_date; - TEST_ASSERT_TRUE(restore::handle_today(ctx, 1, schedule)); + TEST_ASSERT_TRUE(handle_today(ctx, 1, schedule)); TEST_ASSERT_EQUAL(0, ctx.pending.size()); TEST_ASSERT_EQUAL(1, ctx.results.size()); TEST_ASSERT_EQUAL(1, ctx.results[0].index); @@ -453,7 +455,7 @@ void test_restore_today() { schedule.time.minute.reset(); schedule.time.minute.set(ctx.current.utc.tm_min - 4); - TEST_ASSERT_TRUE(restore::handle_today(ctx, 2, schedule)); + TEST_ASSERT_TRUE(handle_today(ctx, 2, schedule)); TEST_ASSERT_EQUAL(0, ctx.pending.size()); TEST_ASSERT_EQUAL(1, ctx.results.size()); TEST_ASSERT_EQUAL(2, ctx.results[0].index); @@ -470,7 +472,7 @@ void test_restore_today() { schedule.time.minute.reset(); schedule.time.minute.set(ctx.current.utc.tm_min + 30); - TEST_ASSERT_TRUE(restore::handle_today(ctx, 3, schedule)); + TEST_ASSERT_TRUE(handle_today(ctx, 3, schedule)); TEST_ASSERT_EQUAL(0, ctx.pending.size()); TEST_ASSERT_EQUAL(1, ctx.results.size()); TEST_ASSERT_EQUAL(3, ctx.results[0].index); @@ -482,22 +484,22 @@ void test_restore_today() { void test_restore_delta_future() { MAKE_RESTORE_CONTEXT(ctx, schedule); - const auto pending = restore::Pending{.index = 1, .schedule = schedule}; + const auto pending = Pending{.index = 1, .schedule = schedule}; ctx.next_delta(datetime::Days{ 25 }); - TEST_ASSERT_FALSE(handle_delta(ctx, pending)); + TEST_ASSERT_FALSE(handle_pending(ctx, pending)); TEST_ASSERT_EQUAL(0, ctx.results.size()); ctx.next_delta(datetime::Days{ -15 }); - TEST_ASSERT_FALSE(handle_delta(ctx, pending)); + TEST_ASSERT_FALSE(handle_pending(ctx, pending)); TEST_ASSERT_EQUAL(0, ctx.results.size()); ctx.next_delta(datetime::Days{ 0 }); - TEST_ASSERT_FALSE(handle_delta(ctx, pending)); + TEST_ASSERT_FALSE(handle_pending(ctx, pending)); TEST_ASSERT_EQUAL(0, ctx.results.size()); ctx.next(); - TEST_ASSERT_FALSE(handle_delta(ctx, pending)); + TEST_ASSERT_FALSE(handle_pending(ctx, pending)); TEST_ASSERT_EQUAL(0, ctx.results.size()); } @@ -512,65 +514,162 @@ void test_restore_delta_past() { constexpr std::array tests{ Expected{ - .index = 123, + .index = 0, .delta = datetime::Days{ -1 }, .day = 1, .weekday = datetime::Sunday, .hours = datetime::Hours{ -31 }}, Expected{ - .index = 567, + .index = 1, .delta = datetime::Days{ -1 }, .day = 31, .weekday = datetime::Saturday, .hours = datetime::Hours{ -55 }}, Expected{ - .index = 890, + .index = 2, .delta = datetime::Days{ -1 }, .day = 30, .weekday = datetime::Friday, .hours = datetime::Hours{ -79 }}, Expected{ - .index = 111, + .index = 3, .delta = datetime::Days{ -2 }, .day = 28, .weekday = datetime::Wednesday, .hours = datetime::Hours{ -127 }}, }; + MAKE_RESTORE_CONTEXT(ctx, schedule); + auto schedule_day_weekday = [&](auto& out, const auto& test) { + out.date = scheduler::DateMatch{}; + out.date.day[test.day] = true; + + out.weekdays = scheduler::WeekdayMatch{}; + out.weekdays.day[test.weekday.c_value()] = true; + }; + for (const auto& test : tests) { - schedule.date = scheduler::DateMatch{}; - schedule.date.day[test.day] = true; + schedule_day_weekday(schedule, test); - schedule.weekdays = scheduler::WeekdayMatch{}; - schedule.weekdays.day[test.weekday.c_value()] = true; + TEST_ASSERT_FALSE( + handle_today(ctx, test.index, schedule)); + } - ctx.next_delta(test.delta); + TEST_ASSERT_EQUAL(0, ctx.results.size()); + TEST_ASSERT_EQUAL(tests.size(), ctx.pending.size()); - TEST_ASSERT_EQUAL(0, ctx.results.size()); - TEST_ASSERT_EQUAL(0, ctx.pending.size()); + for (const auto& test : tests) { + schedule_day_weekday(schedule, test); - TEST_ASSERT_FALSE( - restore::handle_today(ctx, test.index, schedule)); - TEST_ASSERT_EQUAL(0, ctx.results.size()); - TEST_ASSERT_EQUAL(1, ctx.pending.size()); + ctx.next_delta(test.delta); - TEST_ASSERT_TRUE( - restore::handle_delta(ctx, ctx.pending[0])); - TEST_ASSERT_EQUAL(1, ctx.results.size()); - TEST_ASSERT_EQUAL(1, ctx.pending.size()); + for (auto& pending : ctx.pending) { + handle_pending(ctx, pending); + } + } - TEST_ASSERT_EQUAL(test.index, ctx.results[0].index); - TEST_ASSERT_EQUAL( - datetime::Minutes(test.hours).count(), - ctx.results[0].offset.count()); + TEST_ASSERT_EQUAL(tests.size(), ctx.results.size()); + TEST_ASSERT_EQUAL(tests.size(), ctx.pending.size()); - ctx.results.clear(); - ctx.pending.clear(); + for (auto& result : ctx.results) { + TEST_ASSERT_EQUAL(tests[result.index].index, result.index); + TEST_ASSERT_EQUAL( + datetime::Minutes(tests[result.index].hours).count(), + result.offset.count()); } } +void test_event_impl() { + static_assert(std::is_same_v, ""); + const auto now = datetime::Clock::now(); + + static_assert(std::is_same_v, ""); + static_assert(std::is_same_v, ""); + event::TimePoint foo = event::make_time_point(now.time_since_epoch()); + + TEST_ASSERT(event::is_valid(foo)); + TEST_ASSERT_GREATER_OR_EQUAL(0, foo.minutes.count()); + TEST_ASSERT_GREATER_OR_EQUAL(0, foo.seconds.count()); + + const auto minutes = + std::chrono::duration_cast(now.time_since_epoch()); + static_assert(std::is_same_v, ""); + + const auto seconds = + now.time_since_epoch() - minutes; + static_assert(std::is_same_v, ""); + + TEST_ASSERT_EQUAL(minutes.count(), foo.minutes.count()); + TEST_ASSERT_EQUAL(seconds.count(), foo.seconds.count()); +} + +void test_event_parsing() { + auto result = parse_relative("5 after \"foobar\""); + TEST_ASSERT_EQUAL(relative::Order::After, result.order); + TEST_ASSERT_EQUAL(relative::Type::Named, result.type); + TEST_ASSERT_EQUAL_STRING("foobar", result.name.c_str()); + TEST_ASSERT_EQUAL( + datetime::Minutes(5).count(), + result.offset.count()); + + result = parse_relative("33m before \"bar\""); + TEST_ASSERT_EQUAL(relative::Order::Before, result.order); + TEST_ASSERT_EQUAL(relative::Type::Named, result.type); + TEST_ASSERT_EQUAL_STRING("bar", result.name.c_str()); + TEST_ASSERT_EQUAL( + datetime::Minutes(33).count(), + result.offset.count()); + + result = parse_relative("30m before sunrise"); + TEST_ASSERT_EQUAL(relative::Order::Before, result.order); + TEST_ASSERT_EQUAL(relative::Type::Sunrise, result.type); + TEST_ASSERT_EQUAL( + datetime::Minutes(30).count(), + result.offset.count()); + + result = parse_relative("1h15m after sunset"); + TEST_ASSERT_EQUAL(relative::Order::After, result.order); + TEST_ASSERT_EQUAL(relative::Type::Sunset, result.type); + TEST_ASSERT_EQUAL( + datetime::Minutes(75).count(), + result.offset.count()); + + result = parse_relative("after sunset"); + TEST_ASSERT_EQUAL(relative::Order::After, result.order); + TEST_ASSERT_EQUAL(relative::Type::Sunset, result.type); + TEST_ASSERT_EQUAL(1, result.offset.count()); + + result = parse_relative("before sunrise"); + TEST_ASSERT_EQUAL(relative::Order::Before, result.order); + TEST_ASSERT_EQUAL(relative::Type::Sunrise, result.type); + TEST_ASSERT_EQUAL(1, result.offset.count()); + + result = parse_relative("10m before calendar#123"); + TEST_ASSERT_EQUAL(relative::Order::Before, result.order); + TEST_ASSERT_EQUAL(relative::Type::Calendar, result.type); + TEST_ASSERT_EQUAL( + datetime::Minutes(10).count(), + result.offset.count()); + TEST_ASSERT_EQUAL(123, result.data); + + result = parse_relative("after calendar#543"); + TEST_ASSERT_EQUAL(relative::Type::None, result.type); + + result = parse_relative("after"); + TEST_ASSERT_EQUAL(relative::Type::None, result.type); + + result = parse_relative("before"); + TEST_ASSERT_EQUAL(relative::Type::None, result.type); + + result = parse_relative("11 befre boot"); + TEST_ASSERT_EQUAL(relative::Type::None, result.type); + + result = parse_relative("55 afer sunrise"); + TEST_ASSERT_EQUAL(relative::Type::None, result.type); +} + #define TEST_TIME_POINT_BOUNDARIES(X)\ TEST_ASSERT(((X).tm_hour >= 0) && ((X).tm_hour < 24));\ TEST_ASSERT(((X).tm_min >= 0) && ((X).tm_min < 60)) @@ -582,13 +681,12 @@ void test_expect_today() { scheduler::TimeMatch m; m.hour.set(time_point.tm_hour - 1); m.minute.set(time_point.tm_min - 1); + m.flags = scheduler::FlagUtc; - datetime::Minutes offset{}; - TEST_ASSERT_EQUAL(0, offset.count()); + tm closest = time_point; TEST_TIME_POINT_BOUNDARIES(time_point); - TEST_ASSERT_FALSE(expect::closest_delta(offset, m, time_point)); - TEST_ASSERT_EQUAL(0, offset.count()); + TEST_ASSERT_FALSE(search::closest_future(closest, m, time_point)); constexpr auto one_h = datetime::Hours(1); m.hour.reset(); @@ -598,9 +696,11 @@ void test_expect_today() { m.minute.reset(); m.minute.set(time_point.tm_min + twenty_six_m.count()); - TEST_ASSERT_EQUAL(0, offset.count()); - TEST_ASSERT(expect::closest_delta(offset, m, time_point)); - TEST_ASSERT_EQUAL((one_h + twenty_six_m).count(), offset.count()); + closest = time_point; + + TEST_ASSERT(search::closest_future(closest, m, time_point)); + TEST_ASSERT_EQUAL(time_point.tm_hour + one_h.count(), closest.tm_hour); + TEST_ASSERT_EQUAL(time_point.tm_min + twenty_six_m.count(), closest.tm_min); constexpr auto nine_h = datetime::Hours(9); m.hour.reset(); @@ -612,16 +712,16 @@ void test_expect_today() { time_point.tm_hour -= nine_h.count(); time_point.tm_min += thirty_m.count(); + TEST_TIME_POINT_BOUNDARIES(time_point); - offset = offset.zero(); + closest = time_point; - TEST_TIME_POINT_BOUNDARIES(time_point); - TEST_ASSERT_EQUAL(0, offset.count()); - TEST_ASSERT(expect::closest_delta(offset, m, time_point)); - TEST_ASSERT_EQUAL((nine_h - thirty_m).count(), offset.count()); + TEST_ASSERT(search::closest_future(closest, m, time_point)); + TEST_ASSERT_EQUAL(time_point.tm_hour + nine_h.count(), closest.tm_hour); + TEST_ASSERT_EQUAL(time_point.tm_min - thirty_m.count(), closest.tm_min); time_point = original_time_point; - offset = offset.zero(); + closest = time_point; m.hour.reset(); m.hour.set(time_point.tm_hour); @@ -630,9 +730,43 @@ void test_expect_today() { m.minute.set(time_point.tm_min + thirty_m.count()); TEST_TIME_POINT_BOUNDARIES(time_point); - TEST_ASSERT_EQUAL(0, offset.count()); - TEST_ASSERT(expect::closest_delta(offset, m, time_point)); - TEST_ASSERT_EQUAL(thirty_m.count(), offset.count()); + + TEST_ASSERT(search::closest_future(closest, m, time_point)); + TEST_ASSERT_EQUAL(time_point.tm_hour, closest.tm_hour); + TEST_ASSERT_EQUAL(time_point.tm_min + thirty_m.count(), closest.tm_min); +} + +void test_expect_delta_future() { + const auto reference = datetime::make_context(ReferenceTimestamp); + + auto ctx = expect::Context{ reference }; + + Schedule schedule; + schedule.time.flags = scheduler::FlagUtc; + schedule.ok = true; + + constexpr auto one_d = datetime::Days(1); + schedule.date.day[reference.utc.tm_mday + one_d.count()] = true; + + constexpr auto thirty_one_m = datetime::Minutes(31); + schedule.time.hour[reference.utc.tm_hour] = true; + schedule.time.minute[reference.utc.tm_min + thirty_one_m.count()] = true; + schedule.time.flags = FlagUtc; + + TEST_ASSERT_FALSE(handle_today(ctx, 123, schedule)); + TEST_ASSERT_EQUAL(0, ctx.results.size()); + TEST_ASSERT_EQUAL(1, ctx.pending.size()); + TEST_ASSERT_EQUAL(123, ctx.pending[0].index); + + TEST_ASSERT(ctx.next()); + + TEST_ASSERT(handle_pending(ctx, ctx.pending[0])); + TEST_ASSERT_EQUAL(1, ctx.results.size()); + TEST_ASSERT_EQUAL(123, ctx.results[0].index); + + TEST_ASSERT_EQUAL( + (one_d + thirty_one_m).count(), + ctx.results[0].offset.count()); } void test_schedule_invalid_parsing() { @@ -702,6 +836,7 @@ void test_schedule_parsing_time() { TEST_SCHEDULE_MATCH(time_point, "2024-01-01"); TEST_SCHEDULE_MATCH(time_point, "01-01"); TEST_SCHEDULE_MATCH(time_point, "Monday"); + TEST_SCHEDULE_MATCH(time_point, "1"); TEST_SCHEDULE_MATCH(time_point, "00:00"); TEST_SCHEDULE_MATCH(time_point, "UTC"); } @@ -854,7 +989,7 @@ void test_schedule_parsing_weekdays_range() { TEST_SCHEDULE_MATCH(time_point, "Mon,Thu..Sat 10,15,20:30"); } -void test_bitmask() { +void test_search_bits() { constexpr auto Days = uint32_t{ 0b101010111100100010100110 } ; constexpr auto Mask = std::bitset<24>(Days); @@ -863,14 +998,14 @@ void test_bitmask() { TEST_ASSERT(Mask.test(14)); TEST_ASSERT(Mask.test(23)); - const auto past = restore::mask_past_hours(Mask, 12); + const auto past = search::mask_past_hours(Mask, 12); TEST_ASSERT_EQUAL(0b000000000000100010100110, past.to_ulong()); TEST_ASSERT_EQUAL(2, bits::first_set_u32(past.to_ulong())); TEST_ASSERT_EQUAL(12, bits::last_set_u32(past.to_ulong())); TEST_ASSERT_FALSE(past.test(14)); TEST_ASSERT_FALSE(past.test(23)); - const auto future = expect::mask_future_hours(Mask, 12); + const auto future = search::mask_future_hours(Mask, 12); TEST_ASSERT_EQUAL(0b101010111100000000000000, future.to_ulong()); TEST_ASSERT_EQUAL(15, bits::first_set_u32(future.to_ulong())); TEST_ASSERT_EQUAL(24, bits::last_set_u32(future.to_ulong())); @@ -878,6 +1013,60 @@ void test_bitmask() { TEST_ASSERT_FALSE(future.test(11)); } +void test_datetime_parsing() { + datetime::DateHhMmSs parsed{}; + bool utc { false }; + + TEST_ASSERT(parse_simple_iso8601(parsed, utc, "2006-01-02T22:04:05+00:00")); + TEST_ASSERT(utc); + TEST_ASSERT_EQUAL( + ReferenceTimestamp, + datetime::to_seconds(parsed, utc).count()); + + parsed = datetime::DateHhMmSs{}; + utc = false; + + TEST_ASSERT(parse_simple_iso8601(parsed, utc, "2006-01-02T22:04:05Z")); + TEST_ASSERT(utc); + TEST_ASSERT_EQUAL( + ReferenceTimestamp, + datetime::to_seconds(parsed, utc).count()); + + parsed = datetime::DateHhMmSs{}; + utc = false; + + const auto now = datetime::Clock::now(); + + time_t ts; + ts = now.time_since_epoch().count(); + + tm local{}; + localtime_r(&ts, &local); + + char buf[64]{}; + const auto len = strftime(&buf[0], sizeof(buf), "%FT%H:%M:%S", &local); + + TEST_ASSERT_NOT_EQUAL(0, len); + const auto view = StringView{&buf[0], &buf[0] + len}; + + TEST_ASSERT(parse_simple_iso8601(parsed, utc, view)); + TEST_ASSERT_FALSE(utc); + + const auto seconds = datetime::to_seconds(parsed, utc); + TEST_ASSERT_EQUAL( + now.time_since_epoch().count(), seconds.count()); + + tm c_parsed = parsed.c_value(); + localtime_r(&ts, &c_parsed); + + TEST_ASSERT_EQUAL(local.tm_year, c_parsed.tm_year); + TEST_ASSERT_EQUAL(local.tm_mon, c_parsed.tm_mon); + TEST_ASSERT_EQUAL(local.tm_mday, c_parsed.tm_mday); + TEST_ASSERT_EQUAL(local.tm_hour, c_parsed.tm_hour); + TEST_ASSERT_EQUAL(local.tm_min, c_parsed.tm_min); + TEST_ASSERT_EQUAL(local.tm_sec, c_parsed.tm_sec); +} + } // namespace test } // namespace @@ -908,7 +1097,10 @@ int main(int, char**) { RUN_TEST(test_restore_today); RUN_TEST(test_restore_delta_future); RUN_TEST(test_restore_delta_past); + RUN_TEST(test_event_impl); + RUN_TEST(test_event_parsing); RUN_TEST(test_expect_today); + RUN_TEST(test_expect_delta_future); RUN_TEST(test_schedule_invalid_parsing); RUN_TEST(test_schedule_parsing_date); RUN_TEST(test_schedule_parsing_date_range); @@ -917,6 +1109,7 @@ int main(int, char**) { RUN_TEST(test_schedule_parsing_time_range); RUN_TEST(test_schedule_parsing_weekdays); RUN_TEST(test_schedule_parsing_weekdays_range); - RUN_TEST(test_bitmask); + RUN_TEST(test_search_bits); + RUN_TEST(test_datetime_parsing); return UNITY_END(); }