Shared Rust kernels

ported/date_kernels

All modules · quoted at commit 9e1884369a67 of DnaCalc/DnaCalc. Comments are the source's own.

days_from_civil · 9 lines · calc-func/src/ported/date_kernels.rs on GitHub
fn days_from_civil(year: i64, month: i64, day: i64) -> i64 {
    let year = year - i64::from(month <= 2);
    let era = if year >= 0 { year } else { year - 399 } / 400;
    let yoe = year - era * 400;
    let mp = month + if month > 2 { -3 } else { 9 };
    let doy = (153 * mp + 2) / 5 + day - 1;
    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
    era * 146097 + doe - 719468
}

Used by 25 functions.

civil_from_days · 12 lines · calc-func/src/ported/date_kernels.rs on GitHub
fn civil_from_days(z: i64) -> (i64, i64, i64) {
    let z = z + 719468;
    let era = if z >= 0 { z } else { z - 146096 } / 146097;
    let doe = z - era * 146097;
    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
    let y = yoe + era * 400;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let d = doy - (153 * mp + 2) / 5 + 1;
    let m = mp + if mp < 10 { 3 } else { -9 };
    (y + i64::from(m <= 2), m, d)
}

Used by 23 functions.

serial_unbounded_1900 · 12 lines · calc-func/src/ported/date_kernels.rs on GitHub
/// The 1900 serial of a date, with Excel's 29 February 1900 and day 0 of January 1900, for any
/// day of the month (a day past the month's end runs into the next month).
pub(crate) fn serial_unbounded_1900(year: i64, month: i64, day: i64) -> i64 {
    let base = days_from_civil(1899, 12, 31);
    let month_start = days_from_civil(year, month, 1) - base;
    let month_start = if month_start >= 60 {
        month_start + 1
    } else {
        month_start
    };
    month_start + day - 1
}

Used by 17 functions.

ymd_1900 · 11 lines · calc-func/src/ported/date_kernels.rs on GitHub
/// The year, month and day of a whole 1900 serial (0 is 0 January 1900, 60 is 29 February 1900).
pub(crate) fn ymd_1900(serial: i64) -> (i64, i64, i64) {
    if serial == 0 {
        return (1900, 1, 0);
    }
    if serial == 60 {
        return (1900, 2, 29);
    }
    let adjusted = if serial > 60 { serial - 1 } else { serial };
    civil_from_days(days_from_civil(1899, 12, 31) + adjusted)
}

Used by 23 functions.

