cellrune 0.1.18

Bounded XLSX/XLSM reading, deterministic calculation, editing, and writing for Rust
Documentation
use super::super::ast::Expr;
use super::super::coerce::to_logical;
use super::super::eval::{Engine, EvalContext};
use super::super::value::{ErrorKind, Value};
use super::calendar::{
    Date, date_from_serial, days_from_civil, is_leap_year, serial_from_date, weekday_monday_zero,
};
use super::date::{days_360_european, days_360_us};
use super::kernel::DateAdditionalFunction;
use super::util::required_number;

pub(super) fn call(
    engine: &Engine<'_>,
    context: EvalContext<'_>,
    function: DateAdditionalFunction,
    args: &[Expr],
) -> Value {
    match function {
        DateAdditionalFunction::Days => days(engine, context, args),
        DateAdditionalFunction::Days360 => days360(engine, context, args),
        DateAdditionalFunction::Hour => time_part(engine, context, args, TimePart::Hour),
        DateAdditionalFunction::Minute => time_part(engine, context, args, TimePart::Minute),
        DateAdditionalFunction::Second => time_part(engine, context, args, TimePart::Second),
        DateAdditionalFunction::Time => time(engine, context, args),
        DateAdditionalFunction::IsoWeekNum => iso_week_num(engine, context, args),
        DateAdditionalFunction::WeekNum => week_num(engine, context, args),
    }
}

fn days(engine: &Engine<'_>, context: EvalContext<'_>, args: &[Expr]) -> Value {
    if args.len() != 2 {
        return Value::Error(ErrorKind::Value);
    }
    let end = match required_number(engine, context, &args[0]) {
        Ok(number) => number.trunc(),
        Err(kind) => return Value::Error(kind),
    };
    let start = match required_number(engine, context, &args[1]) {
        Ok(number) => number.trunc(),
        Err(kind) => return Value::Error(kind),
    };
    if date_from_serial(end, engine.date_system()).is_none()
        || date_from_serial(start, engine.date_system()).is_none()
    {
        Value::Error(ErrorKind::Num)
    } else {
        Value::Number(end - start)
    }
}

fn days360(engine: &Engine<'_>, context: EvalContext<'_>, args: &[Expr]) -> Value {
    if args.len() < 2 || args.len() > 3 {
        return Value::Error(ErrorKind::Value);
    }
    let start_serial = match required_number(engine, context, &args[0]) {
        Ok(number) => number.trunc(),
        Err(kind) => return Value::Error(kind),
    };
    let end_serial = match required_number(engine, context, &args[1]) {
        Ok(number) => number.trunc(),
        Err(kind) => return Value::Error(kind),
    };
    let european = match args.get(2) {
        Some(expr) => match to_logical(&engine.eval_scalar(context, expr)) {
            Ok(value) => value,
            Err(kind) => return Value::Error(kind),
        },
        None => false,
    };
    let Some(start) = date_from_serial(start_serial, engine.date_system()) else {
        return Value::Error(ErrorKind::Num);
    };
    let Some(end) = date_from_serial(end_serial, engine.date_system()) else {
        return Value::Error(ErrorKind::Num);
    };
    Value::Number(if european {
        days_360_european(start, end)
    } else {
        days_360_us(start, end)
    })
}

#[derive(Debug, Clone, Copy)]
enum TimePart {
    Hour,
    Minute,
    Second,
}

fn time_part(
    engine: &Engine<'_>,
    context: EvalContext<'_>,
    args: &[Expr],
    part: TimePart,
) -> Value {
    if args.len() != 1 {
        return Value::Error(ErrorKind::Value);
    }
    let serial = match required_number(engine, context, &args[0]) {
        Ok(number) if number >= 0.0 => number,
        Ok(_) => return Value::Error(ErrorKind::Num),
        Err(kind) => return Value::Error(kind),
    };
    let seconds = ((serial.fract() * 86_400.0).round() as i64).rem_euclid(86_400);
    let value = match part {
        TimePart::Hour => seconds / 3_600,
        TimePart::Minute => (seconds / 60) % 60,
        TimePart::Second => seconds % 60,
    };
    Value::Number(value as f64)
}

