Functional Weave
Code in Python

hospitality.room-rate@1.0.0

impl/rust.rs

5,564 bytes · the Rust implementation · view raw

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()))
}