Skip to content

Commit

Permalink
Merge pull request #65 from remi-dupre/easter
Browse files Browse the repository at this point in the history
1.0.0 Easter
  • Loading branch information
remi-dupre authored Feb 2, 2025
2 parents 24b0a8a + fb205f1 commit cf213b8
Show file tree
Hide file tree
Showing 11 changed files with 148 additions and 63 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## 1.0.0

That's not really a huge milestone, but:

- Every "obviously missing things" that I had in mind are implemented now.
- The API proved itself to be quite stable.

### General

- Add easter support

## 0.11.1

### Rust
Expand Down
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "opening-hours"
version = "0.11.1"
version = "1.0.0"
authors = ["Rémi Dupré <remi@dupre.io>"]
license = "MIT OR Apache-2.0"
readme = "README.md"
Expand Down Expand Up @@ -34,9 +34,9 @@ log = ["opening-hours-syntax/log", "dep:log"]

[dependencies]
chrono = "0.4"
compact-calendar = { path = "compact-calendar", version = "0.11.1" }
compact-calendar = { path = "compact-calendar", version = "1.0.0" }
flate2 = "1.0"
opening-hours-syntax = { path = "opening-hours-syntax", version = "0.11.1" }
opening-hours-syntax = { path = "opening-hours-syntax", version = "1.0.0" }
sunrise-next = "1.2"

# Feature: log (default)
Expand All @@ -51,7 +51,7 @@ tzf-rs = { version = "0.4", default-features = false, optional = true }

[build-dependencies]
chrono = "0.4"
compact-calendar = { path = "compact-calendar", version = "0.11.1" }
compact-calendar = { path = "compact-calendar", version = "1.0.0" }
country-boundaries = { version = "1.2", optional = true }
flate2 = "1.0"
rustc_version = "0.4.0"
Expand Down
2 changes: 1 addition & 1 deletion compact-calendar/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "compact-calendar"
version = "0.11.1"
version = "1.0.0"
authors = ["Rémi Dupré <remi@dupre.io>"]
license = "MIT OR Apache-2.0"
readme = "README.md"
Expand Down
6 changes: 3 additions & 3 deletions opening-hours-py/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "opening-hours-py"
version = "0.11.1"
version = "1.0.0"
authors = ["Rémi Dupré <remi@dupre.io>"]
license = "MIT OR Apache-2.0"
readme = "README.md"
Expand All @@ -21,12 +21,12 @@ pyo3-log = "0.12"

[dependencies.opening-hours]
path = ".."
version = "0.11.1"
version = "1.0.0"
features = ["log", "auto-country", "auto-timezone"]

[dependencies.opening-hours-syntax]
path = "../opening-hours-syntax"
version = "0.11.1"
version = "1.0.0"
features = ["log"]

[dependencies.pyo3]
Expand Down
2 changes: 1 addition & 1 deletion opening-hours-syntax/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "opening-hours-syntax"
version = "0.11.1"
version = "1.0.0"
authors = ["Rémi Dupré <remi@dupre.io>"]
license = "MIT OR Apache-2.0"
readme = "README.md"
Expand Down
80 changes: 37 additions & 43 deletions opening-hours/src/filter/date_filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ use opening_hours_syntax::rules::day::{self as ds, Month};

use crate::localization::Localize;
use crate::opening_hours::DATE_LIMIT;
use crate::utils::dates::count_days_in_month;
use crate::utils::dates::{count_days_in_month, easter};
use crate::utils::range::{RangeExt, WrappingRange};
use crate::Context;