fn time(engine: &Engine<'_>, context: EvalContext<'_>, args: &[Expr]) -> Value {
    if args.len() != 3 {
        return Value::Error(ErrorKind::Value);
    }
    let mut components = [0_i64; 3];
    for (target, expr) in components.iter_mut().zip(args) {
        *target = match required_number(engine, context, expr) {
            Ok(number) if (0.0..=32_767.0).contains(&number) => number.trunc() as i64,
            Ok(_) => return Value::Error(ErrorKind::Num),
            Err(kind) => return Value::Error(kind),
        };
    }
    let Some(seconds) = components[0]
        .checked_mul(3_600)
        .and_then(|value| value.checked_add(components[1] * 60))
        .and_then(|value| value.checked_add(components[2]))
    else {
        return Value::Error(ErrorKind::Num);
    };
    Value::Number(seconds.rem_euclid(86_400) as f64 / 86_400.0)
}

fn iso_week_num(engine: &Engine<'_>, context: EvalContext<'_>, args: &[Expr]) -> Value {
    if args.len() != 1 {
        return Value::Error(ErrorKind::Value);
    }
    let serial = match required_number(engine, context, &args[0]) {
        Ok(number) => number.trunc(),
        Err(kind) => return Value::Error(kind),
    };
    let Some(date) = date_from_serial(serial, engine.date_system()) else {
        return Value::Error(ErrorKind::Num);
    };
    let Some(weekday) = weekday_monday_zero(serial, engine.date_system()) else {
        return Value::Error(ErrorKind::Num);
    };
    let ordinal =
        days_from_civil(date.year, date.month, date.day) - days_from_civil(date.year, 1, 1) + 1;
    let mut week = (ordinal - i64::from(weekday) + 10).div_euclid(7);
    if week < 1 {
        week = i64::from(iso_weeks_in_year(date.year - 1));
    } else if week > i64::from(iso_weeks_in_year(date.year)) {
        week = 1;
    }
    Value::Number(week as f64)
}

fn iso_weeks_in_year(year: i32) -> i32 {
    let january_first = (days_from_civil(year, 1, 1) + 3).rem_euclid(7) as i32;
    if january_first == 3 || (january_first == 2 && is_leap_year(year)) {
        53
    } else {
        52
    }
}

fn week_num(engine: &Engine<'_>, context: EvalContext<'_>, args: &[Expr]) -> Value {
    if args.is_empty() || args.len() > 2 {
        return Value::Error(ErrorKind::Value);
    }
    let serial = match required_number(engine, context, &args[0]) {
        Ok(number) => number.trunc(),
        Err(kind) => return Value::Error(kind),
    };
    if date_from_serial(serial, engine.date_system()).is_none() {
        return Value::Error(ErrorKind::Num);
    }
    let return_type = match args.get(1) {
        Some(expr) => match required_number(engine, context, expr) {
            Ok(number) => number.trunc() as i32,
            Err(kind) => return Value::Error(kind),
        },
        None => 1,
    };
    if return_type == 21 {
        return iso_week_num(engine, context, &args[..1]);
    }
    let week_start = match return_type {
        1 | 17 => 6,
        2 | 11 => 0,
        12..=16 => return_type - 11,
        _ => return Value::Error(ErrorKind::Num),
    };
    let Some(date) = date_from_serial(serial, engine.date_system()) else {
        return Value::Error(ErrorKind::Num);
    };
    let Some(year_start) = serial_from_date(
        Date {
            year: date.year,
            month: 1,
            day: 1,
        },
        engine.date_system(),
    ) else {
        return Value::Error(ErrorKind::Num);
    };
    let Some(january_weekday) = weekday_monday_zero(year_start, engine.date_system()) else {
        return Value::Error(ErrorKind::Num);
    };
    let offset = (january_weekday - week_start).rem_euclid(7);
    Value::Number(((serial - year_start + f64::from(offset)) / 7.0).floor() + 1.0)
}