rust_finprim 0.5.1

Various finance and accounting calculations/formulas implemented Rust
use crate::FloatLike;

/// APR - Annual (Nominal) Percentage Rate
///
/// Calculated from the effective interest rate and the number of compounding periods per year.
/// Similar behavior and usage to the `NOMINAL` function in Excel.
///
/// The APR is the annualized interest rate that you are charged on your loan.
/// It is expressed as a percentage that represents the actual yearly cost of funds over the term of a loan.
/// This includes any fees or additional costs associated with the transaction but does not take compounding into account.
///
/// # Arguments
/// * `ear` - The effective interest rate (EAR)
/// * `npery` - The number of compounding periods per year
///
/// # Returns
/// * The annual percentage rate (nominal interest rate)
///
/// # Example
/// * EAR of 5% with 12 compounding periods per year
/// ```
/// use rust_finprim::rate::apr;
/// let ear = 0.05; let npery = 12.0;
/// apr(ear, npery);
/// ```
///
/// # Formula
/// $$APR=n(\sqrt\[n\]{1+EAR}-1)$$
///
/// Where:
/// * \\(n\\) = number of compounding periods per year
/// * \\(EAR\\) = effective annual Rate
pub fn apr<T: FloatLike>(ear: T, npery: T) -> T {
    let nth_root = (T::one() + ear).powf(T::one() / npery);
    npery * (nth_root - T::one())
}

/// EAR - Effective Annual Rate
///
/// The effective annual rate (EAR) is the interest rate on a loan or financial product restated
/// from the nominal interest rate with compounding taken into account.
/// Similar behavior and usage to the `EFFECT` function in Excel.
///
/// The EAR is the rate actually paid or earned on an investment, loan or other financial product
/// due to the result of compounding over a given time period.
///
/// # Arguments
/// * `apr` - The annual percentage rate (APR, nominal interest rate)
/// * `npery` - The number of compounding periods per year
///
/// # Returns
/// * The effective annual rate (EAR)
///
/// # Example
/// * APR of 5% with 12 compounding periods per year
/// ```
/// use rust_finprim::rate::ear;
///
/// let apr = 0.05; let npery = 12.0;
/// ear(apr, npery);
/// ```
///
/// # Formula
/// $$EAR=(1+\frac{APR}{n})^n-1$$
///
/// Where:
/// * \\(APR\\) = annual percentage rate (nominal interest rate)
/// * \\(n\\) = number of compounding periods per year
pub fn ear<T: FloatLike>(apr: T, npery: T) -> T {
    let nth_root = T::one() + apr / npery;
    nth_root.powf(npery) - T::one()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[cfg(not(feature = "std"))]
    extern crate std;
    #[cfg(not(feature = "std"))]
    use std::assert;

    #[test]
    fn test_apr() {
        struct TestCase {
            n: f64,
            ear: f64,
            expected: f64,
            description: &'static str,
        }
        impl TestCase {
            fn new(n: f64, ear: f64, expected: f64, description: &'static str) -> TestCase {
                TestCase {
                    n,
                    ear,
                    expected,
                    description,
                }
            }
        }

        let test_cases = [
            TestCase::new(
                12.0,
                0.05,
                0.04889,
                "Standard case with EAR of 0.05 and monthly compounding",
            ),
            TestCase::new(12.0, 0.0, 0.0, "Zero EAR should result in zero APR"),
            TestCase::new(12.0, 0.2, 0.18371, "High EAR of 0.2 with monthly compounding"),
        ];

        for case in &test_cases {
            let calculated_apr = apr(case.ear, case.n);
            assert!(
                (calculated_apr - case.expected).abs() < 1e-5,
                "Failed on case: {}. Expected {}, got {}",
                case.description,
                case.expected,
                calculated_apr
            );
        }
    }

    #[test]
    fn test_ear() {
        struct TestCase {
            n: f64,
            apr: f64,
            expected: f64,
            description: &'static str,
        }
        impl TestCase {
            fn new(n: f64, apr: f64, expected: f64, description: &'static str) -> TestCase {
                TestCase {
                    n,
                    apr,
                    expected,
                    description,
                }
            }
        }

        let test_cases = [
            TestCase::new(
                12.0,
                0.05,
                0.05116,
                "Standard case with APR of 0.05 and monthly compounding",
            ),
            TestCase::new(12.0, 0.0, 0.0, "Zero APR should result in zero EAR"),
            TestCase::new(12.0, 0.2, 0.21939, "High APR of 0.2 with monthly compounding"),
        ];

        for case in &test_cases {
            let calculated_ear = ear(case.apr, case.n);
            assert!(
                (calculated_ear - case.expected).abs() < 1e-5,
                "Failed on case: {}. Expected {}, got {}",
                case.description,
                case.expected,
                calculated_ear
            );
        }
    }
}