Skip to content
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
7b49d2e
enhancement: cherry pick the readme.md for merge in dev
AriajSarkar Dec 6, 2025
7693a72
chore: reorder release workflow, exclude unnecessary files, rename CI
AriajSarkar Dec 8, 2025
79652b1
Merge branch 'main' into dev
AriajSarkar Dec 18, 2025
6b5be7a
feat(recurrence): add lazy OccurrenceIterator for memory-efficient it…
AriajSarkar Mar 6, 2026
604ad97
perf(gap_validation): replace O(N²) pair-check with O(N log N) sweep-…
AriajSarkar Mar 6, 2026
1a0335a
test: strengthen event validation and schedule analysis test coverage
AriajSarkar Mar 6, 2026
5674e0b
perf: add criterion benchmark suite and tidy dependency versions
AriajSarkar Mar 6, 2026
b21ea87
feat: add json_web example for JSON serialization workflows
AriajSarkar Mar 6, 2026
6bcbd94
ci: redesign release workflow with Cargo.toml-authoritative versioning
AriajSarkar Mar 6, 2026
282885b
chore: remove DEVELOPMENT.md and TESTING.md
AriajSarkar Mar 6, 2026
02d4947
docs: update CHANGELOG for v0.3.2
AriajSarkar Mar 6, 2026
bc9c7d6
refactor(recurrence): extract advance_by_frequency shared helper
AriajSarkar Mar 6, 2026
37c1b95
style(gap_validation): clarify checkpoint sort ordering
AriajSarkar Mar 6, 2026
a504af4
fix(ci): use python3 instead of python in release workflow
AriajSarkar Mar 6, 2026
70bf49d
fix(examples): remove recurrence from json_web examples
AriajSarkar Mar 6, 2026
9f9a776
fix: fmt error
AriajSarkar Mar 6, 2026
39321c1
test(recurrence): cover uncovered OccurrenceIterator branches
AriajSarkar Mar 6, 2026
fdf38f7
fix(gap_validation): use BTreeSet for deterministic overlap ordering
AriajSarkar Mar 6, 2026
05671b4
fix(recurrence): clamp day for Monthly/Yearly instead of terminating
AriajSarkar Mar 6, 2026
0fb25e9
fix(recurrence): guard interval==0 and implement weekdays filtering
AriajSarkar Mar 6, 2026
4f22e72
fix(recurrence): add TODO for handling silent drops of valid occurren…
AriajSarkar Mar 6, 2026
0aa7f17
fix(recurrence): count emitted results not scanned slots, fix DST drift
AriajSarkar Mar 6, 2026
d683b15
fix(event): use intersection filter in occurrences_between
AriajSarkar Mar 6, 2026
9b46252
chore: bump to v0.4.0, rewrite changelog for minor release
AriajSarkar Mar 6, 2026
19a78e0
fix(recurrence): DST gap fallback, lazy occurrences_between, weekly w…
AriajSarkar Mar 6, 2026
1d822e0
fix(recurrence): proper DST gap handling via pre-gap UTC offset, DRY …
AriajSarkar Mar 6, 2026
11f7552
Add full sub-daily recurrence and lazy capped occurrence filtering
AriajSarkar Mar 8, 2026
841b2ba
fmt
AriajSarkar Mar 8, 2026
d59160e
chore: update changelog and README for new eager cap helper and break…
AriajSarkar Mar 10, 2026
8c693ff
fix(recurrence): enforce bounded recurrences in generate_occurrences …
AriajSarkar Mar 10, 2026
b1a0c48
fmt
AriajSarkar Mar 10, 2026
7b5651d
fix(docs): clarify error type in generate_occurrences documentation
AriajSarkar Mar 10, 2026
83bb72a
ai review fixes
AriajSarkar Mar 10, 2026
13a876f
feat: enhance recurrence handling and documentation
AriajSarkar Mar 10, 2026
145554c
fix(recurrence): improve error handling for recurrence and exdates in…
AriajSarkar Mar 12, 2026
3291f08
fix(ics): improve error handling for EXDATE parsing and BYDAY validation
AriajSarkar Mar 12, 2026
9e0d939
fix(ics): enforce RFC 5545 rules by rejecting COUNT and UNTIL togethe…
AriajSarkar Mar 12, 2026
b47225e
fix(changelog): update `generate_occurrences()` to return an error fo…
AriajSarkar Mar 12, 2026
d78b7e1
fix(recurrence): improve error handling for interval overflow and val…
AriajSarkar Mar 12, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/rust.yml → .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Rust CI
name: EventixCI

on:
push:
Expand Down
132 changes: 107 additions & 25 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,58 @@ jobs:

- name: Run all tests
run: |
cargo test --all-features --verbose
cargo clippy --all-features -- -D warnings
set -euo pipefail
cargo test --all-features --all-targets --verbose
cargo clippy --all-features --all-targets -- -D warnings
cargo fmt -- --check

- name: Build documentation
run: cargo doc --all-features --no-deps

