Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::dates_add_days::add_days; ← from dates.add-days ^1.0.0 · built alongside by fune
use super::dates_day_of_week::day_of_week; ← from dates.day-of-week ^1.0.0 · built alongside by fune
use super::money_add::subtract_money; ← from money.add ^1.0.0 · built alongside by fune
use super::money_amount::{money_from_value, money_to_value, Money}; ← from money.amount ^1.0.0 · built alongside by fune
use super::money_apply_rate::apply_rate; ← from money.apply-rate ^1.0.0 · built alongside by fune
use super::money_sum::sum_money; ← from money.sum ^1.0.0 · built alongside by fune
/// 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<i64> = 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<RoomNight> = 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<Money> = 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<NightlyRate> = args[0].as_arr().iter().map(nightly_rate_from_value).collect();
let discounts: Vec<StayDiscount> = 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()))
}