Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.5.0] - 2026-03-23

### Added
- **Calendar view iterators**: `Calendar::days()`, `days_back()`, `weeks()`, and `weeks_back()` provide lazy day/week traversal for UI-oriented calendar rendering.
- **Owned day/week view models**: Added `DayView`, `WeekView`, and `OwnedEventOccurrence` so iterator output can be collected and moved independently of the source `Calendar`.
- **Calendar views example**: Added `examples/calendar_views.rs` to demonstrate day/week traversal and UI-friendly mapping patterns.
- Integration tests: shared `tests/common` helpers (`parse`); coverage-focused cases distributed across `calendar_view_tests`, `timezone_ics_tests`, `gap_validation_tests`, `property_tests`, and `booking_status_tests`.
- Unit tests: ICS path round-trip and parse edge cases; timezone `local_day_window` (spring-forward) and `convert_timezone`; calendar JSON (malformed input, calendar-level timezone round-trip).

### Changed
- **[BREAKING] Fallible view iteration**: `DayIterator` and `WeekIterator` now yield `Result<DayView>` / `Result<WeekView>` items instead of silently stopping on expansion errors.
- **Week back-navigation**: `WeekIterator::backward()` now yields proper contiguous Monday-Sunday blocks when walking into the past.
- **[BREAKING] Explicit day boundaries**: `DayView::end()` now returns the exclusive next-midnight boundary, with `end_inclusive()` available for display-only scenarios.
- **[BREAKING]** `Calendar::events_on_date()` and `Event::occurs_on()` now use an exclusive next-day boundary, improving correctness for occurrences that start exactly at midnight boundaries and for DST-length days.
- `Calendar::to_json`: serialization failures are returned as `EventixError::Other` instead of panicking.
- `local_day_window`: validation errors now say “Failed to resolve start/end time” (clearer than “Ambiguous”).
- `rustfmt.toml`: stricter line breaking (`use_small_heuristics = "Off"`, `chain_width`).

### Fixed
- `expand_weekdays_in_month`: advance with `succ_opt()` without panicking on unexpected `None`.
- README: calendar view iterator example imports `Duration`.

### Documentation
- `Calendar::from_json`: documented that an invalid top-level `"timezone"` string is ignored (lenient import); event-level timezones must still parse.

## [0.4.0] - 2026-03-07

### Added
Expand Down
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "eventix"
version = "0.4.0"
version = "0.5.0"
edition = "2021"
authors = ["Raj Sarkar <ariajsarkar@gmail.com>"]
description = "High-level calendar & recurrence crate with timezone-aware scheduling, exceptions, and ICS import/export"
Expand All @@ -18,6 +18,7 @@ exclude = [
"TODO.md",
"llms.txt",
"rustfmt.toml",
"schedule.ics",
]

[dependencies]
Expand Down
54 changes: 52 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
# Eventix 📅

A high-level calendar and recurrence library for Rust with timezone-aware scheduling, exceptions, and ICS import/export.
A high-level calendar and recurrence library for Rust with timezone-aware scheduling, exceptions, ICS import/export, and lazy calendar views.

