use super::helpers::{get_lx_and_qx, get_new_config_with_selected_table, get_value};
use crate::RSLifeResult;
use crate::mt_config::{AssumptionEnum, MortTableConfig};
use crate::param::SurvivalFunctionParams;
use bon::builder;
#[builder]
pub fn tpx(
mt: &MortTableConfig,
x: f64,
#[builder(default = 1.0)] t: f64,
#[builder(default = 0.0)] k: f64,
entry_age: Option<u32>,
#[builder(default = true)] validate: bool,
) -> RSLifeResult<f64> {
if validate {
let params = SurvivalFunctionParams {
mt: mt.clone(),
x,
t,
k,
entry_age,
};
params
.validate_all()
.map_err(|err| Box::new(err) as Box<dyn std::error::Error>)?;
}
let mt = get_new_config_with_selected_table(mt, entry_age)?;
let t = t + k;
if x.fract() == 0.0 && t.fract() == 0.0 {
return tpx_whole(&mt, x as u32, t as u32);
}
let x_whole = x.floor() as u32; let x_frac = x.fract(); let time_to_next_age = 1.0 - x_frac;
if t <= time_to_next_age {
tpx_frac_t(&mt, x, t)
} else {
let survival_to_next_age = tpx_frac_t(&mt, x, time_to_next_age)?;
let remaining_time = t - time_to_next_age;
let remaining_time_whole = remaining_time.floor() as u32;
let remaining_time_frac = remaining_time.fract();
let part1 = tpx_whole(&mt, x_whole + 1, remaining_time_whole)?;
let survival_for_remaining_time = if remaining_time_frac == 0.0 {
part1
} else {
let part2 = tpx_frac_t(
&mt,
x_whole as f64 + 1.0 + remaining_time_whole as f64,
remaining_time_frac,
)?;
part1 * part2
};
Ok(survival_to_next_age * survival_for_remaining_time)
}
}
#[builder]
pub fn tqx(
mt: &MortTableConfig,
x: f64,
#[builder(default = 1.0)] t: f64,
#[builder(default = 0.0)] k: f64,
entry_age: Option<u32>,
#[builder(default = true)] validate: bool,
) -> RSLifeResult<f64> {
let kpx_built = tpx().mt(mt).x(x).t(k).k(0.0).validate(validate);
let kpx = match entry_age {
Some(age) => kpx_built.entry_age(age).call()?,
None => kpx_built.call()?,
};
let ktpx_built = tpx().mt(mt).x(x).t(t).k(k).validate(validate);
let ktpx = match entry_age {
Some(age) => ktpx_built.entry_age(age).call()?,
None => ktpx_built.call()?,
};
Ok(kpx - ktpx)
}
#[builder]
pub fn lx(
mt: &MortTableConfig,
x: f64,
entry_age: Option<u32>,
#[builder(default = true)] validate: bool,
) -> RSLifeResult<f64> {
if validate {
let params = SurvivalFunctionParams {
mt: mt.clone(),
x,
t: 0.0,
k: 0.0,
entry_age,
};
params
.validate_all()
.map_err(|err| Box::new(err) as Box<dyn std::error::Error>)?;
}
let mt = get_new_config_with_selected_table(mt, entry_age)?;
let x_floor = x.floor() as u32;
let x_frac = x.fract();
if x_frac == 0.0 {
return get_value(&mt, x_floor, None, "lx");
}
let (lx, lx_next, qx) = get_lx_and_qx(&mt, x_floor)?;
let result = match mt.assumption {
AssumptionEnum::UDD => lx * (1.0 - x_frac) + lx_next * x_frac,
AssumptionEnum::CFM => (1.0 - qx).powf(x_frac) * lx,
_ => (1.0 - x_frac * qx) * lx,
};
Ok(result)
}
#[builder]
pub fn dx(
mt: &MortTableConfig,
x: f64,
entry_age: Option<u32>,
#[builder(default = true)] validate: bool,
) -> RSLifeResult<f64> {
if validate {
let params = SurvivalFunctionParams {
mt: mt.clone(),
x,
t: 0.0,
k: 0.0,
entry_age,
};
params
.validate_all()
.map_err(|err| Box::new(err) as Box<dyn std::error::Error>)?;
}
let mt = get_new_config_with_selected_table(mt, entry_age)?;
let lx_curr = lx().mt(&mt).x(x).validate(validate).call()?;
let lx_next = lx().mt(&mt).x(x + 1.0).validate(validate).call()?;
Ok(lx_curr - lx_next)
}
fn tpx_whole(mt: &MortTableConfig, x: u32, t: u32) -> RSLifeResult<f64> {
let l_x_t = get_value(mt, x + t, None, "lx")?;
let l_x = get_value(mt, x, None, "lx")?;
Ok(l_x_t / l_x)
}
fn tpx_frac_t(mt: &MortTableConfig, x: f64, t: f64) -> RSLifeResult<f64> {
let x_whole = x.floor() as u32;
let x_frac = x.fract();
let qx = get_value(mt, x_whole, None, "qx")?;
let survival_rate = match mt.assumption {
AssumptionEnum::UDD => 1.0 - t * qx / (1.0 - x_frac * qx),
AssumptionEnum::CFM => (1.0 - qx).powf(t),
_ => 1.0 - t * qx / (1.0 + x_frac * qx),
};
Ok(survival_rate)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mt_config::mt_data::MortData;
use crate::mt_config::{AssumptionEnum, MortTableConfig};
use approx::assert_abs_diff_eq;
#[test]
fn test_tpx_01() {
let am92 =
MortData::from_builtin("ELT15_F").expect("Failed to load EL15 No.15 Female table");
let mt = MortTableConfig::builder().data(am92).build().unwrap();
let ans = tpx().mt(&mt).x(58.0).t(0.5).k(0.0).call().unwrap();
let expected = 0.99670;
assert_abs_diff_eq!(ans, expected, epsilon = 1e-6);
}
#[test]
fn test_tpx_02() {
let pfa92c20 = MortData::from_builtin("PFA92C20").expect("Failed to load PFA92C20 table");
let mt = MortTableConfig::builder()
.data(pfa92c20)
.assumption(AssumptionEnum::CFM)
.build()
.unwrap();
let ans = tpx().mt(&mt).x(62.5).t(3.0).k(0.0).call().unwrap();
let expected = 0.988861;
assert_abs_diff_eq!(ans, expected, epsilon = 1e-5);
}
#[test]
fn test_tpx_03() {
let pfa92c20 = MortData::from_builtin("PFA92C20").expect("Failed to load PFA92C20 table");
let mt = MortTableConfig::builder().data(pfa92c20).build().unwrap();
let ans = tpx().mt(&mt).x(62.5).t(3.0).call().unwrap();
let expected = 0.988863;
assert_abs_diff_eq!(ans, expected, epsilon = 1e-6);
}
#[test]
fn test_tpx_04() {
let am92 = MortData::from_builtin("AM92").expect("Failed to load AM92 selected table");
let mt = MortTableConfig::builder().data(am92).build().unwrap();
let ans = tpx().mt(&mt).x(42.0).t(2.0).entry_age(42).call().unwrap();
let expected = 0.997929;
assert_abs_diff_eq!(ans, expected, epsilon = 1e-6);
}
#[test]
fn test_tqx_01() {
let am92 = MortData::from_builtin("AM92").expect("Failed to load AM92 selected table");
let mt = MortTableConfig::builder().data(am92).build().unwrap();
let ans = tqx()
.mt(&mt)
.x(41.0)
.t(3.0)
.k(0.0)
.entry_age(40)
.call()
.unwrap();
let expected = 0.003270;
assert_abs_diff_eq!(ans, expected, epsilon = 1e-6);
}
#[test]
fn test_tqx_02() {
let am92 = MortData::from_builtin("AM92").expect("Failed to load AM92 selected table");
let mt = MortTableConfig::builder().data(am92).build().unwrap();
let ans = tqx().mt(&mt).x(42.0).k(2.0).entry_age(41).call().unwrap();
let expected = 0.001324;
assert_abs_diff_eq!(ans, expected, epsilon = 1e-6);
}
#[test]
fn test_lx_01() {
let am92 = MortData::from_builtin("AM92").expect("Failed to load AM92 selected table");
let mt = MortTableConfig::builder()
.data(am92)
.radix(10000)
.build()
.unwrap();
let ans = (
lx().mt(&mt).x(42.0).entry_age(42).call().unwrap(),
lx().mt(&mt).x(42.0).entry_age(41).call().unwrap(),
lx().mt(&mt).x(42.0).call().unwrap(),
);
let expected = (9834.7030, 9836.5245, 9837.0661);
[ans.0, ans.1, ans.2]
.into_iter()
.zip([expected.0, expected.1, expected.2])
.fold((), |_, (a, e)| assert_abs_diff_eq!(a, e, epsilon = 1e-4));
}
}