diff --git a/DIRECTORY.md b/DIRECTORY.md index df0d5e2f7e3..3685ba5bf56 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -101,6 +101,8 @@ * [Subset Generation](https://github.com/TheAlgorithms/Rust/blob/master/src/dynamic_programming/subset_generation.rs) * [Trapped Rainwater](https://github.com/TheAlgorithms/Rust/blob/master/src/dynamic_programming/trapped_rainwater.rs) * [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) * 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) diff --git a/src/dynamic_programming/minimum_cost_path.rs b/src/dynamic_programming/minimum_cost_path.rs index f352965a6fd..e06481199cf 100644 --- a/src/dynamic_programming/minimum_cost_path.rs +++ b/src/dynamic_programming/minimum_cost_path.rs @@ -1,80 +1,177 @@ -/// Minimum Cost Path via Dynamic Programming - -/// Find the minimum cost traced by all possible paths from top left to bottom right in -/// a given matrix, by allowing only right and down movement - -/// For example, in matrix, -/// [2, 1, 4] -/// [2, 1, 3] -/// [3, 2, 1] -/// The minimum cost path is 7 - -/// # Arguments: -/// * `matrix` - The input matrix. -/// # Complexity -/// - time complexity: O( rows * columns ), -/// - space complexity: O( rows * columns ) use std::cmp::min; -pub fn minimum_cost_path(mut matrix: Vec>) -> usize { - // Add rows and columns variables for better readability - let rows = matrix.len(); - let columns = matrix[0].len(); +/// Represents possible errors that can occur when calculating the minimum cost path in a matrix. +#[derive(Debug, PartialEq, Eq)] +pub enum MatrixError { + /// Error indicating that the matrix is empty or has empty rows. + EmptyMatrix, + /// Error indicating that the matrix is not rectangular in shape. + NonRectangularMatrix, +} - // Preprocessing the first row - for i in 1..columns { - matrix[0][i] += matrix[0][i - 1]; +/// Computes the minimum cost path from the top-left to the bottom-right +/// corner of a matrix, where movement is restricted to right and down directions. +/// +/// # Arguments +/// +/// * `matrix` - A 2D vector of positive integers, where each element represents +/// the cost to step on that cell. +/// +/// # Returns +/// +/// * `Ok(usize)` - The minimum path cost to reach the bottom-right corner from +/// the top-left corner of the matrix. +/// * `Err(MatrixError)` - An error if the matrix is empty or improperly formatted. +/// +/// # Complexity +/// +/// * Time complexity: `O(m * n)`, where `m` is the number of rows +/// and `n` is the number of columns in the input matrix. +/// * Space complexity: `O(n)`, as only a single row of cumulative costs +/// is stored at any time. +pub fn minimum_cost_path(matrix: Vec>) -> Result { + // Check if the matrix is rectangular + if !matrix.iter().all(|row| row.len() == matrix[0].len()) { + return Err(MatrixError::NonRectangularMatrix); } - // Preprocessing the first column - for i in 1..rows { - matrix[i][0] += matrix[i - 1][0]; + // Check if the matrix is empty or contains empty rows + if matrix.is_empty() || matrix.iter().all(|row| row.is_empty()) { + return Err(MatrixError::EmptyMatrix); } - // Updating path cost for the remaining positions - // For each position, cost to reach it from top left is - // Sum of value of that position and minimum of upper and left position value + // Initialize the first row of the cost vector + let mut cost = matrix[0] + .iter() + .scan(0, |acc, &val| { + *acc += val; + Some(*acc) + }) + .collect::>(); - for i in 1..rows { - for j in 1..columns { - matrix[i][j] += min(matrix[i - 1][j], matrix[i][j - 1]); + // Process each row from the second to the last + for row in matrix.iter().skip(1) { + // Update the first element of cost for this row + cost[0] += row[0]; + + // Update the rest of the elements in the current row of cost + for col in 1..matrix[0].len() { + cost[col] = row[col] + min(cost[col - 1], cost[col]); } } - // Return cost for bottom right element - matrix[rows - 1][columns - 1] + // The last element in cost contains the minimum path cost to the bottom-right corner + Ok(cost[matrix[0].len() - 1]) } #[cfg(test)] mod tests { use super::*; - #[test] - fn basic() { - // For test case in example - let matrix = vec![vec![2, 1, 4], vec![2, 1, 3], vec![3, 2, 1]]; - assert_eq!(minimum_cost_path(matrix), 7); - - // For a randomly generated matrix - let matrix = vec![vec![1, 2, 3], vec![4, 5, 6]]; - assert_eq!(minimum_cost_path(matrix), 12); - } - - #[test] - fn one_element_matrix() { - let matrix = vec![vec![2]]; - assert_eq!(minimum_cost_path(matrix), 2); - } - - #[test] - fn one_row() { - let matrix = vec![vec![1, 3, 2, 1, 5]]; - assert_eq!(minimum_cost_path(matrix), 12); + macro_rules! minimum_cost_path_tests { + ($($name:ident: $test_case:expr,)*) => { + $( + #[test] + fn $name() { + let (matrix, expected) = $test_case; + assert_eq!(minimum_cost_path(matrix), expected); + } + )* + }; } - #[test] - fn one_column() { - let matrix = vec![vec![1], vec![3], vec![2], vec![1], vec![5]]; - assert_eq!(minimum_cost_path(matrix), 12); + minimum_cost_path_tests! { + basic: ( + vec![ + vec![2, 1, 4], + vec![2, 1, 3], + vec![3, 2, 1] + ], + Ok(7) + ), + single_element: ( + vec![ + vec![5] + ], + Ok(5) + ), + single_row: ( + vec![ + vec![1, 3, 2, 1, 5] + ], + Ok(12) + ), + single_column: ( + vec![ + vec![1], + vec![3], + vec![2], + vec![1], + vec![5] + ], + Ok(12) + ), + large_matrix: ( + vec![ + vec![1, 3, 1, 5], + vec![2, 1, 4, 2], + vec![3, 2, 1, 3], + vec![4, 3, 2, 1] + ], + Ok(10) + ), + uniform_matrix: ( + vec![ + vec![1, 1, 1], + vec![1, 1, 1], + vec![1, 1, 1] + ], + Ok(5) + ), + increasing_values: ( + vec![ + vec![1, 2, 3], + vec![4, 5, 6], + vec![7, 8, 9] + ], + Ok(21) + ), + high_cost_path: ( + vec![ + vec![1, 100, 1], + vec![1, 100, 1], + vec![1, 1, 1] + ], + Ok(5) + ), + complex_matrix: ( + vec![ + vec![5, 9, 6, 8], + vec![1, 4, 7, 3], + vec![2, 1, 8, 2], + vec![3, 6, 9, 4] + ], + Ok(23) + ), + empty_matrix: ( + vec![], + Err(MatrixError::EmptyMatrix) + ), + empty_row: ( + vec![ + vec![], + vec![], + vec![] + ], + Err(MatrixError::EmptyMatrix) + ), + non_rectangular: ( + vec![ + vec![1, 2, 3], + vec![4, 5], + vec![6, 7, 8] + ], + Err(MatrixError::NonRectangularMatrix) + ), } } diff --git a/src/financial/mod.rs b/src/financial/mod.rs new file mode 100644 index 00000000000..89b36bfa5e0 --- /dev/null +++ b/src/financial/mod.rs @@ -0,0 +1,2 @@ +mod present_value; +pub use present_value::present_value; diff --git a/src/financial/present_value.rs b/src/financial/present_value.rs new file mode 100644 index 00000000000..5294b71758c --- /dev/null +++ b/src/financial/present_value.rs @@ -0,0 +1,91 @@ +/// In economics and finance, present value (PV), also known as present discounted value, +/// is the value of an expected income stream determined as of the date of valuation. +/// +/// -> Wikipedia reference: https://en.wikipedia.org/wiki/Present_value + +#[derive(PartialEq, Eq, Debug)] +pub enum PresentValueError { + NegetiveDiscount, + EmptyCashFlow, +} + +pub fn present_value(discount_rate: f64, cash_flows: Vec) -> Result { + if discount_rate < 0.0 { + return Err(PresentValueError::NegetiveDiscount); + } + if cash_flows.is_empty() { + return Err(PresentValueError::EmptyCashFlow); + } + + let present_value = cash_flows + .iter() + .enumerate() + .map(|(i, &cash_flow)| cash_flow / (1.0 + discount_rate).powi(i as i32)) + .sum::(); + + Ok(round(present_value)) +} + +fn round(value: f64) -> f64 { + (value * 100.0).round() / 100.0 +} + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! test_present_value { + ($($name:ident: $inputs:expr,)*) => { + $( + #[test] + fn $name() { + let ((discount_rate,cash_flows), expected) = $inputs; + assert_eq!(present_value(discount_rate,cash_flows).unwrap(), expected); + } + )* + } + } + + macro_rules! test_present_value_Err { + ($($name:ident: $inputs:expr,)*) => { + $( + #[test] + fn $name() { + let ((discount_rate,cash_flows), expected) = $inputs; + assert_eq!(present_value(discount_rate,cash_flows).unwrap_err(), expected); + } + )* + } + } + + macro_rules! test_round { + ($($name:ident: $inputs:expr,)*) => { + $( + #[test] + fn $name() { + let (input, expected) = $inputs; + assert_eq!(round(input), expected); + } + )* + } + } + + test_present_value! { + general_inputs1:((0.13, vec![10.0, 20.70, -293.0, 297.0]),4.69), + general_inputs2:((0.07, vec![-109129.39, 30923.23, 15098.93, 29734.0, 39.0]),-42739.63), + general_inputs3:((0.07, vec![109129.39, 30923.23, 15098.93, 29734.0, 39.0]), 175519.15), + zero_input:((0.0, vec![109129.39, 30923.23, 15098.93, 29734.0, 39.0]), 184924.55), + + } + + test_present_value_Err! { + negative_discount_rate:((-1.0, vec![10.0, 20.70, -293.0, 297.0]), PresentValueError::NegetiveDiscount), + empty_cash_flow:((1.0, vec![]), PresentValueError::EmptyCashFlow), + + } + test_round! { + test1:(0.55434, 0.55), + test2:(10.453, 10.45), + test3:(1111_f64, 1111_f64), + } +} diff --git a/src/graph/minimum_spanning_tree.rs b/src/graph/minimum_spanning_tree.rs index 5efcb625e0c..9d36cafb303 100644 --- a/src/graph/minimum_spanning_tree.rs +++ b/src/graph/minimum_spanning_tree.rs @@ -1,24 +1,22 @@ -use super::DisjointSetUnion; +//! This module implements Kruskal's algorithm to find the Minimum Spanning Tree (MST) +//! of an undirected, weighted graph using a Disjoint Set Union (DSU) for cycle detection. -#[derive(Debug)] -pub struct Edge { - source: i64, - destination: i64, - cost: i64, -} +use crate::graph::DisjointSetUnion; -impl PartialEq for Edge { - fn eq(&self, other: &Self) -> bool { - self.source == other.source - && self.destination == other.destination - && self.cost == other.cost - } +/// Represents an edge in the graph with a source, destination, and associated cost. +#[derive(Debug, PartialEq, Eq)] +pub struct Edge { + /// The starting vertex of the edge. + source: usize, + /// The ending vertex of the edge. + destination: usize, + /// The cost associated with the edge. + cost: usize, } -impl Eq for Edge {} - impl Edge { - fn new(source: i64, destination: i64, cost: i64) -> Self { + /// Creates a new edge with the specified source, destination, and cost. + pub fn new(source: usize, destination: usize, cost: usize) -> Self { Self { source, destination, @@ -27,112 +25,135 @@ impl Edge { } } -pub fn kruskal(mut edges: Vec, number_of_vertices: i64) -> (i64, Vec) { - let mut dsu = DisjointSetUnion::new(number_of_vertices as usize); - - edges.sort_unstable_by(|a, b| a.cost.cmp(&b.cost)); - let mut total_cost: i64 = 0; - let mut final_edges: Vec = Vec::new(); - let mut merge_count: i64 = 0; - for edge in edges.iter() { - if merge_count >= number_of_vertices - 1 { +/// Executes Kruskal's algorithm to compute the Minimum Spanning Tree (MST) of a graph. +/// +/// # Parameters +/// +/// - `edges`: A vector of `Edge` instances representing all edges in the graph. +/// - `num_vertices`: The total number of vertices in the graph. +/// +/// # Returns +/// +/// An `Option` containing a tuple with: +/// +/// - The total cost of the MST (usize). +/// - A vector of edges that are included in the MST. +/// +/// Returns `None` if the graph is disconnected. +/// +/// # Complexity +/// +/// The time complexity is O(E log E), where E is the number of edges. +pub fn kruskal(mut edges: Vec, num_vertices: usize) -> Option<(usize, Vec)> { + let mut dsu = DisjointSetUnion::new(num_vertices); + let mut mst_cost: usize = 0; + let mut mst_edges: Vec = Vec::with_capacity(num_vertices - 1); + + // Sort edges by cost in ascending order + edges.sort_unstable_by_key(|edge| edge.cost); + + for edge in edges { + if mst_edges.len() == num_vertices - 1 { break; } - let source: i64 = edge.source; - let destination: i64 = edge.destination; - if dsu.merge(source as usize, destination as usize) < usize::MAX { - merge_count += 1; - let cost: i64 = edge.cost; - total_cost += cost; - let final_edge: Edge = Edge::new(source, destination, cost); - final_edges.push(final_edge); + // Attempt to merge the sets containing the edge’s vertices + if dsu.merge(edge.source, edge.destination) != usize::MAX { + mst_cost += edge.cost; + mst_edges.push(edge); } } - (total_cost, final_edges) + + // Return MST if it includes exactly num_vertices - 1 edges, otherwise None for disconnected graphs + (mst_edges.len() == num_vertices - 1).then_some((mst_cost, mst_edges)) } #[cfg(test)] mod tests { use super::*; - #[test] - fn test_seven_vertices_eleven_edges() { - let edges = vec![ - Edge::new(0, 1, 7), - Edge::new(0, 3, 5), - Edge::new(1, 2, 8), - Edge::new(1, 3, 9), - Edge::new(1, 4, 7), - Edge::new(2, 4, 5), - Edge::new(3, 4, 15), - Edge::new(3, 5, 6), - Edge::new(4, 5, 8), - Edge::new(4, 6, 9), - Edge::new(5, 6, 11), - ]; - - let number_of_vertices: i64 = 7; - - let expected_total_cost = 39; - let expected_used_edges = vec![ - Edge::new(0, 3, 5), - Edge::new(2, 4, 5), - Edge::new(3, 5, 6), - Edge::new(0, 1, 7), - Edge::new(1, 4, 7), - Edge::new(4, 6, 9), - ]; - - let (actual_total_cost, actual_final_edges) = kruskal(edges, number_of_vertices); - - assert_eq!(actual_total_cost, expected_total_cost); - assert_eq!(actual_final_edges, expected_used_edges); + macro_rules! test_cases { + ($($name:ident: $test_case:expr,)*) => { + $( + #[test] + fn $name() { + let (edges, num_vertices, expected_result) = $test_case; + let actual_result = kruskal(edges, num_vertices); + assert_eq!(actual_result, expected_result); + } + )* + }; } - #[test] - fn test_ten_vertices_twenty_edges() { - let edges = vec![ - Edge::new(0, 1, 3), - Edge::new(0, 3, 6), - Edge::new(0, 4, 9), - Edge::new(1, 2, 2), - Edge::new(1, 3, 4), - Edge::new(1, 4, 9), - Edge::new(2, 3, 2), - Edge::new(2, 5, 8), - Edge::new(2, 6, 9), - Edge::new(3, 6, 9), - Edge::new(4, 5, 8), - Edge::new(4, 9, 18), - Edge::new(5, 6, 7), - Edge::new(5, 8, 9), - Edge::new(5, 9, 10), - Edge::new(6, 7, 4), - Edge::new(6, 8, 5), - Edge::new(7, 8, 1), - Edge::new(7, 9, 4), - Edge::new(8, 9, 3), - ]; - - let number_of_vertices: i64 = 10; - - let expected_total_cost = 38; - let expected_used_edges = vec![ - Edge::new(7, 8, 1), - Edge::new(1, 2, 2), - Edge::new(2, 3, 2), - Edge::new(0, 1, 3), - Edge::new(8, 9, 3), - Edge::new(6, 7, 4), - Edge::new(5, 6, 7), - Edge::new(2, 5, 8), - Edge::new(4, 5, 8), - ]; - - let (actual_total_cost, actual_final_edges) = kruskal(edges, number_of_vertices); - - assert_eq!(actual_total_cost, expected_total_cost); - assert_eq!(actual_final_edges, expected_used_edges); + test_cases! { + test_seven_vertices_eleven_edges: ( + vec![ + Edge::new(0, 1, 7), + Edge::new(0, 3, 5), + Edge::new(1, 2, 8), + Edge::new(1, 3, 9), + Edge::new(1, 4, 7), + Edge::new(2, 4, 5), + Edge::new(3, 4, 15), + Edge::new(3, 5, 6), + Edge::new(4, 5, 8), + Edge::new(4, 6, 9), + Edge::new(5, 6, 11), + ], + 7, + Some((39, vec![ + Edge::new(0, 3, 5), + Edge::new(2, 4, 5), + Edge::new(3, 5, 6), + Edge::new(0, 1, 7), + Edge::new(1, 4, 7), + Edge::new(4, 6, 9), + ])) + ), + test_ten_vertices_twenty_edges: ( + vec![ + Edge::new(0, 1, 3), + Edge::new(0, 3, 6), + Edge::new(0, 4, 9), + Edge::new(1, 2, 2), + Edge::new(1, 3, 4), + Edge::new(1, 4, 9), + Edge::new(2, 3, 2), + Edge::new(2, 5, 8), + Edge::new(2, 6, 9), + Edge::new(3, 6, 9), + Edge::new(4, 5, 8), + Edge::new(4, 9, 18), + Edge::new(5, 6, 7), + Edge::new(5, 8, 9), + Edge::new(5, 9, 10), + Edge::new(6, 7, 4), + Edge::new(6, 8, 5), + Edge::new(7, 8, 1), + Edge::new(7, 9, 4), + Edge::new(8, 9, 3), + ], + 10, + Some((38, vec![ + Edge::new(7, 8, 1), + Edge::new(1, 2, 2), + Edge::new(2, 3, 2), + Edge::new(0, 1, 3), + Edge::new(8, 9, 3), + Edge::new(6, 7, 4), + Edge::new(5, 6, 7), + Edge::new(2, 5, 8), + Edge::new(4, 5, 8), + ])) + ), + test_disconnected_graph: ( + vec![ + Edge::new(0, 1, 4), + Edge::new(0, 2, 6), + Edge::new(3, 4, 2), + ], + 5, + None + ), } } diff --git a/src/lib.rs b/src/lib.rs index db12b52b6dd..fb64d09282a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ pub mod compression; pub mod conversions; pub mod data_structures; pub mod dynamic_programming; +pub mod financial; pub mod general; pub mod geometry; pub mod graph; diff --git a/src/string/palindrome.rs b/src/string/palindrome.rs index fae60cbebfa..6ee2d0be7ca 100644 --- a/src/string/palindrome.rs +++ b/src/string/palindrome.rs @@ -1,10 +1,29 @@ +//! A module for checking if a given string is a palindrome. + +/// Checks if the given string is a palindrome. +/// +/// A palindrome is a sequence that reads the same backward as forward. +/// This function ignores non-alphanumeric characters and is case-insensitive. +/// +/// # Arguments +/// +/// * `s` - A string slice that represents the input to be checked. +/// +/// # Returns +/// +/// * `true` if the string is a palindrome; otherwise, `false`. pub fn is_palindrome(s: &str) -> bool { - let mut chars = s.chars(); + let mut chars = s + .chars() + .filter(|c| c.is_alphanumeric()) + .map(|c| c.to_ascii_lowercase()); + while let (Some(c1), Some(c2)) = (chars.next(), chars.next_back()) { if c1 != c2 { return false; } } + true } @@ -12,13 +31,44 @@ pub fn is_palindrome(s: &str) -> bool { mod tests { use super::*; - #[test] - fn palindromes() { - assert!(is_palindrome("abcba")); - assert!(is_palindrome("abba")); - assert!(is_palindrome("a")); - assert!(is_palindrome("arcra")); - assert!(!is_palindrome("abcde")); - assert!(!is_palindrome("aaaabbbb")); + macro_rules! palindrome_tests { + ($($name:ident: $inputs:expr,)*) => { + $( + #[test] + fn $name() { + let (input, expected) = $inputs; + assert_eq!(is_palindrome(input), expected); + } + )* + } + } + + palindrome_tests! { + odd_palindrome: ("madam", true), + even_palindrome: ("deified", true), + single_character_palindrome: ("x", true), + single_word_palindrome: ("eye", true), + case_insensitive_palindrome: ("RaceCar", true), + mixed_case_and_punctuation_palindrome: ("A man, a plan, a canal, Panama!", true), + mixed_case_and_space_palindrome: ("No 'x' in Nixon", true), + empty_string: ("", true), + pompeii_palindrome: ("Roma-Olima-Milo-Amor", true), + napoleon_palindrome: ("Able was I ere I saw Elba", true), + john_taylor_palindrome: ("Lewd did I live, & evil I did dwel", true), + well_know_english_palindrome: ("Never odd or even", true), + palindromic_phrase: ("Rats live on no evil star", true), + names_palindrome: ("Hannah", true), + prime_minister_of_cambodia: ("Lon Nol", true), + japanese_novelist_and_manga_writer: ("Nisio Isin", true), + actor: ("Robert Trebor", true), + rock_vocalist: ("Ola Salo", true), + pokemon_species: ("Girafarig", true), + lychrel_num_56: ("121", true), + universal_palindrome_date: ("02/02/2020", true), + french_palindrome: ("une Slave valse nu", true), + finnish_palindrome: ("saippuakivikauppias", true), + non_palindrome_simple: ("hello", false), + non_palindrome_with_punctuation: ("hello!", false), + non_palindrome_mixed_case: ("Hello, World", false), } }