feat: add recurrence and time zone support
This commit is contained in:
@@ -1,13 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace nocal {
|
||||
|
||||
using Clock = std::chrono::system_clock;
|
||||
using TimePoint = Clock::time_point;
|
||||
|
||||
enum class TimeBasis { floating, utc, zoned };
|
||||
|
||||
enum class RecurrenceFrequency { daily, weekly, monthly, yearly };
|
||||
|
||||
struct RecurrenceRule {
|
||||
RecurrenceFrequency frequency{RecurrenceFrequency::daily};
|
||||
unsigned interval{1};
|
||||
std::optional<unsigned> count;
|
||||
std::optional<TimePoint> until;
|
||||
std::vector<std::chrono::weekday> by_weekday;
|
||||
std::vector<int> by_month_day;
|
||||
std::vector<unsigned> by_month;
|
||||
|
||||
bool operator==(const RecurrenceRule&) const = default;
|
||||
};
|
||||
|
||||
struct Event {
|
||||
std::string uid;
|
||||
std::string title;
|
||||
@@ -18,6 +36,10 @@ struct Event {
|
||||
std::string description;
|
||||
std::string calendar_id;
|
||||
std::string color;
|
||||
TimeBasis time_basis{TimeBasis::utc};
|
||||
std::string time_zone;
|
||||
std::optional<RecurrenceRule> recurrence;
|
||||
std::vector<TimePoint> recurrence_exceptions;
|
||||
};
|
||||
|
||||
struct Calendar {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "nocal/domain/date.hpp"
|
||||
#include "nocal/domain/event.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
@@ -12,6 +13,30 @@ namespace nocal {
|
||||
using EventRef = std::reference_wrapper<const Event>;
|
||||
using EventRefs = std::vector<EventRef>;
|
||||
|
||||
struct EventOccurrence {
|
||||
const Event* source{};
|
||||
TimePoint start{};
|
||||
TimePoint end{};
|
||||
std::size_t ordinal{};
|
||||
};
|
||||
|
||||
using EventOccurrences = std::vector<EventOccurrence>;
|
||||
|
||||
[[nodiscard]] bool occurrence_less(const EventOccurrence& lhs,
|
||||
const EventOccurrence& rhs) noexcept;
|
||||
void sort_occurrences(EventOccurrences& occurrences);
|
||||
|
||||
// Recurrence query intervals are half-open. DTSTART is ordinal zero and COUNT
|
||||
// includes it. EXDATE can remove any exact generated start, including DTSTART.
|
||||
// Invalid rules produce no additional occurrences but retain DTSTART unless it
|
||||
// is explicitly excepted.
|
||||
[[nodiscard]] EventOccurrences occurrences_overlapping(
|
||||
std::span<const Event> events, TimePoint begin, TimePoint end);
|
||||
[[nodiscard]] EventOccurrences occurrences_on_day(std::span<const Event> events,
|
||||
Date date);
|
||||
[[nodiscard]] EventOccurrences occurrences_in_month(std::span<const Event> events,
|
||||
YearMonth month);
|
||||
|
||||
[[nodiscard]] bool event_less(const Event& lhs, const Event& rhs) noexcept;
|
||||
void sort_events(EventRefs& events);
|
||||
|
||||
|
||||
@@ -7,9 +7,17 @@
|
||||
#include <string_view>
|
||||
|
||||
#include "nocal/domain/event.hpp"
|
||||
#include "nocal/domain/event_query.hpp"
|
||||
|
||||
namespace nocal::tui {
|
||||
|
||||
enum class ItemSegment {
|
||||
single,
|
||||
start,
|
||||
continuation,
|
||||
end,
|
||||
};
|
||||
|
||||
// A deliberately small view-model keeps the renderer independent of storage and
|
||||
// makes it straightforward to unit test. TuiApp adapts domain Events to this.
|
||||
struct CalendarItem {
|
||||
@@ -26,6 +34,10 @@ struct CalendarItem {
|
||||
std::string detail_title;
|
||||
bool detail_all_day{false};
|
||||
std::optional<std::chrono::minutes> detail_start_time_of_day;
|
||||
ItemSegment segment{ItemSegment::single};
|
||||
bool recurring{false};
|
||||
std::string recurrence_summary;
|
||||
std::string source_time_zone;
|
||||
};
|
||||
|
||||
struct ScreenState {
|
||||
@@ -50,11 +62,17 @@ struct ScreenState {
|
||||
[[nodiscard]] std::string render_month(std::span<const CalendarItem> items,
|
||||
const ScreenState& state);
|
||||
|
||||
// Convenience adapter for the domain model. Event timestamps are projected to
|
||||
// the process's local civil time until explicit calendar time zones are added.
|
||||
// Convenience adapter for the domain model. Occurrence instants are projected
|
||||
// into the user's process-local zone; the reader retains an explicit source TZID.
|
||||
[[nodiscard]] std::string render_month(std::span<const Event> events,
|
||||
const ScreenState& state);
|
||||
|
||||
// Stable focus identity shared by the renderer and controller. Non-recurring
|
||||
// events retain their historical UID/synthetic identity; recurring instances
|
||||
// additionally include their concrete start instant.
|
||||
[[nodiscard]] std::string occurrence_focus_id(std::span<const Event> events,
|
||||
const EventOccurrence& occurrence);
|
||||
|
||||
enum class Action {
|
||||
none,
|
||||
left,
|
||||
|
||||
@@ -72,6 +72,7 @@ private:
|
||||
std::vector<HistoryEntry> redo_history_;
|
||||
bool running_{true};
|
||||
|
||||
[[nodiscard]] std::optional<EventOccurrence> focused_occurrence() const;
|
||||
[[nodiscard]] std::optional<std::size_t> focused_event_index() const;
|
||||
[[nodiscard]] HistorySnapshot capture_history_snapshot() const;
|
||||
void restore_history_snapshot(HistorySnapshot snapshot);
|
||||
|
||||
Reference in New Issue
Block a user