#![allow(non_snake_case)]
#![allow(clippy::too_many_arguments)]
use super::helpers::get_new_config_with_selected_table;
use super::survivals::{tpx, tqx};
use crate::RSLifeResult;
use crate::mt_config::MortTableConfig;
use crate::param::SingleLifeParams;
use bon::builder;
#[builder]
pub fn Exn(
mt: &MortTableConfig,
i: f64,
x: f64,
n: f64,
#[builder(default = 0.0)] t: f64,
#[builder(default = 1)] moment: u32,
entry_age: Option<u32>,
#[builder(default = true)] validate: bool,
) -> RSLifeResult<f64> {
if validate {
let params = SingleLifeParams {
mt: mt.clone(),
i,
x,
n,
t,
m: 1, moment,
entry_age,
};
params
.validate_all()
.map_err(|err| Box::new(err) as Box<dyn std::error::Error>)?;
};
let v = 1.0 / (1.0 + i);
let moment = moment as f64;
let mt = get_new_config_with_selected_table(mt, entry_age)?;
let discount_factor = v.powf(moment * (t + n)); let prob = tpx().mt(&mt).x(x).t(t + n).call()?; let result = discount_factor * prob;
Ok(result)
}
#[builder]
pub fn Axn1(
mt: &MortTableConfig,
i: f64,
x: f64,
n: f64,
#[builder(default = 0.0)] t: f64,
#[builder(default = 1)] moment: u32,
entry_age: Option<u32>,
#[builder(default = true)] validate: bool,
) -> RSLifeResult<f64> {
let built = Exn()
.mt(mt)
.i(i)
.x(x)
.n(n)
.t(t)
.moment(moment)
.validate(validate);
match entry_age {
Some(age) => built.entry_age(age).call(),
None => built.call(),
}
}
#[builder]
pub fn Ax1n(
mt: &MortTableConfig,
i: f64,
x: f64,
n: f64,
#[builder(default = 0.0)] t: f64,
#[builder(default = 1)] m: u32,
#[builder(default = 1)] moment: u32,
entry_age: Option<u32>,
#[builder(default = true)] validate: bool,
) -> RSLifeResult<f64> {
benefit_procedure(
mt,
i,
x,
n,
t,
m,
moment,
entry_age,
validate,
CashFlowStructure::Flat,
)
}
#[builder]
pub fn Ax(
mt: &MortTableConfig,
i: f64,
x: f64,
#[builder(default = 0.0)] t: f64,
#[builder(default = 1)] m: u32,
#[builder(default = 1)] moment: u32,
entry_age: Option<u32>,
#[builder(default = true)] validate: bool,
) -> RSLifeResult<f64> {
let max_age = mt.max_age()? as f64;
let n = max_age - x - t;
benefit_procedure(
mt,
i,
x,
n,
t,
m,
moment,
entry_age,
validate,
CashFlowStructure::Flat,
)
}
#[builder]
pub fn Axn(
mt: &MortTableConfig,
i: f64,
x: f64,
n: f64,
#[builder(default = 0.0)] t: f64,
#[builder(default = 1)] m: u32,
#[builder(default = 1)] moment: u32,
entry_age: Option<u32>,
#[builder(default = true)] validate: bool,
) -> RSLifeResult<f64> {
let mt = get_new_config_with_selected_table(mt, entry_age)?;
let term = Ax1n()
.mt(&mt)
.i(i)
.x(x)
.n(n)
.t(t)
.m(m)
.moment(moment)
.validate(validate)
.call()?;
let pure_endowment = Exn()
.mt(&mt)
.i(i)
.x(x)
.n(n)
.t(t)
.moment(moment)
.validate(validate)
.call()?;
Ok(term + pure_endowment)
}
#[builder]
pub fn IAx1n(
mt: &MortTableConfig,
i: f64,
x: f64,
n: f64,
#[builder(default = 0.0)] t: f64,
#[builder(default = 1)] m: u32,
#[builder(default = 1)] moment: u32,
entry_age: Option<u32>,
#[builder(default = true)] validate: bool,
) -> RSLifeResult<f64> {
benefit_procedure(
mt,
i,
x,
n,
t,
m,
moment,
entry_age,
validate,
CashFlowStructure::Increasing,
)
}
#[builder]
pub fn IAx(
mt: &MortTableConfig,
i: f64,
x: f64,
#[builder(default = 0.0)] t: f64,
#[builder(default = 1)] m: u32,
#[builder(default = 1)] moment: u32,
entry_age: Option<u32>,
#[builder(default = true)] validate: bool,
) -> RSLifeResult<f64> {
let max_age = mt.max_age()? as f64;
let n = max_age - x - t;
benefit_procedure(
mt,
i,
x,
n,
t,
m,
moment,
entry_age,
validate,
CashFlowStructure::Increasing,
)
}
#[builder]
pub fn IAxn(
mt: &MortTableConfig,
i: f64,
x: f64,
n: f64,
#[builder(default = 0.0)] t: f64,
#[builder(default = 1)] m: u32,
#[builder(default = 1)] moment: u32,
entry_age: Option<u32>,
#[builder(default = true)] validate: bool,
) -> RSLifeResult<f64> {
let mt = get_new_config_with_selected_table(mt, entry_age)?;
let term = IAx1n()
.mt(&mt)
.i(i)
.x(x)
.n(n)
.t(t)
.m(m)
.moment(moment)
.validate(validate)
.call()?;
let pure_endowment = Exn()
.mt(&mt)
.i(i)
.x(x)
.n(n)
.t(t)
.moment(moment)
.validate(validate)
.call()?;
Ok(term + pure_endowment)
}
#[builder]
pub fn DAx1n(
mt: &MortTableConfig,
i: f64,
x: f64,
n: f64,
#[builder(default = 0.0)] t: f64,
#[builder(default = 1)] m: u32,
#[builder(default = 1)] moment: u32,
entry_age: Option<u32>,
#[builder(default = true)] validate: bool,
) -> RSLifeResult<f64> {
benefit_procedure(
mt,
i,
x,
n,
t,
m,
moment,
entry_age,
validate,
CashFlowStructure::Decreasing,
)
}
#[builder]
pub fn DAxn(
mt: &MortTableConfig,
i: f64,
x: f64,
n: f64,
#[builder(default = 0.0)] t: f64,
#[builder(default = 1)] m: u32,
#[builder(default = 1)] moment: u32,
entry_age: Option<u32>,
#[builder(default = true)] validate: bool,
) -> RSLifeResult<f64> {
let mt = get_new_config_with_selected_table(mt, entry_age)?;
let term = DAx1n()
.mt(&mt)
.i(i)
.x(x)
.n(n)
.t(t)
.m(m)
.moment(moment)
.validate(validate)
.call()?;
let pure_endowment = Exn()
.mt(&mt)
.i(i)
.x(x)
.n(n)
.t(t)
.moment(moment)
.validate(validate)
.call()?;
Ok(term + pure_endowment)
}
#[builder]
pub fn gAx(
mt: &MortTableConfig,
i: f64,
x: f64,
#[builder(default = 0.0)] t: f64,
#[builder(default = 1)] m: u32,
#[builder(default = 1)] moment: u32,
entry_age: Option<u32>,
#[builder(default = true)] validate: bool,
g: f64,
) -> RSLifeResult<f64> {
let new_i = (1.0 + i) / (1.0 + g) - 1.0;
let built = Ax()
.mt(mt)
.i(new_i)
.x(x)
.t(t)
.m(m)
.moment(moment)
.validate(validate);
match entry_age {
Some(age) => built.entry_age(age).call(),
None => built.call(),
}
}
#[builder]
pub fn gAx1n(
mt: &MortTableConfig,
i: f64,
x: f64,
n: f64,
#[builder(default = 0.0)] t: f64,
#[builder(default = 1)] m: u32,
#[builder(default = 1)] moment: u32,
entry_age: Option<u32>,
#[builder(default = true)] validate: bool,
g: f64,
) -> RSLifeResult<f64> {
let new_i = (1.0 + i) / (1.0 + g) - 1.0;
let built = Ax1n()
.mt(mt)
.i(new_i)
.x(x)
.n(n)
.t(t)
.m(m)
.moment(moment)
.validate(validate);
match entry_age {
Some(age) => built.entry_age(age).call(),
None => built.call(),
}
}
#[builder]
pub fn gAxn(
mt: &MortTableConfig,
i: f64,
x: f64,
n: f64,
#[builder(default = 0.0)] t: f64,
#[builder(default = 1)] m: u32,
#[builder(default = 1)] moment: u32,
entry_age: Option<u32>,
#[builder(default = true)] validate: bool,
g: f64,
) -> RSLifeResult<f64> {
let new_i = (1.0 + i) / (1.0 + g) - 1.0;
let built = Axn()
.mt(mt)
.i(new_i)
.x(x)
.n(n)
.t(t)
.m(m)
.moment(moment)
.validate(validate);
match entry_age {
Some(age) => built.entry_age(age).call(),
None => built.call(),
}
}
#[derive(PartialEq)]
enum CashFlowStructure {
Flat,
Increasing,
Decreasing,
}
fn benefit_procedure(
mt: &MortTableConfig,
i: f64,
x: f64,
n: f64,
t: f64,
m: u32,
moment: u32,
entry_age: Option<u32>,
validate: bool,
structure: CashFlowStructure,
) -> RSLifeResult<f64> {
if validate {
let params = SingleLifeParams {
mt: mt.clone(),
i,
x,
n,
t,
m,
moment,
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 total_periods = (n * m as f64) as u32;
let k_arr: Vec<f64> = (0..total_periods).map(|k| k as f64).collect();
let v = 1.0 / (1.0 + i);
let m = f64::from(m);
let moment = f64::from(moment);
let discount_factors: Vec<f64> = k_arr
.iter()
.map(|&k| v.powf(moment * ((k + 1.0) / m)))
.collect();
let probabilities: Vec<f64> = k_arr
.iter()
.map(|&k| {
tqx()
.mt(&mt)
.x(x + t)
.t(1.0 / m)
.k(k / m)
.validate(false)
.call()
.unwrap_or(0.0)
})
.collect();
let amounts: Vec<f64> = match structure {
CashFlowStructure::Flat => vec![1.0; (n * m) as usize],
CashFlowStructure::Increasing => k_arr.iter().map(|&k| (k / m).floor() + 1.0).collect(),
CashFlowStructure::Decreasing => k_arr.iter().map(|&k| n - (k / m).floor()).collect(),
};
let summation: f64 = discount_factors
.iter()
.zip(probabilities.iter())
.zip(amounts.iter())
.map(|((df, prob), amount)| df * prob * amount)
.sum();
let deferred_factor = v.powf(t) * tpx().mt(&mt).x(x).t(t).validate(false).call()?;
Ok(summation * deferred_factor)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mt_config::MortTableConfig;
use crate::mt_config::mt_data::MortData;
use approx::assert_abs_diff_eq;
#[test]
fn test_fn_A1xn_n_is_0() {
let am92 = MortData::from_builtin("AM92").expect("Failed to load AM92 selected table");
let mt = MortTableConfig::builder().data(am92).build().unwrap();
let ans = Ax1n().mt(&mt).i(0.05).x(70.0).n(0.0).call().unwrap();
let expected = 0.0;
assert_abs_diff_eq!(ans, expected, epsilon = 1e-6);
}
#[test]
fn test_fn_Exn_n_is_0() {
let am92 = MortData::from_builtin("AM92").expect("Failed to load AM92 selected table");
let mt = MortTableConfig::builder().data(am92).build().unwrap();
let ans = Exn().mt(&mt).i(0.05).x(70.0).n(0.0).call().unwrap();
let expected = 1.0;
assert_abs_diff_eq!(ans, expected, epsilon = 1e-6);
}
#[test]
fn test_fn_Ax_benefit_01() {
let am92 = MortData::from_builtin("AM92").expect("Failed to load AM92 selected table");
let mt = MortTableConfig::builder().data(am92).build().unwrap();
let age = [17.0, 36.0, 71.0, 90.0];
let expected_ultimate = [0.10127, 0.19933, 0.61548, 0.84196];
let expected_selected = [0.10108, 0.19921, 0.61093, 0.82362];
let expected_second_moment_ultimate = [0.01716, 0.05207, 0.40686, 0.71874];
let expected_second_moment_selected = [0.01696, 0.05193, 0.40012, 0.68768];
for (i, &a) in age.iter().enumerate() {
let ultimate = Ax().mt(&mt).i(0.04).x(a).call().unwrap();
assert_abs_diff_eq!(ultimate, expected_ultimate[i], epsilon = 1e-5);
let selected = Ax()
.mt(&mt)
.i(0.04)
.x(a)
.entry_age(a as u32)
.call()
.unwrap();
assert_abs_diff_eq!(selected, expected_selected[i], epsilon = 1e-5);
let second_moment_ultimate = Ax().mt(&mt).i(0.04).x(a).moment(2).call().unwrap();
assert_abs_diff_eq!(
second_moment_ultimate,
expected_second_moment_ultimate[i],
epsilon = 1e-5
);
let second_moment_selected = Ax()
.mt(&mt)
.i(0.04)
.x(a)
.moment(2)
.entry_age(a as u32)
.call()
.unwrap();
assert_abs_diff_eq!(
second_moment_selected,
expected_second_moment_selected[i],
epsilon = 1e-5
);
}
}
#[test]
fn test_fn_Ax_benefit_02() {
let sult =
MortData::from_soa_custom("SULT").expect("Failed to load Standard Ultimate Life Table");
let mt = MortTableConfig::builder().data(sult).build().unwrap();
let age = [20.0, 27.0, 63.0, 84.0, 100.0];
let expected = [0.04922, 0.06725, 0.32785, 0.65990, 0.87068];
let expected_second_moment = [0.00580, 0.00900, 0.13421, 0.46137, 0.76427];
for (i, &a) in age.iter().enumerate() {
let ultimate = Ax().mt(&mt).i(0.05).x(a).call().unwrap();
assert_abs_diff_eq!(ultimate, expected[i], epsilon = 1e-5);
let second_moment = Ax().mt(&mt).i(0.05).x(a).moment(2).call().unwrap();
assert_abs_diff_eq!(second_moment, expected_second_moment[i], epsilon = 1e-5);
}
}
#[test]
fn test_fn_Axn_benefit_01() {
let am92 = MortData::from_builtin("AM92").expect("Failed to load AM92 selected table");
let mt = MortTableConfig::builder().data(am92).build().unwrap();
let age = [17.0, 29.0, 43.0, 59.0];
let expected_ultimate = [0.19475, 0.30525, 0.52073, 0.96154];
let expected_selected = [0.19459, 0.30515, 0.52061, 0.96154];
for (i, &a) in age.iter().enumerate() {
let ultimate = Axn().mt(&mt).i(0.04).x(a).n(60.0 - a).call().unwrap();
assert_abs_diff_eq!(ultimate, expected_ultimate[i], epsilon = 1e-5);
let selected = Axn()
.mt(&mt)
.i(0.04)
.x(a)
.n(60.0 - a)
.entry_age(a as u32)
.call()
.unwrap();
assert_abs_diff_eq!(selected, expected_selected[i], epsilon = 1e-5);
}
}
#[test]
fn test_fn_Axn_benefit_02() {
let sult =
MortData::from_soa_custom("SULT").expect("Failed to load Standard Ultimate Life Table");
let mt = MortTableConfig::builder().data(sult).build().unwrap();
let age = [20.0, 34.0, 57.0, 91.0, 100.0];
let expected_n10 = [0.61433, 0.61460, 0.61914, 0.77609, 0.87078];
let expected_n20 = [0.37829, 0.37961, 0.40118, 0.76735, 0.87068];
for (i, &a) in age.iter().enumerate() {
let n10 = Axn().mt(&mt).i(0.05).x(a).n(10.0).call().unwrap();
assert_abs_diff_eq!(n10, expected_n10[i], epsilon = 1e-5);
let n20 = Axn().mt(&mt).i(0.05).x(a).n(20.0).call().unwrap();
assert_abs_diff_eq!(n20, expected_n20[i], epsilon = 1e-5);
}
}
#[test]
fn test_fn_IAx_benefit_01() {
let am92 = MortData::from_builtin("AM92").expect("Failed to load AM92 selected table");
let mt = MortTableConfig::builder().data(am92).build().unwrap();
let ans = IAx().mt(&mt).i(0.06).x(110.0).call().unwrap();
let expected = 1.42096;
assert_abs_diff_eq!(ans, expected, epsilon = 1e-4);
}
#[test]
fn test_fn_IAx1n_benefit_01() {
let am92 = MortData::from_builtin("AM92").expect("Failed to load AM92 selected table");
let mt = MortTableConfig::builder().data(am92).build().unwrap();
let ans = IAx1n().mt(&mt).i(0.04).x(50.0).n(10.0).call().unwrap();
let expected = 8.55929 - (882.85 / 1366.61) * (8.36234 + 10.0 * 0.45640);
assert_abs_diff_eq!(ans, expected, epsilon = 1e-4);
}
}