is_leap · 3 lines · calc-func/src/ported/date_kernels.rs on GitHub
pub(crate) fn is_leap(year: i64) -> bool {
    (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}

Used by 19 functions.

days_in_month · 14 lines · calc-func/src/ported/date_kernels.rs on GitHub
pub(crate) fn days_in_month(year: i64, month: i64) -> i64 {
    match month {
        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
        4 | 6 | 9 | 11 => 30,
        2 => {
            if is_leap(year) {
                29
            } else {
                28
            }
        }
        _ => 30,
    }
}

Used by 19 functions.

serial_to_ymd · 11 lines · calc-func/src/ported/date_kernels.rs on GitHub
/// The year, month and day of a 1900 serial, rounded to the nearest second first.
pub(crate) fn serial_to_ymd(serial: f64) -> Result<(i64, i64, i64), W> {
    if !serial.is_finite() || serial < 0.0 {
        return Err(W::Num);
    }
    let rounded = serial + 0.5 / 86_400.0;
    if rounded >= (MAX_SERIAL_1900 + 1) as f64 {
        return Err(W::Num);
    }
    Ok(ymd_1900(rounded.floor() as i64))
}

Used by 3 functions.

hms · 13 lines · calc-func/src/ported/date_kernels.rs on GitHub
pub(crate) fn hms(serial: f64) -> Result<(f64, f64, f64), W> {
    let rounded = serial + 0.5 / 86_400.0;
    if !serial.is_finite() || serial < 0.0 || rounded >= (MAX_SERIAL_1900 + 1) as f64 {
        return Err(W::Num);
    }
    let fractional = rounded - rounded.floor();
    let hours = fractional * 24.0;
    let hour = hours.floor();
    let minutes = (hours - hour) * 60.0;
    let minute = minutes.floor();
    let second = ((minutes - minute) * 60.0).floor().rem_euclid(60.0);
    Ok((hour, minute, second))
}

Used by 3 functions.

days · 7 lines · calc-func/src/ported/date_kernels.rs on GitHub
pub(crate) fn days(end_serial: f64, start_serial: f64) -> Result<f64, W> {
    let range = 0.0..(MAX_SERIAL_1900 + 1) as f64;
    if !range.contains(&end_serial) || !range.contains(&start_serial) {
        return Err(W::Num);
    }
    Ok(end_serial.trunc() - start_serial.trunc())
}

Used by 5 functions.

whole_serial · 6 lines · calc-func/src/ported/date_kernels.rs on GitHub
fn whole_serial(n: f64) -> Result<i64, W> {
    if !(0.0..(MAX_SERIAL_1900 + 1) as f64).contains(&n) {
        return Err(W::Num);
    }
    Ok(n.trunc() as i64)
}

Used by 23 functions.

whole · 6 lines · calc-func/src/ported/date_kernels.rs on GitHub
fn whole(n: f64) -> Result<i64, W> {
    if !n.is_finite() {
        return Err(W::Num);
    }
    Ok(n.trunc() as i64)
}

Used by 24 functions.

shifted_month · 8 lines · calc-func/src/ported/date_kernels.rs on GitHub
fn shifted_month(year: i64, month: i64, offset: i64) -> Result<(i64, i64), W> {
    let month_index = year
        .checked_mul(12)
        .and_then(|v| v.checked_add(month - 1))
        .and_then(|v| v.checked_add(offset))
        .ok_or(W::Num)?;
    Ok((month_index.div_euclid(12), month_index.rem_euclid(12) + 1))
}

Used by 2 functions.

weekday_sunday_one_based · 3 lines · calc-func/src/ported/date_kernels.rs on GitHub
fn weekday_sunday_one_based(serial: i64) -> i64 {
    (serial - 1).rem_euclid(7) + 1
}

Used by 25 functions.

remap_weekday · 12 lines · calc-func/src/ported/date_kernels.rs on GitHub
fn remap_weekday(sunday_one_based: i64, return_type: i64) -> Option<f64> {
    match return_type {
        1 => Some(sunday_one_based as f64),
        2 | 11 => Some(((sunday_one_based + 5) % 7 + 1) as f64),
        3 => Some(((sunday_one_based + 5) % 7) as f64),
        12..=17 => {
            let start_day = (return_type - 10).rem_euclid(7) + 1;
            Some(((sunday_one_based - start_day).rem_euclid(7) + 1) as f64)
        }
        _ => None,
    }
}

Used by 21 functions.

weekday_of · 3 lines · calc-func/src/ported/date_kernels.rs on GitHub
fn weekday_of(serial: i64, return_type: i64) -> Result<f64, W> {
    remap_weekday(weekday_sunday_one_based(serial), return_type).ok_or(W::Num)
}

Used by 21 functions.

weekday · 8 lines · calc-func/src/ported/date_kernels.rs on GitHub
pub(crate) fn weekday(serial: f64, return_type: Option<f64>) -> Result<f64, W> {
    if serial < 0.0 {
        return Err(W::Num);
    }
    let serial = whole_serial(serial + 0.5 / 86_400.0)?;
    let return_type = whole(date_integer_floor(return_type.unwrap_or(1.0)))?;
    weekday_of(serial, return_type)
}

Used by 20 functions.

iso_weeknum_serial · 10 lines · calc-func/src/ported/date_kernels.rs on GitHub
fn iso_weeknum_serial(serial: i64) -> Result<f64, W> {
    let weekday_monday = weekday_of(serial, 2)? as i64;
    let thursday = serial + (4 - weekday_monday);
    let (iso_year, _, _) =
        ymd_from_excel_serial(DateSystem::System1900, thursday as f64).ok_or(W::Num)?;
    let jan4 = serial_unbounded_1900(iso_year, 1, 4);
    let jan4_weekday = weekday_of(jan4, 2)? as i64;
    let first_monday = jan4 - (jan4_weekday - 1);
    Ok(((serial - first_monday).div_euclid(7) + 1) as f64)
}

Used by 2 functions.

ymd_of · 10 lines · calc-func/src/ported/date_kernels.rs on GitHub
fn ymd_of(serial: f64) -> Result<(i64, i64, i64), W> {
    if !serial.is_finite() {
        return Err(W::Num);
    }
    let whole = serial.trunc() as i64;
    if whole < 0 {
        return Err(W::Num);
    }
    Ok(ymd_1900(whole))
}

Used by 2 functions.

WeekendMask::saturday_sunday · 5 lines · calc-func/src/ported/date_kernels.rs on GitHub
pub(crate) const fn saturday_sunday() -> Self {
    Self {
        monday_first: [false, false, false, false, false, true, true],
    }
}

Used by 4 functions.

WeekendMask::from_number · 20 lines · calc-func/src/ported/date_kernels.rs on GitHub
pub(crate) fn from_number(code: i64) -> Option<Self> {
    let monday_first = match code {
        1 => [false, false, false, false, false, true, true],
        2 => [true, false, false, false, false, false, true],
        3 => [true, true, false, false, false, false, false],
        4 => [false, true, true, false, false, false, false],
        5 => [false, false, true, true, false, false, false],
        6 => [false, false, false, true, true, false, false],
        7 => [false, false, false, false, true, true, false],
        11 => [false, false, false, false, false, false, true],
        12 => [true, false, false, false, false, false, false],
        13 => [false, true, false, false, false, false, false],
        14 => [false, false, true, false, false, false, false],
        15 => [false, false, false, true, false, false, false],
        16 => [false, false, false, false, true, false, false],
        17 => [false, false, false, false, false, true, false],
        _ => return None,
    };
    Some(Self { monday_first })
}

Used by 4 functions.

WeekendMask::from_mask_text · 11 lines · calc-func/src/ported/date_kernels.rs on GitHub
/// A mask of seven 0 and 1 characters, Monday first.
pub(crate) fn from_mask_text(text: &str) -> Option<Self> {
    if text.len() != 7 || !text.chars().all(|ch| ch == '0' || ch == '1') {
        return None;
    }
    let mut monday_first = [false; 7];
    for (idx, ch) in text.chars().enumerate() {
        monday_first[idx] = ch == '1';
    }
    Some(Self { monday_first })
}

Used by 4 functions.

WeekendMask::is_all_days · 3 lines · calc-func/src/ported/date_kernels.rs on GitHub
pub(crate) fn is_all_days(&self) -> bool {
    self.monday_first.iter().all(|bit| *bit)
}

Used by 4 functions.

WeekendMask::contains_serial · 4 lines · calc-func/src/ported/date_kernels.rs on GitHub
fn contains_serial(&self, serial: i64) -> bool {
    let monday_idx = ((weekday_sunday_one_based(serial) + 5).rem_euclid(7)) as usize;
    self.monday_first[monday_idx]
}

Used by 4 functions.

serial_from_number · 10 lines · calc-func/src/ported/date_kernels.rs on GitHub
pub(crate) fn serial_from_number(value: f64) -> Result<i64, W> {
    if !value.is_finite() {
        return Err(W::Num);
    }
    let serial = value.trunc() as i64;
    if !(0..=MAX_SERIAL_1900).contains(&serial) {
        return Err(W::Num);
    }
    Ok(serial)
}

Used by 4 functions.

is_business_day · 3 lines · calc-func/src/ported/date_kernels.rs on GitHub
fn is_business_day(serial: i64, weekend: WeekendMask, holidays: &BTreeSet<i64>) -> bool {
    !weekend.contains_serial(serial) && !holidays.contains(&serial)
}

Used by 4 functions.

workday · 30 lines · calc-func/src/ported/date_kernels.rs on GitHub
pub(crate) fn workday(
    start_serial: f64,
    days: f64,
    weekend: WeekendMask,
    holidays: &BTreeSet<i64>,
) -> Result<f64, W> {
    if weekend.is_all_days() {
        return Err(W::Value);
    }
    let start = serial_from_number(start_serial)?;
    if !days.is_finite() {
        return Err(W::Num);
    }
    let mut remaining = days.floor() as i64;
    if remaining == 0 {
        return Ok(start as f64);
    }
    let step = if remaining > 0 { 1 } else { -1 };
    let mut current = start;
    while remaining != 0 {
        current += step;
        if !(0..=MAX_SERIAL_1900).contains(&current) {
            return Err(W::Num);
        }
        if is_business_day(current, weekend, holidays) {
            remaining -= step;
        }
    }
    Ok(current as f64)
}

Used by 4 functions.

networkdays · 34 lines · calc-func/src/ported/date_kernels.rs on GitHub
pub(crate) fn networkdays(
    start_serial: f64,
    end_serial: f64,
    weekend: WeekendMask,
    holidays: &BTreeSet<i64>,
) -> Result<f64, W> {
    let start = serial_from_number(start_serial)?;
    let end = serial_from_number(end_serial)?;
    if weekend.is_all_days() {
        return Ok(0.0);
    }
    let (lo, hi, sign) = if start <= end {
        (start, end, 1.0)
    } else {
        (end, start, -1.0)
    };
    // Whole weeks hold the same business days, so only the remainder is walked.
    let per_week = (0..7)
        .filter(|&offset| !weekend.contains_serial(lo + offset))
        .count() as i64;
    let span = hi - lo + 1;
    let weeks = span / 7;
    let mut count = weeks * per_week;
    for serial in lo + weeks * 7..=hi {
        if !weekend.contains_serial(serial) {
            count += 1;
        }
    }
    count -= holidays
        .range(lo..=hi)
        .filter(|&&serial| !weekend.contains_serial(serial))
        .count() as i64;
    Ok(sign * count as f64)
}

Used by 2 functions.