feat: add recurrence and time zone support
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -215,6 +216,83 @@ void test_rendering()
|
||||
check(exact_width, "wide Unicode input retains exact terminal-cell width");
|
||||
}
|
||||
|
||||
nocal::TimePoint zoned_time(const std::string_view zone_name, const nocal::Date date,
|
||||
const unsigned hour, const unsigned minute)
|
||||
{
|
||||
const auto* zone = std::chrono::locate_zone(zone_name);
|
||||
const auto local = std::chrono::local_days{date} + std::chrono::hours{hour} +
|
||||
std::chrono::minutes{minute};
|
||||
return zone->to_sys(local, std::chrono::choose::earliest);
|
||||
}
|
||||
|
||||
void test_zoned_edit_preserves_series_and_rejects_dst_gap()
|
||||
{
|
||||
using namespace std::chrono;
|
||||
nocal::Event event;
|
||||
event.uid = "zoned-series";
|
||||
event.title = "New York standup";
|
||||
event.time_basis = nocal::TimeBasis::zoned;
|
||||
event.time_zone = "America/New_York";
|
||||
event.start = zoned_time(event.time_zone, nocal::Date{2026y / March / 7d}, 2, 30);
|
||||
event.end = zoned_time(event.time_zone, nocal::Date{2026y / March / 7d}, 3, 30);
|
||||
event.recurrence = nocal::RecurrenceRule{
|
||||
.frequency = nocal::RecurrenceFrequency::daily,
|
||||
.interval = 1,
|
||||
.count = 4,
|
||||
.until = std::nullopt,
|
||||
.by_weekday = {},
|
||||
.by_month_day = {},
|
||||
.by_month = {},
|
||||
};
|
||||
event.recurrence_exceptions.push_back(
|
||||
zoned_time(event.time_zone, nocal::Date{2026y / March / 9d}, 2, 30));
|
||||
|
||||
nocal::tui::EventEditor editor{event};
|
||||
check(editor.field_value(nocal::tui::EditorField::start_date) == "2026-03-07" &&
|
||||
editor.field_value(nocal::tui::EditorField::start_time) == "02:30",
|
||||
"a zoned editor displays civil values in the event source timezone");
|
||||
const auto form = editor.render(80, 18, false);
|
||||
check(form.find("America/New_York") != std::string::npos,
|
||||
"a zoned editor explicitly labels its source timezone");
|
||||
check(form.find("EDIT ENTIRE RECURRING SERIES") != std::string::npos,
|
||||
"the editor clearly states that edits affect the recurring series");
|
||||
replace_field(editor, "Renamed standup");
|
||||
check(editor.handle_key("\x13") == nocal::tui::EditorOutcome::submit,
|
||||
"a valid zoned series edit submits");
|
||||
check(editor.result().time_basis == nocal::TimeBasis::zoned &&
|
||||
editor.result().time_zone == "America/New_York" &&
|
||||
editor.result().recurrence == event.recurrence &&
|
||||
editor.result().recurrence_exceptions == event.recurrence_exceptions,
|
||||
"editing preserves time basis, TZID, recurrence, and exceptions");
|
||||
|
||||
nocal::tui::EventEditor series_kind_editor{event};
|
||||
focus(series_kind_editor, nocal::tui::EditorField::all_day);
|
||||
(void)series_kind_editor.handle_key(" ");
|
||||
check(series_kind_editor.handle_key("\x13") == nocal::tui::EditorOutcome::active &&
|
||||
series_kind_editor.focused_field() == nocal::tui::EditorField::all_day &&
|
||||
series_kind_editor.error().find("recurring series") != std::string_view::npos,
|
||||
"a recurring series rejects timed/all-day conversion to preserve its exceptions");
|
||||
|
||||
auto one_off = event;
|
||||
one_off.recurrence.reset();
|
||||
one_off.recurrence_exceptions.clear();
|
||||
nocal::tui::EventEditor all_day_editor{one_off};
|
||||
focus(all_day_editor, nocal::tui::EditorField::all_day);
|
||||
(void)all_day_editor.handle_key(" ");
|
||||
check(all_day_editor.handle_key("\x13") == nocal::tui::EditorOutcome::submit &&
|
||||
all_day_editor.result().all_day &&
|
||||
all_day_editor.result().time_basis == nocal::TimeBasis::floating &&
|
||||
all_day_editor.result().time_zone.empty(),
|
||||
"a one-off zoned event converted to all-day normalizes to floating DATE semantics");
|
||||
|
||||
nocal::tui::EventEditor gap_editor{event};
|
||||
focus(gap_editor, nocal::tui::EditorField::start_date);
|
||||
replace_field(gap_editor, "2026-03-08");
|
||||
check(gap_editor.handle_key("\x13") == nocal::tui::EditorOutcome::active &&
|
||||
gap_editor.error().find("cannot be represented") != std::string_view::npos,
|
||||
"a nonexistent source-zone civil time in the DST gap is rejected");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
@@ -225,6 +303,7 @@ int main()
|
||||
test_all_day_exclusive_end();
|
||||
test_unicode_backspace_navigation_and_cancel();
|
||||
test_rendering();
|
||||
test_zoned_edit_preserves_series_and_rejects_dst_gap();
|
||||
if (failures != 0) {
|
||||
std::cerr << failures << " editor test(s) failed\n";
|
||||
return EXIT_FAILURE;
|
||||
|
||||
Reference in New Issue
Block a user