Skip to content

Commit

Permalink
Merge pull request #353 from woshilapin/bank-holiday
Browse files Browse the repository at this point in the history
Add bank holidays management in TransXChange
  • Loading branch information
datanel committed Sep 11, 2019
2 parents abba04c + 10e8d77 commit f89d728
Show file tree
Hide file tree
Showing 9 changed files with 941 additions and 181 deletions.
1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ minidom = "0.9.0"
num-traits = "0.2.6"
pretty_assertions = "0.6.1"
proj = { version = "0.9.3", optional = true }
regex = "1.1.2"
rust_decimal = "1.0.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Expand Down
200 changes: 200 additions & 0 deletions src/transxchange/bank_holidays.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// copyright 2017 kisio digital and/or its affiliates.
//
// This program is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see
// <http://www.gnu.org/licenses/>.

//! Module to handle Bank Holidays in UK
//! The data structure is based on the JSON provided by the UK government at
//! https://www.gov.uk/bank-holidays.json

use crate::{
objects::{Date, ValidityPeriod},
Result,
};
use chrono::Datelike;
use serde::Deserialize;
use std::{collections::HashMap, fs::File, path::Path};

#[derive(Debug, Deserialize)]
pub struct BankHolidayRegion {
events: Vec<BankHolidayEvent>,
}

pub fn date_from_string<'de, D>(deserializer: D) -> std::result::Result<Date, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;

Date::parse_from_str(&s, "%Y-%m-%d").map_err(serde::de::Error::custom)
}

pub fn bank_holiday_from_string<'de, D>(
deserializer: D,
) -> std::result::Result<BankHoliday, D::Error>
where
D: serde::Deserializer<'de>,
{
let title = String::deserialize(deserializer)?;
use BankHoliday::*;
// All of the following are equivalent and should be `BankHoliday::EarlyMay`
// - "Early May bank holiday"
// - "Early May bank holiday (VE Day)"
// Therefore, we trim anything that is in parenthesis at the end
let parenthesis_offset = title.find('(').unwrap_or_else(|| title.len());
let day = match title[0..parenthesis_offset].trim() {
"New Year’s Day" => NewYearHoliday,
"2nd January" => JanuarySecondHoliday,
"St Patrick’s Day" => SaintPatrick,
"Good Friday" => GoodFriday,
"Easter Monday" => EasterMonday,
"Early May bank holiday" => EarlyMay,
"Spring bank holiday" => Spring,
"Queen’s Diamond Jubilee" => QueensDiamondJubilee,
"Battle of the Boyne" => BattleOfTheBoyne,
"Summer bank holiday" => Summer,
"St Andrew’s Day" => SaintAndrewsHoliday,
"Christmas Day" => ChristmasHoliday,
"Boxing Day" => BoxingDayHoliday,
title => {
return Err(serde::de::Error::custom(format!(
"Failed to match '{}' with a known bank holiday",
title
)))
}
};
Ok(day)
}

#[derive(Debug, Deserialize)]
struct BankHolidayEvent {
#[serde(deserialize_with = "bank_holiday_from_string")]
title: BankHoliday,
#[serde(deserialize_with = "date_from_string")]
date: Date,
}

#[derive(Clone, Debug, Ord, PartialOrd, PartialEq, Eq, Hash)]
pub enum BankHoliday {
NewYear,
// Bank Holiday for New Year, not necessarily on the 1st of January
NewYearHoliday,
JanuarySecond,
// Bank Holiday for January Second, not necessarily on the 2nd of January
JanuarySecondHoliday,
SaintPatrick,
GoodFriday,
EasterMonday,
EarlyMay,
Spring,
QueensDiamondJubilee,
BattleOfTheBoyne,
Summer,
SaintAndrews,
// Bank Holiday for Saint Andrews, not necessarily on the 30th of November
SaintAndrewsHoliday,
ChristmasEve,
Christmas,
// Bank Holiday for Christmas, not necessarily on the 25th of December
ChristmasHoliday,
BoxingDay,
// Bank Holiday for Boxing Day, not necessarily on the 26th of December
BoxingDayHoliday,
NewYearEve,
}

pub fn get_bank_holiday<P: AsRef<Path>>(
bank_holiday_path: P,
) -> Result<HashMap<BankHoliday, Vec<Date>>> {
let bank_holidays_file = File::open(bank_holiday_path)?;
let region: BankHolidayRegion = serde_json::from_reader(bank_holidays_file)?;
let mut day_per_bank_holiday: HashMap<BankHoliday, Vec<Date>> = HashMap::new();
for event in region.events {
day_per_bank_holiday
.entry(event.title)
.or_insert_with(Vec::new)
.push(event.date);
}
Ok(day_per_bank_holiday)
}

// Generate a list of all fixed dates between two dates.
// For example, let's say you want to generate all the Christmas dates between
// the 1st of January 2000 and the 31st December of 2020
// ```
// let validity_period = ValidityPeriod {
// start_date: NaiveDate::from_ymd(2000, 1, 1),
// end_date: NaiveDate::from_ymd(2020, 12, 31),
// };
// let dates = get_fixed_days(25, 12, &validity_period);
// for year in 2000..=2020 {
// let date = NaiveDate::from_ymd(year, 12, 25);
// assert!(dates.contains(&date));
// }
// ```
pub fn get_fixed_days(day: u32, month: u32, validity_period: &ValidityPeriod) -> Vec<Date> {
let start_year = if Date::from_ymd(validity_period.start_date.year(), month, day)
>= validity_period.start_date
{
validity_period.start_date.year()
} else {
validity_period.start_date.year() + 1
};
let end_year = if Date::from_ymd(validity_period.end_date.year(), month, day)
<= validity_period.end_date
{
validity_period.end_date.year()
} else {
validity_period.end_date.year() - 1
};
(start_year..=end_year)
.map(|year| Date::from_ymd(year, month, day))
.collect()
}

#[cfg(test)]
mod tests {
use super::*;

mod fixed_years {
use super::*;
use chrono::NaiveDate;

#[test]
fn included_limits() {
let validity_period = ValidityPeriod {
start_date: NaiveDate::from_ymd(2000, 12, 25),
end_date: NaiveDate::from_ymd(2002, 12, 25),
};
let dates = get_fixed_days(25, 12, &validity_period);
let date = NaiveDate::from_ymd(2000, 12, 25);
assert!(dates.contains(&date));
let date = NaiveDate::from_ymd(2001, 12, 25);
assert!(dates.contains(&date));
let date = NaiveDate::from_ymd(2002, 12, 25);
assert!(dates.contains(&date));
}

#[test]
fn excluded_limits() {
let validity_period = ValidityPeriod {
start_date: NaiveDate::from_ymd(2000, 12, 26),
end_date: NaiveDate::from_ymd(2002, 12, 24),
};
let dates = get_fixed_days(25, 12, &validity_period);
let date = NaiveDate::from_ymd(2001, 12, 25);
assert!(dates.contains(&date));
}
}
}
2 changes: 2 additions & 0 deletions src/transxchange/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@

//! Module to handle UK Theoric Data Model for Public Transportation called TransXChange

mod bank_holidays;
mod naptan;
mod operating_profile;
mod read;

pub use read::read;
Loading

0 comments on commit f89d728

Please sign in to comment.