Shared Rust kernels

ported/coupon_family

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

max_excel_serial · 3 lines · calc-func/src/ported/coupon_family.rs on GitHub
fn max_excel_serial() -> i64 {
    excel_serial_from_ymd(WorkbookDateSystem::System1900, 9999, 12, 31).unwrap() as i64
}

Used by 6 functions.

days_in_month · 11 lines · calc-func/src/ported/coupon_family.rs on GitHub
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 => {
            let leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
            if leap { 29 } else { 28 }
        }
        _ => 30,
    }
}

Used by 6 functions.

days_from_civil · 9 lines · calc-func/src/ported/coupon_family.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 6 functions.

excel_serial_from_ymd_unbounded_1900 · 8 lines · calc-func/src/ported/coupon_family.rs on GitHub
fn excel_serial_from_ymd_unbounded_1900(year: i64, month: i64, day: i64) -> i64 {
    if year == 1900 && month == 2 && day == 29 {
        return 60;
    }
    let base = days_from_civil(1899, 12, 31);
    let days = days_from_civil(year, month, 1) - base + (day - 1);
    if days >= 60 { days + 1 } else { days }
}

Used by 6 functions.

calendar_days360 · 45 lines · calc-func/src/ported/coupon_family.rs on GitHub
fn calendar_days360(
    start: i64,
    end: i64,
    adjustment: CalendarDayAdjustment,
) -> Result<f64, WorksheetErrorCode> {
    let (sy, sm, mut sd) = ymd_from_excel_serial(WorkbookDateSystem::System1900, start as f64)
        .ok_or(WorksheetErrorCode::Value)?;
    let (mut ey, mut em, mut ed) = ymd_from_excel_serial(WorkbookDateSystem::System1900, end as f64)
        .ok_or(WorksheetErrorCode::Value)?;
    if matches!(adjustment, CalendarDayAdjustment::CouponAccrued) {
        let start_last_feb = sm == 2 && sd == days_in_month(sy, sm);
        let end_last_feb = em == 2 && ed == days_in_month(ey, em);
        // Coupon accrual adjusts a day-31 end using the original start day, before
        // normalizing a February-end start. The shared DAYS360 convention differs.
        // Fn7 paired COUPDAYBS/COUPDAYSNC controls cover real and fictitious February ends.
        if ed == 31 {
            if sd < 30 {
                (ey, em) = if em == 12 { (ey + 1, 1) } else { (ey, em + 1) };
                ed = 1;
            } else {
                ed = 30;
            }
        }
        if sd == 31 || start_last_feb {
            sd = 30;
        }
        if start_last_feb && end_last_feb {
            ed = 30;
        }
        return Ok(((ey - sy) * 360 + (em - sm) * 30 + (ed - sd)) as f64);
    }
    if sd == 31
        || (matches!(adjustment, CalendarDayAdjustment::CouponPeriod)
            && sm == 2 && sd == days_in_month(sy, sm))
    {
        sd = 30;
    }
    if ed == 31
        || (matches!(adjustment, CalendarDayAdjustment::CouponPeriod)
            && em == 2 && ed == days_in_month(ey, em))
    {
        ed = 30;
    }
    Ok(((ey - sy) * 360 + (em - sm) * 30 + (ed - sd)) as f64)
}

Used by 2 functions.

actual_days · 3 lines · calc-func/src/ported/coupon_family.rs on GitHub
fn actual_days(start: i64, end: i64) -> f64 {
    (end - start) as f64
}

Used by 3 functions.

accrued_or_remaining_days · 17 lines · calc-func/src/ported/coupon_family.rs on GitHub
fn accrued_or_remaining_days(
    start: i64,
    end: i64,
    basis: CouponBasis,
) -> Result<f64, WorksheetErrorCode> {
    match basis {
        CouponBasis::Us30_360 => {
            calendar_days360(start, end, CalendarDayAdjustment::CouponAccrued)
        }
        CouponBasis::ActualActual | CouponBasis::Actual360 | CouponBasis::Actual365 => {
            Ok(actual_days(start, end))
        }
        CouponBasis::European30_360 => {
            calendar_days360(start, end, CalendarDayAdjustment::European)
        }
    }
}

Used by 2 functions.

parse_basis · 13 lines · calc-func/src/ported/coupon_family.rs on GitHub
fn parse_basis(value: f64) -> Result<CouponBasis, WorksheetErrorCode> {
    if !value.is_finite() {
        return Err(WorksheetErrorCode::Num);
    }
    match value.trunc() as i64 {
        0 => Ok(CouponBasis::Us30_360),
        1 => Ok(CouponBasis::ActualActual),
        2 => Ok(CouponBasis::Actual360),
        3 => Ok(CouponBasis::Actual365),
        4 => Ok(CouponBasis::European30_360),
        _ => Err(WorksheetErrorCode::Num),
    }
}

