Functional Weave
Code in Python

telecoms.call-rating@1.0.0

impl/rust.rs

4,591 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 std::collections::HashSet;

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::math_round_div::round_div;  ← from math.round-div ^1.0.0 · built alongside by fune
use super::money_amount::{assert_same_currency, money, money_from_value, money_to_value};  ← from money.amount ^1.0.0 · built alongside by fune

fn is_digits(s: &str) -> bool {
    !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit())
}

/// Rate one call: longest-prefix match on the dialled number, duration rounded
/// up to the rate's billing increments, per-minute charge rounded once by
/// `mode`, connection fee added and the minimum charge applied.
///
/// # Panics
/// Panics on a malformed number, a negative duration, a bad rate deck, no
/// matching prefix, or an unknown rounding mode.
pub fn rate_call(dialled_number: &str, duration_seconds: i64, rates: &[CallRate], mode: &str) -> RatedCall {
    let number = dialled_number.strip_prefix('+').unwrap_or(dialled_number);
    if !is_digits(number) {
        panic!(
            "dialled number must be international format digits, optionally with a leading +, received \"{}\"",
            dialled_number
        );
    }
    if duration_seconds < 0 {
        panic!("duration must be whole seconds of 0 or more, received {}", duration_seconds);
    }
    let mut seen: HashSet<&str> = HashSet::new();
    let mut best: Option<&CallRate> = None;
    for rate in rates {
        if !is_digits(&rate.prefix) {
            panic!("rate prefix must be digits, received \"{}\"", rate.prefix);
        }
        if !seen.insert(rate.prefix.as_str()) {
            panic!("duplicate prefix {} in the rate deck", rate.prefix);
        }
        if rate.first_increment_seconds < 1 || rate.next_increment_seconds < 1 {
            panic!(
                "billing increments must be 1 second or more, received {}/{} for prefix {}",
                rate.first_increment_seconds, rate.next_increment_seconds, rate.prefix
            );
        }
        assert_same_currency(&rates[0].per_minute, &rate.per_minute);
        assert_same_currency(&rates[0].per_minute, &rate.connection_fee);
        assert_same_currency(&rates[0].per_minute, &rate.minimum_charge);
        if number.starts_with(rate.prefix.as_str()) && best.map_or(true, |b| rate.prefix.len() > b.prefix.len()) {
            best = Some(rate);
        }
    }
    let best = match best {
        Some(rate) => rate,
        None => panic!("no rate for dialled number {}", number),
    };

    let currency = best.per_minute.currency.as_str();
    if duration_seconds == 0 {
        return RatedCall {
            prefix: best.prefix.clone(),
            destination: best.destination.clone(),
            billed_seconds: 0,
            usage_charge: money(0, currency),
            connection_fee: money(0, currency),
            total: money(0, currency),
        };
    }
    let first = best.first_increment_seconds;
    let step = best.next_increment_seconds;
    let billed_seconds = if duration_seconds <= first {
        first
    } else {
        first + ((duration_seconds - first + step - 1) / step) * step
    };
    let usage = round_div(best.per_minute.minor * billed_seconds, 60, mode);
    let total = (usage + best.connection_fee.minor).max(best.minimum_charge.minor);
    RatedCall {
        prefix: best.prefix.clone(),
        destination: best.destination.clone(),
        billed_seconds,
        usage_charge: money(usage, currency),
        connection_fee: best.connection_fee.clone(),
        total: money(total, currency),
    }
}

pub fn call_rate_from_value(v: &Value) -> CallRate {
    CallRate {
        prefix: v.get("prefix").as_str().to_string(),
        destination: v.get("destination").as_str().to_string(),
        per_minute: money_from_value(v.get("perMinute")),
        connection_fee: money_from_value(v.get("connectionFee")),
        minimum_charge: money_from_value(v.get("minimumCharge")),
        first_increment_seconds: v.get("firstIncrementSeconds").as_i64(),
        next_increment_seconds: v.get("nextIncrementSeconds").as_i64(),
    }
}

pub fn rated_call_to_value(c: &RatedCall) -> Value {
    Value::obj(vec![
        ("prefix", Value::str(&c.prefix)),
        ("destination", Value::str(&c.destination)),
        ("billedSeconds", Value::Int(c.billed_seconds)),
        ("usageCharge", money_to_value(&c.usage_charge)),
        ("connectionFee", money_to_value(&c.connection_fee)),
        ("total", money_to_value(&c.total)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let rates: Vec<CallRate> = args[2].as_arr().iter().map(call_rate_from_value).collect();
    rated_call_to_value(&rate_call(args[0].as_str(), args[1].as_i64(), &rates, args[3].as_str()))
}