- name: Security audit
run: |
set -euo pipefail
cargo install cargo-audit
cargo audit

- name: Resolve release version
id: release_meta
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
run: |
set -euo pipefail
PACKAGE_VERSION=$(python3 -c "import tomllib; print(tomllib.load(open('Cargo.toml', 'rb'))['package']['version'])")
TAG_VERSION="${GITHUB_REF#refs/tags/v}"

echo "Cargo.toml version: $PACKAGE_VERSION"
echo "Git tag version: $TAG_VERSION"

if [ "$PACKAGE_VERSION" != "$TAG_VERSION" ]; then
echo "⚠️ Tag v$TAG_VERSION does not match Cargo.toml version $PACKAGE_VERSION"
echo "⚠️ Using Cargo.toml version as the authoritative README/publish version"
fi

echo "version=$PACKAGE_VERSION" >> "$GITHUB_OUTPUT"

- name: Prepare README for publish
id: prepare_readme
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
run: |
set -euo pipefail
VERSION="${{ steps.release_meta.outputs.version }}"
echo "Ensuring README dependency snippets match Cargo.toml version $VERSION"

sed -i "s/^\([[:space:]]*\)eventix = \".*\"/\1eventix = \"$VERSION\"/" README.md

if git diff --quiet README.md; then
echo "README already matches Cargo.toml version $VERSION"
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "README updated in working tree before publish"
echo "changed=true" >> "$GITHUB_OUTPUT"
git diff -- README.md
fi

# Publish the tagged source, with README normalized from Cargo.toml just before packaging
- name: Publish to crates.io
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
env:
Expand All @@ -53,41 +93,83 @@ jobs:
LICENSE-MIT
LICENSE-APACHE

- name: Update README version
- name: Persist README version on main
id: sync_readme_main
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
run: |
# Checkout main branch for updates (release triggered on tag)
git checkout main
git pull origin main --rebase

# Configure Git
set -euo pipefail
VERSION="${{ steps.release_meta.outputs.version }}"

git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'

