diff --git a/DIRECTORY.md b/DIRECTORY.md index 7a800e41379..f3ebd28b4a3 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -105,6 +105,10 @@ * [Word Break](https://github.com/TheAlgorithms/Rust/blob/master/src/dynamic_programming/word_break.rs) * Financial * [Present Value](https://github.com/TheAlgorithms/Rust/blob/master/src/financial/present_value.rs) + * [Compound Interest](https://github.com/TheAlgorithms/Rust/blob/master/src/financial/compound_interest.rs) + * [Net Present Value](https://github.com/TheAlgorithms/Rust/blob/master/src/financial/npv.rs) + * [Payback Period](https://github.com/TheAlgorithms/Rust/blob/master/src/financial/payback.rs) + * General * [Convex Hull](https://github.com/TheAlgorithms/Rust/blob/master/src/general/convex_hull.rs) * [Fisher Yates Shuffle](https://github.com/TheAlgorithms/Rust/blob/master/src/general/fisher_yates_shuffle.rs) @@ -262,6 +266,7 @@ * Navigation * [Bearing](https://github.com/TheAlgorithms/Rust/blob/master/src/navigation/bearing.rs) * [Haversine](https://github.com/TheAlgorithms/Rust/blob/master/src/navigation/haversine.rs) + * [Rhumbline](https://github.com/TheAlgorithms/Rust/blob/master/src/navigation/rhumbline.rs) * Number Theory * [Compute Totient](https://github.com/TheAlgorithms/Rust/blob/master/src/number_theory/compute_totient.rs) * [Euler Totient](https://github.com/TheAlgorithms/Rust/blob/master/src/number_theory/euler_totient.rs) diff --git a/src/financial/compound_interest.rs b/src/financial/compound_interest.rs new file mode 100644 index 00000000000..d348078ddcc --- /dev/null +++ b/src/financial/compound_interest.rs @@ -0,0 +1,24 @@ +// compound interest is given by A = P(1+r/n)^nt +// where: A = Final Amount, P = Principal Amount, r = rate of interest, +// n = number of times interest is compounded per year and t = time (in years) + +pub fn compound_interest(princpal: f64, rate: f64, comp_per_year: u32, years: f64) -> f64 { + let amount = princpal * (1.00 + rate / comp_per_year as f64).powf(comp_per_year as f64 * years); + return amount; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_compound_interest() { + let principal = 1000.0; + let rate = 0.05; // 5% annual interest + let times_per_year = 4; // interest compounded quarterly + let years = 2.0; // 2 years tenure + let result = compound_interest(principal, rate, times_per_year, years); + assert!((result - 1104.486).abs() < 0.001); // expected value rounded up to 3 decimal + // places + } +} diff --git a/src/financial/mod.rs b/src/financial/mod.rs index 89b36bfa5e0..0e0301b8555 100644 --- a/src/financial/mod.rs +++ b/src/financial/mod.rs @@ -1,2 +1,9 @@ +mod compound_interest; +mod npv; +mod payback; mod present_value; + +pub use compound_interest::compound_interest; +pub use npv::npv; +pub use payback::payback; pub use present_value::present_value; diff --git a/src/financial/npv.rs b/src/financial/npv.rs new file mode 100644 index 00000000000..d194fa302ff --- /dev/null +++ b/src/financial/npv.rs @@ -0,0 +1,44 @@ +/// Calculates Net Present Value given a vector of cash flows and a discount rate. +/// cash_flows: Vector of f64 representing cash flows for each period. +/// rate: Discount rate as an f64 (e.g., 0.05 for 5%) + +pub fn npv(cash_flows: &[f64], rate: f64) -> f64 { + cash_flows + .iter() + .enumerate() + .map(|(t, &cf)| cf / (1.00 + rate).powi(t as i32)) + .sum() +} + +// tests + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_npv_basic() { + let cash_flows = vec![-1000.0, 300.0, 400.0, -50.0]; + let rate = 0.10; + let result = npv(&cash_flows, rate); + // Calculated value ≈ -434.25 + assert!((result - (-434.25)).abs() < 0.05); // Allow small margin of error + } + + #[test] + fn test_npv_zero_rate() { + let cash_flows = vec![100.0, 200.0, -50.0]; + let rate = 0.0; + let result = npv(&cash_flows, rate); + assert!((result - 250.0).abs() < 0.05); + } + + #[test] + fn test_npv_empty() { + // For empty cash flows: NPV should be 0 + let cash_flows: Vec = vec![]; + let rate = 0.05; + let result = npv(&cash_flows, rate); + assert_eq!(result, 0.0); + } +} diff --git a/src/financial/payback.rs b/src/financial/payback.rs new file mode 100644 index 00000000000..012e50c503a --- /dev/null +++ b/src/financial/payback.rs @@ -0,0 +1,30 @@ +/// Returns the payback period in years +/// If investment is not paid back, returns None. + +pub fn payback(cash_flow: &[f64]) -> Option { + let mut total = 0.00; + for (year, &cf) in cash_flow.iter().enumerate() { + total += cf; + if total >= 0.00 { + return Some(year); + } + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_payback() { + let cash_flows = vec![-1000.0, 300.0, 400.0, 500.0]; + assert_eq!(payback(&cash_flows), Some(3)); // paid back in year 3 + } + + #[test] + fn test_no_payback() { + let cash_flows = vec![-1000.0, 100.0, 100.0, 100.0]; + assert_eq!(payback(&cash_flows), None); // never paid back + } +} diff --git a/src/navigation/mod.rs b/src/navigation/mod.rs index e62be90acbc..515396899c8 100644 --- a/src/navigation/mod.rs +++ b/src/navigation/mod.rs @@ -1,5 +1,8 @@ mod bearing; mod haversine; - +mod rhumbline; pub use self::bearing::bearing; pub use self::haversine::haversine; +pub use self::rhumbline::rhumb_bearing; +pub use self::rhumbline::rhumb_destination; +pub use self::rhumbline::rhumb_dist; diff --git a/src/navigation/rhumbline.rs b/src/navigation/rhumbline.rs new file mode 100644 index 00000000000..7f5e14d7257 --- /dev/null +++ b/src/navigation/rhumbline.rs @@ -0,0 +1,109 @@ +use std::f64::consts::PI; + +const EARTH_RADIUS: f64 = 6371000.0; + +pub fn rhumb_dist(lat1: f64, long1: f64, lat2: f64, long2: f64) -> f64 { + let phi1 = lat1 * PI / 180.00; + let phi2 = lat2 * PI / 180.00; + let del_phi = phi2 - phi1; + let mut del_lambda = (long2 - long1) * PI / 180.00; + + if del_lambda > PI { + del_lambda -= 2.00 * PI; + } else if del_lambda < -PI { + del_lambda += 2.00 * PI; + } + + let del_psi = ((phi2 / 2.00 + PI / 4.00).tan() / (phi1 / 2.00 + PI / 4.00).tan()).ln(); + let q = if del_psi.abs() > 1e-12 { + del_phi / del_psi + } else { + phi1.cos() + }; + + (del_phi.powf(2.00) + (q * del_lambda).powf(2.00)).sqrt() * EARTH_RADIUS +} + +pub fn rhumb_bearing(lat1: f64, long1: f64, lat2: f64, long2: f64) -> f64 { + let phi1 = lat1 * PI / 180.00; + let phi2 = lat2 * PI / 180.00; + let mut del_lambda = (long2 - long1) * PI / 180.00; + + if del_lambda > PI { + del_lambda -= 2.0 * PI; + } else if del_lambda < -PI { + del_lambda += 2.0 * PI; + } + + let del_psi = ((phi2 / 2.00 + PI / 4.00).tan() / (phi1 / 2.00 + PI / 4.00).tan()).ln(); + let bearing = del_lambda.atan2(del_psi) * 180.0 / PI; + (bearing + 360.00) % 360.00 +} +pub fn rhumb_destination(lat: f64, long: f64, distance: f64, bearing: f64) -> (f64, f64) { + let del = distance / EARTH_RADIUS; + let phi1 = lat * PI / 180.00; + let lambda1 = long * PI / 180.00; + let theta = bearing * PI / 180.00; + + let del_phi = del * theta.cos(); + let phi2 = (phi1 + del_phi).clamp(-PI / 2.0, PI / 2.0); + + let del_psi = ((phi2 / 2.00 + PI / 4.00).tan() / (phi1 / 2.0 + PI / 4.0).tan()).ln(); + let q = if del_psi.abs() > 1e-12 { + del_phi / del_psi + } else { + phi1.cos() + }; + + let del_lambda = del * theta.sin() / q; + let lambda2 = lambda1 + del_lambda; + + (phi2 * 180.00 / PI, lambda2 * 180.00 / PI) +} + +// TESTS +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_rhumb_distance() { + let distance = rhumb_dist(28.5416, 77.2006, 28.5457, 77.1928); + assert!(distance > 700.00 && distance < 1000.0); + } + + #[test] + fn test_rhumb_bearing() { + let bearing = rhumb_bearing(28.5416, 77.2006, 28.5457, 77.1928); + assert!((bearing - 300.0).abs() < 5.0); + } + + #[test] + fn test_rhumb_destination_point() { + let (lat, lng) = rhumb_destination(28.5457, 77.1928, 1000.00, 305.0); + assert!((lat - 28.550).abs() < 0.010); + assert!((lng - 77.1851).abs() < 0.010); + } + // edge cases + + #[test] + fn test_rhumb_distance_cross_antimeridian() { + // Test when del_lambda > PI (line 12) + let distance = rhumb_dist(0.0, 170.0, 0.0, -170.0); + assert!(distance > 0.0); + } + + #[test] + fn test_rhumb_distance_cross_antimeridian_negative() { + // Test when del_lambda < -PI (line 14) + let distance = rhumb_dist(0.0, -170.0, 0.0, 170.0); + assert!(distance > 0.0); + } + + #[test] + fn test_rhumb_distance_to_equator() { + // Test when del_psi is near zero (line 21 - the else branch) + let distance = rhumb_dist(0.0, 0.0, 0.0, 1.0); + assert!(distance > 0.0); + } +}