/// Get the first valid date before give "yyyy/mm/dd", for example if
/// Get the first valid date before given "yyyy/mm/dd", for example if
/// 2021/02/30 is given, this will return february 28th as 2021 is not a leap
/// year.
fn first_valid_ymd(year: i32, month: u32, day: u32) -> NaiveDate {
Expand Down Expand Up @@ -138,6 +138,17 @@ impl DateFilter for ds::MonthdayRange {
let in_year = date.year() as u16;
let in_month = Month::from_date(date);

fn on_year(date: ds::Date, for_year: i32) -> Option<NaiveDate> {
match date {
ds::Date::Easter { year } => easter(year.map(Into::into).unwrap_or(for_year)),
ds::Date::Fixed { year, month, day } => Some(first_valid_ymd(
year.map(Into::into).unwrap_or(for_year),
month.into(),
day.into(),
)),
}
}

match self {
ds::MonthdayRange::Month { year, range } => {
year.unwrap_or(in_year) == in_year && range.wrapping_contains(&in_month)
Expand All @@ -146,48 +157,31 @@ impl DateFilter for ds::MonthdayRange {
start: (start, start_offset),
end: (end, end_offset),
} => {
match (start, end) {
(
&ds::Date::Fixed { year: year_1, month: month_1, day: day_1 },
&ds::Date::Fixed { year: year_2, month: month_2, day: day_2 },
) => {
let mut start = start_offset.apply(first_valid_ymd(
year_1.map(Into::into).unwrap_or(date.year()),
month_1.into(),
day_1.into(),
));

// If no year is specified we can shift of as many years as needed.
if year_1.is_none() && start > date {
start = start_offset.apply(first_valid_ymd(
year_1.map(Into::into).unwrap_or(date.year()) - 1,
month_1.into(),
day_1.into(),
))
}

let mut end = end_offset.apply(first_valid_ymd(
year_2.map(Into::into).unwrap_or(start.year()),
month_2.into(),
day_2.into(),
));

// If no year is specified we can shift of as many years as needed.
if year_2.is_none() && end < start {
end = end_offset.apply(first_valid_ymd(
year_2.map(Into::into).unwrap_or(start.year()) + 1,
month_2.into(),
day_2.into(),
))
}

(start..=end).contains(&date)
}
(_, ds::Date::Easter { year: _ }) | (ds::Date::Easter { year: _ }, _) => {
// TODO: Easter support
false
}
let mut start_date = match on_year(*start, date.year()) {
Some(date) => start_offset.apply(date),
None => return false,
};

if start_date > date {
start_date = match on_year(*start, date.year() - 1) {
Some(date) => start_offset.apply(date),
None => return false,
};
}

let mut end_date = match on_year(*end, start_date.year()) {
Some(date) => end_offset.apply(date),
None => return false,
};

if end_date < start_date {
end_date = match on_year(*end, start_date.year() + 1) {
Some(date) => end_offset.apply(date),
None => return false,
};
}

(start_date..=end_date).contains(&date)
}
}
}
Expand Down
40 changes: 40 additions & 0 deletions opening-hours/src/tests/weekday_selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,43 @@ fn holiday() {
assert!(oh.is_open(datetime!("2024-07-14 12:00")));
assert!(oh.is_closed(datetime!("2024-07-13 12:00")));
}

#[test]
fn easter_days() {
let oh = OpeningHours::parse("24/7 open ; easter off").unwrap();
assert!(oh.is_open(datetime!("2024-03-30 12:00")));
assert!(oh.is_closed(datetime!("2024-03-31 12:00")));
assert!(oh.is_open(datetime!("2024-04-01 12:00")));
}

#[test]
fn easter_interval() {
let oh = OpeningHours::parse("Jan01-easter").unwrap();
assert!(oh.is_closed(datetime!("2023-12-31 12:00")));
assert!(oh.is_open(datetime!("2024-01-01 12:00")));
assert!(oh.is_open(datetime!("2024-03-30 12:00")));
assert!(oh.is_open(datetime!("2024-03-31 12:00")));
assert!(oh.is_closed(datetime!("2024-04-01 12:00")));

let oh = OpeningHours::parse("easter-Dec31").unwrap();
assert!(oh.is_closed(datetime!("2024-03-30 12:00")));
assert!(oh.is_open(datetime!("2024-03-31 12:00")));
assert!(oh.is_open(datetime!("2024-12-31 12:00")));
assert!(oh.is_closed(datetime!("2025-01-01 12:00")));
}

#[test]
fn easter_interval_with_year() {
let oh = OpeningHours::parse("Jan01-easter").unwrap();
assert!(oh.is_closed(datetime!("2023-12-31 12:00")));
assert!(oh.is_open(datetime!("2024-01-01 12:00")));
assert!(oh.is_open(datetime!("2024-03-30 12:00")));
assert!(oh.is_open(datetime!("2024-03-31 12:00")));
assert!(oh.is_closed(datetime!("2024-04-01 12:00")));

let oh = OpeningHours::parse("easter-Dec31").unwrap();
assert!(oh.is_closed(datetime!("2024-03-30 12:00")));
assert!(oh.is_open(datetime!("2024-03-31 12:00")));
assert!(oh.is_open(datetime!("2024-12-31 12:00")));
assert!(oh.is_closed(datetime!("2025-01-01 12:00")));
}
6 changes: 0 additions & 6 deletions opening-hours/src/tests/year_selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,3 @@ fn range() -> Result<(), Error> {

Ok(())
}

#[test]
fn easter() -> Result<(), Error> {
assert_eq!(schedule_at!("easter", "2024-03-31"), schedule! {});
Ok(())
}
46 changes: 46 additions & 0 deletions opening-hours/src/utils/dates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,49 @@ pub(crate) fn count_days_in_month(date: NaiveDate) -> u8 {
.try_into()
.expect("time not monotonic while comparing dates")
}

/// Find Easter date for given year using.
///
/// See https://en.wikipedia.org/wiki/Date_of_Easter#Anonymous_Gregorian_algorithm
pub(crate) fn easter(year: i32) -> Option<NaiveDate> {
let a = year % 19;
let b = year / 100;
let c = year % 100;
let d = b / 4;
let e = b % 4;
let f = (b + 8) / 25;
let g = (b - f + 1) / 3;
let h = (19 * a + b - d - g + 15) % 30;
let i = c / 4;
let k = c % 4;
let l = (32 + 2 * e + 2 * i - h - k) % 7;
let m = (a + 11 * h + 22 * l) / 451;
let n = (h + l - 7 * m + 114) / 31;
let o = (h + l - 7 * m + 114) % 31;

NaiveDate::from_ymd_opt(
year,
n.try_into().expect("month cannot be negative"),
(o + 1).try_into().expect("day cannot be negative"),
)
}

#[cfg(test)]
mod test {
use super::easter;
use crate::date;

#[test]
fn test_easter() {
assert_eq!(easter(i32::MIN), None);
assert_eq!(easter(i32::MAX), None);
assert_eq!(easter(1901), Some(date!("1901-04-07")));
assert_eq!(easter(1961), Some(date!("1961-04-02")));
assert_eq!(easter(2024), Some(date!("2024-03-31")));
assert_eq!(easter(2025), Some(date!("2025-04-20")));
assert_eq!(easter(2050), Some(date!("2050-04-10")));
assert_eq!(easter(2106), Some(date!("2106-04-18")));
assert_eq!(easter(2200), Some(date!("2200-04-06")));
assert_eq!(easter(3000), Some(date!("3000-04-13")));
}
}
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "opening-hours-py"
version = "0.11.1"
version = "1.0.0"
description = "A parser for the opening_hours fields from OpenStreetMap."
authors = ["Rémi Dupré <remi@dupre.io>"]
package-mode = false
Expand Down

0 comments on commit cf213b8

Please sign in to comment.