# Extract version from tag (e.g., v0.3.0 -> 0.3.0)
VERSION=${GITHUB_REF#refs/tags/v}
echo "Updating README to version $VERSION"

# Replace 'eventix = "..."' with 'eventix = "$VERSION"', preserving indentation

# Discard pre-publish README edit before switching branches
git checkout -- README.md

git checkout main
git pull origin main --rebase

sed -i "s/^\([[:space:]]*\)eventix = \".*\"/\1eventix = \"$VERSION\"/" README.md

# Check for changes

if git diff --quiet README.md; then
echo "No changes to README.md"
echo "README already up to date on main"
echo "commit_sha=" >> "$GITHUB_OUTPUT"
else
git commit -am "docs: update readme version to $VERSION [skip ci]"
git push origin main
echo "Pushed README update to main"

COMMIT_SHA=$(git rev-parse HEAD)
echo "commit_sha=$COMMIT_SHA" >> "$GITHUB_OUTPUT"
echo "Pushed README update to main ($COMMIT_SHA)"
fi

- name: Merge main into dev
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
- name: Sync README update to dev
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && steps.sync_readme_main.outputs.commit_sha != ''
run: |
VERSION=${GITHUB_REF#refs/tags/v}
echo "Merging main into dev..."
set -euo pipefail
COMMIT_SHA="${{ steps.sync_readme_main.outputs.commit_sha }}"
echo "Syncing README commit $COMMIT_SHA to dev..."

git checkout dev
git pull origin dev --rebase
git merge origin/main -m "merge: sync main to dev after release $VERSION"
git push origin dev
echo "Merged main into dev"

# Check if commit is already on dev
if git merge-base --is-ancestor "$COMMIT_SHA" HEAD; then
echo "ℹ️ SKIPPED: Commit $COMMIT_SHA is already on dev branch"
exit 0
fi

# Attempt cherry-pick
echo "🍒 Cherry-picking commit $COMMIT_SHA..."
if git cherry-pick "$COMMIT_SHA" 2>&1; then
git push origin dev
echo "✅ SUCCESS: Cherry-picked and pushed README update to dev"
else
PICK_EXIT=$?
# Check if cherry-pick produced an empty commit (change already present)
if git diff --cached --quiet 2>/dev/null; then
echo "ℹ️ Cherry-pick is empty — change already present on dev"
git cherry-pick --abort 2>/dev/null || true
exit 0
fi

# Real conflict
echo "⚠️ Cherry-pick failed with exit code $PICK_EXIT"

# Abort the failed cherry-pick
git cherry-pick --abort 2>/dev/null || true

# Reset to clean state
git reset --hard HEAD

echo "❌ CONFLICT: Could not cherry-pick README update to dev"
echo " The commit $COMMIT_SHA conflicts with current dev branch."
echo " Manual resolution required:"
echo " git checkout dev"
echo " git cherry-pick $COMMIT_SHA"
echo " # resolve conflicts"
echo " git push origin dev"

# Exit with error to make the conflict visible
exit 1
fi
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,35 @@ 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.4.0] - 2026-03-07

### Added
- **Lazy recurrence iteration**: `Recurrence::occurrences()` returns an `OccurrenceIterator` for memory-efficient, on-demand occurrence generation.
- **Eager cap helper**: `Recurrence::generate_occurrences_capped()` provides capped eager collection, replacing the old `max_occurrences` parameter of `generate_occurrences()`.
- **Weekday filtering**: `Recurrence::weekdays()` builder method to restrict occurrences to specific days of the week.
- **Benchmark suite**: Criterion benchmarks for overlap/gap detection, density analysis, recurrence generation, and slot availability.
- **JSON/web examples**: Examples showing calendar import/export as JSON for API workflows.

### Changed
- **[BREAKING] `generate_occurrences()` signature**: Changed from `generate_occurrences(start, max_occurrences)` to `generate_occurrences(start)` — now collects all occurrences defined by the recurrence's own `count`/`until` bounds. Use `generate_occurrences_capped(start, max)` for the old capped behavior.
- **[BREAKING] Count semantics**: `Recurrence::count(n)` now caps *emitted* occurrences, not scanned candidates. With weekday filters, `count(14)` yields exactly 14 matching days instead of scanning 14 slots. This affects both `generate_occurrences()` and `occurrences_between()`.
- **[BREAKING] Intersection-based filtering**: `Event::occurrences_between()` now uses time-range intersection (`dt < end && dt + duration > start`) instead of start-in-range filtering. Events starting before a query window but extending into it are now correctly included.
- **[BREAKING] Monthly/Yearly day clamping**: Monthly and yearly recurrence now clamps the day to the valid range (e.g. Jan 31 → Feb 28) instead of terminating the series. Code that relied on early termination will see additional occurrences.
- **Full sub-daily recurrence support**: `Hourly`, `Minutely`, and `Secondly` frequencies are now fully implemented. Sub-daily advancement uses UTC-duration arithmetic (`DateTime<Tz> + Duration`) so DST transitions are handled transparently without any local-time look-up. New convenience constructors `Recurrence::hourly()`, `minutely()`, and `secondly()` are provided. `Recurrence::new()` remains infallible and accepts all seven RFC 5545 frequencies.
- **Faster overlap detection**: `find_overlaps()` uses an O(N log N) sweep-line algorithm instead of O(N²).
- **Correct boundary handling**: Back-to-back events that merely touch at a boundary are no longer reported as overlapping.
- **DST-safe recurrence**: Daily/Weekly recurrence uses local date arithmetic to preserve wall-clock time across DST transitions.
- **DST spring-forward resilience**: Recurrence generation uses pre-gap UTC offset conversion to land on the correct post-transition wall-clock time (e.g. 2:30 AM EST → 3:30 AM EDT), matching Google Calendar / RFC 5545 behaviour. Subsequent occurrences return to the originally intended time rather than drifting.
- **Deterministic overlap ordering**: `find_overlaps()` uses `BTreeSet` for consistent results.
- **Zero-duration events**: No longer interfere with overlap detection.

### Fixed
- `occurs_on()` now correctly finds later occurrences of recurring events by using lazy iteration with post-filter capping.
- `occurrences_between()` now applies recurrence filters and exception dates lazily *before* the `max_occurrences` cap, so filtered-out dates don't consume result slots and capped queries stop after enough accepted results.
- `interval(0)` returns no occurrences instead of looping infinitely (previously caused an infinite loop). Consistent across all frequencies including weekly with BYDAY rules.
- `generate_occurrences()` now returns an error for unbounded recurrences (no `count` or `until`). Use `occurrences()` for lazy iteration or `generate_occurrences_capped()` for a hard cap.
- `to_rrule_string()` now converts `UNTIL` values to UTC before appending the `Z` suffix, per RFC 5545 compliance.

## [0.3.1] - 2025-12-18

### Added
Expand Down
27 changes: 20 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "eventix"
version = "0.3.1"
version = "0.4.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 @@ -10,21 +10,34 @@ readme = "README.md"
homepage = "https://github.com/AriajSarkar/eventix"
keywords = ["calendar", "scheduling", "booking", "availability", "ics"]
categories = ["date-and-time"]
exclude = ["usage-examples/"]
exclude = [
"usage-examples/",
"examples_output/",
".github/",
"target/",
"TODO.md",
"llms.txt",
"rustfmt.toml",
]

[dependencies]
chrono = { version = "0.4", features = ["serde"] }
chrono-tz = { version = "0.10", features = ["serde"] }
rrule = "0.14"
icalendar = "0.17"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "2.0"
uuid = { version = "1.0", features = ["v4"] }
serde_json = "1"
thiserror = "2"
uuid = { version = "1", features = ["v4"] }

[dev-dependencies]
anyhow = "1.0"
proptest = "1.0"
anyhow = "1"
proptest = "1.10"
criterion = { version = "0.8", features = ["html_reports"] }

[[bench]]
name = "performance"
harness = false

[lints.clippy]
unwrap_used = "warn"
Expand Down
Loading
Loading