Used by 6 functions.

parse_frequency · 9 lines · calc-func/src/ported/coupon_family.rs on GitHub
fn parse_frequency(value: f64) -> Result<i64, WorksheetErrorCode> {
    if !value.is_finite() {
        return Err(WorksheetErrorCode::Num);
    }
    match value.trunc() as i64 {
        1 | 2 | 4 => Ok(value.trunc() as i64),
        _ => Err(WorksheetErrorCode::Num),
    }
}

Used by 6 functions.

check_coupon_dates · 10 lines · calc-func/src/ported/coupon_family.rs on GitHub
/// The check of a COUP function's dates on their own: each a date, and settlement before
/// maturity; #NUM! otherwise.
pub(crate) fn check_coupon_dates(settlement: f64, maturity: f64) -> Result<(), WorksheetErrorCode> {
    let settlement = parse_coupon_date(settlement)?;
    let maturity = parse_coupon_date(maturity)?;
    if settlement >= maturity {
        return Err(WorksheetErrorCode::Num);
    }
    Ok(())
}

Used by 6 functions.

parse_coupon_date · 12 lines · calc-func/src/ported/coupon_family.rs on GitHub
fn parse_coupon_date(value: f64) -> Result<i64, WorksheetErrorCode> {
    if !value.is_finite() {
        return Err(WorksheetErrorCode::Num);
    }
    let serial = value.trunc() as i64;
    if serial < 0 || serial > max_excel_serial() {
        return Err(WorksheetErrorCode::Num);
    }
    ymd_from_excel_serial(WorkbookDateSystem::System1900, serial as f64)
        .ok_or(WorksheetErrorCode::Num)?;
    Ok(serial)
}

Used by 6 functions.

parse_coupon_context · 20 lines · calc-func/src/ported/coupon_family.rs on GitHub
fn parse_coupon_context(
    settlement: f64,
    maturity: f64,
    frequency: f64,
    basis: Option<f64>,
) -> Result<CouponContext, WorksheetErrorCode> {
    let settlement = parse_coupon_date(settlement)?;
    let maturity = parse_coupon_date(maturity)?;
    let frequency = parse_frequency(frequency)?;
    let basis = parse_basis(basis.unwrap_or(0.0))?;
    if settlement >= maturity {
        return Err(WorksheetErrorCode::Num);
    }
    Ok(CouponContext {
        settlement,
        maturity,
        frequency,
        basis,
    })
}

Used by 6 functions.

coupon_date_before_maturity · 29 lines · calc-func/src/ported/coupon_family.rs on GitHub
/// Coupon date `k` periods before maturity, computed from maturity itself rather than by
/// chaining one period at a time: a maturity on the 28th-30th keeps that day of month in every
/// month long enough for it (clamped only in shorter months), and only a month-end maturity
/// gives month-end coupons. Chaining off an already-clamped date drifted to month ends after
/// the first short month (maturity 2046-08-28 quarterly gave a next coupon of 2045-05-31
/// where Excel gives 2045-05-28). Live Excel 20430, W111-5 G8-08.
fn coupon_date_before_maturity(ctx: CouponContext, periods: i64, preserve_month_end: bool) -> Option<i64> {
    let months_per_coupon = 12 / ctx.frequency;
    let (year, month, day) =
        ymd_from_excel_serial(WorkbookDateSystem::System1900, ctx.maturity as f64)?;
    let month_index = year
        .checked_mul(12)?
        .checked_add(month - 1)?
        .checked_sub(periods.checked_mul(months_per_coupon)?)?;
    let target_year = month_index.div_euclid(12);
    let target_month = month_index.rem_euclid(12) + 1;
    let month_end = days_in_month(target_year, target_month);
    let maturity_is_month_end = preserve_month_end && day == days_in_month(year, month);
    let target_day = if maturity_is_month_end {
        month_end
    } else {
        day.min(month_end)
    };
    Some(excel_serial_from_ymd_unbounded_1900(
        target_year,
        target_month,
        target_day,
    ))
}

Used by 6 functions.

locate_coupon_period · 17 lines · calc-func/src/ported/coupon_family.rs on GitHub
fn locate_coupon_period(ctx: CouponContext) -> Result<CouponPeriod, WorksheetErrorCode> {
    let mut next = ctx.maturity;
    let mut coupons = 1i64;
    loop {
        let previous =
            coupon_date_before_maturity(ctx, coupons, true).ok_or(WorksheetErrorCode::Num)?;
        if previous <= ctx.settlement {
            return Ok(CouponPeriod {
                previous: previous.max(0),
                next,
                remaining_coupons: coupons,
            });
        }
        next = previous;
        coupons += 1;
    }
}

Used by 6 functions.