Functional Weave
Code in Rust

telecoms.roaming-charge@1.0.1

impl/rust.rs

4,576 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::epoch_day_from_iso;  ← from dates.add-days ^1.0.0 · built alongside by fune
use super::money_amount::{money, money_to_value};  ← from money.amount ^1.0.0 · built alongside by fune
use super::money_sum::sum_money;  ← from money.sum ^1.0.0 · built alongside by fune
use super::telecoms_roaming_charge_data::{RoamingRate, ROAMING_TARIFFS, ROAMING_TARIFFS_HISTORY};  ← this capability’s own data, compiled from data/roaming-tariffs.json into the same file by fune build

fn is_country(s: &str) -> bool {
    s.len() == 2 && s.bytes().all(|b| b.is_ascii_uppercase())
}

/// Price roaming usage in one country on one date: find the tariff's zone for
/// the country in force on that date, then multiply each kind of usage by its
/// price. Exact integer arithmetic throughout.
///
/// # Panics
/// Panics on a malformed country or date, negative usage, or when no zone of
/// the tariff covers the country on that date.
pub fn roaming_charge(tariff: &str, country: &str, on_date: &str, usage: &RoamingUsage) -> RoamingCharge {
    if !is_country(country) {
        panic!(
            "country must be an ISO 3166-1 alpha-2 code in capitals, received \"{}\"",
            country
        );
    }
    epoch_day_from_iso(on_date);
    let counts = [usage.days, usage.data_mb, usage.call_out_minutes, usage.call_in_minutes, usage.sms_out];
    if counts.iter().any(|&n| n < 0) {
        panic!("usage must be whole numbers of 0 or more");
    }

    let covers = |rule: &RoamingRate| rule.tariff == tariff && rule.countries.split(' ').any(|c| c == country);
    let mut best: Option<&RoamingRate> = None;
    for rule in ROAMING_TARIFFS {
        if !covers(rule) || on_date < rule.valid_from {
            continue;
        }
        if let Some(valid_to) = rule.valid_to {
            if on_date > valid_to {
                continue;
            }
        }
        if best.map_or(true, |b| rule.valid_from > b.valid_from) {
            best = Some(rule);
        }
    }

    let best = match best {
        Some(rule) => rule,
        None => {
            // A history=current build has dropped the rows that ended;
            // answering an older date with today's prices would be wrong, so
            // say what happened.
            if ROAMING_TARIFFS_HISTORY != "full" {
                let mut earliest: Option<&'static str> = None;
                for rule in ROAMING_TARIFFS {
                    if covers(rule) && earliest.map_or(true, |e| rule.valid_from < e) {
                        earliest = Some(rule.valid_from);
                    }
                }
                if let Some(earliest) = earliest {
                    if on_date < earliest {
                        panic!(
                            "no roaming zone for {} in tariff {} on {}: this build was installed with history={}, so it only carries rules from {}. Reinstall with history=full to price older usage.",
                            country, tariff, on_date, ROAMING_TARIFFS_HISTORY, earliest
                        );
                    }
                }
            }
            panic!("no roaming zone for {} in tariff {} on {}", country, tariff, on_date)
        }
    };

    let c = best.currency;
    let daily_fees = money(usage.days * best.daily_fee_minor, c);
    let data = money(usage.data_mb * best.data_per_mb_minor, c);
    let calls = money(
        usage.call_out_minutes * best.call_out_per_minute_minor + usage.call_in_minutes * best.call_in_per_minute_minor,
        c,
    );
    let sms = money(usage.sms_out * best.sms_out_minor, c);
    let total = sum_money(&[daily_fees.clone(), data.clone(), calls.clone(), sms.clone()], c);
    RoamingCharge {
        tariff: tariff.to_string(),
        zone: best.zone.to_string(),
        daily_fees,
        data,
        calls,
        sms,
        total,
    }
}

pub fn roaming_usage_from_value(v: &Value) -> RoamingUsage {
    RoamingUsage {
        days: v.get("days").as_i64(),
        data_mb: v.get("dataMb").as_i64(),
        call_out_minutes: v.get("callOutMinutes").as_i64(),
        call_in_minutes: v.get("callInMinutes").as_i64(),
        sms_out: v.get("smsOut").as_i64(),
    }
}

pub fn roaming_charge_to_value(r: &RoamingCharge) -> Value {
    Value::obj(vec![
        ("tariff", Value::str(&r.tariff)),
        ("zone", Value::str(&r.zone)),
        ("dailyFees", money_to_value(&r.daily_fees)),
        ("data", money_to_value(&r.data)),
        ("calls", money_to_value(&r.calls)),
        ("sms", money_to_value(&r.sms)),
        ("total", money_to_value(&r.total)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    roaming_charge_to_value(&roaming_charge(
        args[0].as_str(),
        args[1].as_str(),
        args[2].as_str(),
        &roaming_usage_from_value(&args[3]),
    ))
}