[![Crates.io](https://img.shields.io/crates/v/eventix.svg)](https://crates.io/crates/eventix)
[![Documentation](https://docs.rs/eventix/badge.svg)](https://docs.rs/eventix)
[![CI](https://github.com/AriajSarkar/eventix/workflows/EventixCI/badge.svg)](https://github.com/AriajSarkar/eventix/actions)
[![License](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](LICENSE)
![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/AriajSarkar/eventix?utm_source=oss&utm_medium=github&utm_campaign=AriajSarkar%2Feventix&labelColor=171717&color=FF570A&link=https%3A%2F%2Fcoderabbit.ai&label=CodeRabbit+Reviews)

## Features

- 🌍 **Timezone-aware events** - Full support for timezones and DST handling using `chrono-tz`
- 🔄 **Recurrence patterns** - All seven RFC 5545 frequencies (secondly, minutely, hourly, daily, weekly, monthly, yearly) with advanced rules
- 🗓️ **Calendar view iterators** - Lazy day/week traversal for UI rendering and infinite-scroll agendas
- 🚫 **Exception handling** - Skip specific dates, weekends, or custom holiday lists
- 🚦 **Booking workflow** - Manage event status (`Confirmed`, `Tentative`, `Cancelled`) with smart gap validation
- 📅 **ICS support** - Import and export events using the iCalendar (`.ics`) format
Expand All @@ -28,14 +30,15 @@ A high-level calendar and recurrence library for Rust with timezone-aware schedu
| **Booking State** | ✅ Confirmed/Cancelled | ❌ No Concept | ❌ No Concept |
| **Timezone/DST** | ✅ Built-in (`chrono-tz`) | ⚠️ Partial | ✅ Built-in |
| **Recurrence** | ✅ RRule + Exdates | ✅ RRule | ❌ None |
| **View Iterators** | ✅ Day/Week lazy APIs | ❌ Manual Grouping | ❌ Manual Logic |

## Quick Start

Add eventix to your `Cargo.toml`:

```toml
[dependencies]
eventix = "0.4.0"
eventix = "0.5.0"
```

### Basic Usage
Expand Down Expand Up @@ -180,6 +183,49 @@ let mondays: Vec<_> = daily.occurrences(start)
.collect();
```

### Calendar View Iterators

For UI rendering, `Calendar::days()` and `Calendar::weeks()` lazily bucket
active occurrences into day/week views. This avoids choosing a large query
window up front and maps cleanly into frontend components. The iterators yield
`Result<DayView>` / `Result<WeekView>` so expansion errors stay explicit.

```rust
use eventix::{Calendar, Duration, Event, Recurrence, timezone};

let mut cal = Calendar::new("Personal");
cal.add_event(
Event::builder()
.title("Standup")
.start("2025-11-04 10:00:00", "America/New_York")
.duration(Duration::minutes(15))
.recurrence(Recurrence::daily().count(10))
.build()?
);

let tz = timezone::parse_timezone("America/New_York")?;
let start = timezone::parse_datetime_with_tz("2025-11-03 00:00:00", tz)?;

let busy_days: Vec<_> = cal.days(start)
.take(14)
.collect::<eventix::Result<Vec<_>>>()?
.into_iter()
.filter(|day| !day.is_empty())
.collect();

for week in cal.weeks(start).take(2) {
let week = week?;
println!("{} -> {}", week.start_date(), week.event_count());
}
```

`DayView::start()` and `DayView::end()` expose the actual half-open day window
`[start, end)`, so `end()` is the next midnight. Use `end_inclusive()` only for
display formatting. Day and week views are built by interval intersection, so
overnight events appear on every day they overlap. If you're passing large
`DayView` values through Yew props, wrapping them in `Rc<DayView>` can avoid
expensive prop clones.

### Booking Status

```rust
Expand Down Expand Up @@ -329,6 +375,9 @@ Run the examples:
# Basic calendar usage
cargo run --example basic

# Day/week calendar views
cargo run --example calendar_views

# Recurrence patterns
cargo run --example recurrence

Expand All @@ -352,6 +401,7 @@ The crate is organized into several modules:
- **`calendar`** - Calendar container for managing events
- **`event`** - Event types and builder API
- **`recurrence`** - Recurrence rules and patterns
- **`views`** - Lazy day/week calendar view iterators
- **`ics`** - ICS format import/export
- **`timezone`** - Timezone handling and DST support
- **`gap_validation`** - Schedule analysis, gap detection, conflict resolution
Expand Down
49 changes: 49 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,52 @@ This document outlines the strategic technical direction for the `eventix` crate
- [ ] **Property-Based Testing** `(Priority: High)`
- **Action**: Implement `proptest` suites for `gap_validation`.
- **Goal**: mathematically guarantee no overlapping slots are missed under edge-case conditions (e.g., DST transitions).

## 5. Calendar View API
*Objective: Expose lazy, UI-friendly calendar traversal primitives for day/week rendering.*

- [x] **Day and Week View Iterators** `(Priority: Medium)`
- **Feature**: Add lazy `Calendar::days()` / `days_back()` and `Calendar::weeks()` / `weeks_back()` iterators that yield pre-bucketed `DayView` and `WeekView` values.
- **Use Case**: Lets consumers render personal calendar UIs in frameworks like Yew, Leptos, and Dioxus without eagerly expanding wide date ranges or manually grouping occurrences by day.

## 6. v0.5.1 API Polish
*Objective: Small additive improvements requested by early users and follow-up review feedback.*

- [ ] **Calendar `FromStr` parsing** `(Priority: Medium)`
- **Feature**: Parse ICS data directly from `&str` so callers can load calendar payloads without a temporary file.
- **Value**: Useful for tests, embedded fixtures, and API handlers that already have the raw string body.

- [ ] **Streaming ICS import** `(Priority: Medium)`
- **Feature**: Add `Calendar::from_ics_reader(impl Read)` for incremental loading from files, sockets, or database-backed readers.
- **Value**: Avoids forcing all ICS payloads into memory up front.

- [ ] **DayView date conversions** `(Priority: Low)`
- **Feature**: Implement `From<DayView> for NaiveDate` and `From<&DayView> for NaiveDate`.
- **Value**: Makes calendar views easier to feed into UI and serialization layers.

- [ ] **Yew ergonomics docs** `(Priority: Low)`
- **Feature**: Document the `Rc<DayView>` wrapping pattern for component props.
- **Value**: Clarifies the intended usage for UI users without changing runtime behavior.

- [ ] **Optional serde for views** `(Priority: Low)`
- **Feature**: Gate `DayView` and `WeekView` serialization behind an opt-in `serde` feature.
- **Value**: Helps SSR and persistence consumers without imposing serde on every downstream build.

## 7. v0.6.0 Performance / Bigger Features
*Objective: Scale out the calendar view layer and import path for heavier workloads.*

- [ ] **K-way merge optimization** `(Priority: Medium)`
- **Feature**: Merge per-event occurrence streams instead of materializing every event's range independently.
- **Value**: Reduces work when rendering large day/week windows across many recurring events.

- [ ] **Chunked/lazy ICS loading** `(Priority: Low)`
- **Feature**: Stream ICS parsing in chunks so enterprise-scale calendars do not need to fit fully in memory.
- **Value**: Better for very large imports, long-running services, and database-backed sync pipelines.

- [ ] **Configurable week start** `(Priority: Low)`
- **Feature**: Allow Sunday-first calendars for regions that expect that convention.
- **Value**: Improves UX for US-style calendar rendering without changing the default ISO behavior.

- [ ] **MonthView / YearView iterators** `(Priority: Low)`
- **Feature**: Extend the lazy view model beyond day/week traversal.
- **Value**: Useful for full calendar grids and high-level overview screens.
90 changes: 90 additions & 0 deletions examples/calendar_views.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//! Demonstrates lazy calendar day/week view iteration.

use eventix::{timezone, Calendar, Duration, Event, EventStatus, Recurrence};

fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut calendar = Calendar::new("Personal Planner");

calendar.add_event(
Event::builder()
.title("Design Review")
.start("2025-11-03 09:00:00", "America/New_York")
.duration_hours(1)
.location("Room A")
.build()?,
);

calendar.add_event(
Event::builder()
.title("Daily Standup")
.start("2025-11-04 10:00:00", "America/New_York")
.duration(Duration::minutes(15))
.recurrence(Recurrence::daily().count(10))
.build()?,
);

calendar.add_event(
Event::builder()
.title("Draft Blog Post")
.start("2025-11-05 13:00:00", "America/New_York")
.duration_minutes(45)
.status(EventStatus::Tentative)
.build()?,
);

let tz = timezone::parse_timezone("America/New_York")?;
let start = timezone::parse_datetime_with_tz("2025-11-03 00:00:00", tz)?;

println!("Next 7 days:");
for day in calendar.days(start).take(7) {
let day = day?;
println!("{} -> {} events", day.date(), day.event_count());
for occurrence in day.events() {
println!(
" - {} at {} ({:?})",
occurrence.title(),
occurrence.occurrence_time.format("%H:%M %Z"),
occurrence.status
);
}
}

println!("\nNext 2 weeks:");
for week in calendar.weeks(start).take(2) {
let week = week?;
println!(
"Week {} to {} -> {} events",
week.start_date(),
week.end_date(),
week.event_count()
);
for day in week.days() {
println!(" {} -> {} events", day.date(), day.event_count());
}
}

// A UI layer can map DayView values directly into components.
let busy_day_titles: Vec<_> = calendar
.days(start)
.take(14)
.collect::<eventix::Result<Vec<_>>>()?
.into_iter()
.filter(|day| !day.is_empty())
.map(|day| {
let titles = day
.events()
.iter()
.map(|event| event.title().to_string())
.collect::<Vec<_>>()
.join(", ");
format!("{} => {}", day.date(), titles)
})
.collect();

println!("\nBusy days:");
for line in busy_day_titles {
println!(" {line}");
}

Ok(())
}
54 changes: 16 additions & 38 deletions rustfmt.toml
Original file line number Diff line number Diff line change
@@ -1,48 +1,26 @@
# CrabGraph Rustfmt Configuration
# eventix — rustfmt (stable `cargo fmt` compatible)
# Run: cargo fmt --all

# Edition
edition = "2021"

# Formatting
# Primary line length.
max_width = 100
hard_tabs = false
tab_spaces = 4
newline_style = "Auto"

# Imports
imports_granularity = "Crate"
group_imports = "StdExternalCrate"

# Function calls
use_small_heuristics = "Default"
fn_call_width = 80
fn_single_line = false

# Chains
# Break method / builder chains sooner (sub-limit of max_width).
chain_width = 80

# Comments
comment_width = 100
wrap_comments = true
normalize_comments = true
normalize_doc_attributes = true
# "Off" = stricter wrapping at max_width (fewer very long lines than "Default").
use_small_heuristics = "Off"

# Attributes
reorder_impl_items = false

# Match
match_block_trailing_comma = false
match_arm_blocks = true
match_arm_leading_pipes = "Never"

# Spacing
spaces_around_ranges = false
type_punctuation_density = "Wide"
hard_tabs = false
tab_spaces = 4
newline_style = "Auto"

# Misc
reorder_imports = true
reorder_modules = true
remove_nested_parens = true
format_code_in_doc_comments = true
format_macro_matchers = true
format_macro_bodies = true

# --- Nightly rustfmt only (ignored on stable; use `cargo +nightly fmt --all`) ---
# imports_granularity = "Module"
# group_imports = "StdExternalCrate"
# wrap_comments = true
# comment_width = 80
# format_code_in_doc_comments = true
Loading
Loading