use super::funejson::Value; use super::dates_add_days::add_days; use super::dates_day_of_week::day_of_week; use super::money_add::subtract_money; use super::money_amount::{money_from_value, money_to_value, Money}; use super::money_apply_rate::apply_rate; use super::money_sum::sum_money; /// Price a stay night by night from a rate table, then take off the /// length-of-stay discount the stay qualifies for. /// /// A night is named by the date it starts, so a Friday night is the night of /// a Friday. Exactly one row must match each night: two matching rows are /// an error rather than a guess about which one the hotel meant. /// /// # Panics /// Panics on a malformed table or discount list, a bad date, a night no row /// or several rows match, or mixed currencies. pub fn room_rate(rates: &[NightlyRate], stay_discounts: &[StayDiscount], arrival: &str, nights: i64) -> RoomRateQuote { if nights < 1 { panic!("nights must be at least 1, received {}", nights); } if rates.is_empty() { panic!("the rate table is empty"); } for row in rates { // add_days parses both ends strictly, so a malformed date fails here. add_days(&row.first_night, 0); add_days(&row.last_night, 0); if row.last_night < row.first_night { panic!("rate \"{}\" ends before it starts", row.season); } for d in &row.days_of_week { if *d < 1 || *d > 7 { panic!( "daysOfWeek must be 1 (Monday) to 7 (Sunday), received {} in rate \"{}\"", d, row.season ); } } } let mut seen: Vec = Vec::new(); for discount in stay_discounts { if discount.min_nights < 1 { panic!("minNights must be at least 1, received {}", discount.min_nights); } if seen.contains(&discount.min_nights) { panic!("two stay discounts start at {} nights", discount.min_nights); } seen.push(discount.min_nights); if !(0..=10000).contains(&discount.basis_points) { panic!( "basisPoints must be a whole number from 0 to 10000, received {}", discount.basis_points ); } } let mut stay: Vec = Vec::new(); for i in 0..nights { let date = add_days(arrival, i); let weekday = day_of_week(&date); let matches: Vec<&NightlyRate> = rates .iter() .filter(|row| { row.first_night.as_str() <= date.as_str() && date.as_str() <= row.last_night.as_str() && (row.days_of_week.is_empty() || row.days_of_week.contains(&weekday)) }) .collect(); if matches.is_empty() { panic!("no rate for the night of {}", date); } if matches.len() > 1 { panic!( "more than one rate for the night of {}: \"{}\" and \"{}\"", date, matches[0].season, matches[1].season ); } stay.push(RoomNight { season: matches[0].season.clone(), rate: matches[0].rate.clone(), date }); } let amounts: Vec = stay.iter().map(|n| n.rate.clone()).collect(); let subtotal = sum_money(&amounts, &stay[0].rate.currency); let mut discount_basis_points = 0; let mut best = 0; for discount in stay_discounts { if discount.min_nights <= nights && discount.min_nights > best { best = discount.min_nights; discount_basis_points = discount.basis_points; } } let discount = apply_rate(&subtotal, discount_basis_points, "half-up"); RoomRateQuote { nights: stay, total: subtract_money(&subtotal, &discount), subtotal, discount_basis_points, discount, } } pub fn nightly_rate_from_value(v: &Value) -> NightlyRate { NightlyRate { season: v.get("season").as_str().to_string(), first_night: v.get("firstNight").as_str().to_string(), last_night: v.get("lastNight").as_str().to_string(), days_of_week: v.get("daysOfWeek").as_arr().iter().map(|d| d.as_i64()).collect(), rate: money_from_value(v.get("rate")), } } pub fn stay_discount_from_value(v: &Value) -> StayDiscount { StayDiscount { min_nights: v.get("minNights").as_i64(), basis_points: v.get("basisPoints").as_i64() } } pub fn room_rate_quote_to_value(q: &RoomRateQuote) -> Value { Value::obj(vec![ ( "nights", Value::Arr( q.nights .iter() .map(|n| { Value::obj(vec![ ("date", Value::str(&n.date)), ("season", Value::str(&n.season)), ("rate", money_to_value(&n.rate)), ]) }) .collect(), ), ), ("subtotal", money_to_value(&q.subtotal)), ("discountBasisPoints", Value::Int(q.discount_basis_points)), ("discount", money_to_value(&q.discount)), ("total", money_to_value(&q.total)), ]) } pub fn fune_vector(args: &[Value]) -> Value { let rates: Vec = args[0].as_arr().iter().map(nightly_rate_from_value).collect(); let discounts: Vec = args[1].as_arr().iter().map(stay_discount_from_value).collect(); room_rate_quote_to_value(&room_rate(&rates, &discounts, args[2].as_str(), args[3].as_i64())) }