sum_of_products/
lib.rs

1#![doc = include_str!("../README.md")]
2
3mod rendering;
4
5use std::{error::Error, io, iter};
6
7/// Result of computing products and their sum.
8pub struct SumOfProducts {
9    /// The product of each input row.
10    pub products: Vec<f64>,
11    /// The sum of all products.
12    pub sum: f64,
13}
14
15const EQ: &str = " = ";
16const SEP: &str = "----";
17
18/// Computes the product of each row and the sum of all products.
19#[must_use]
20pub fn compute(parsed: &[Vec<f64>]) -> SumOfProducts {
21    let products: Vec<f64> = parsed
22        .iter()
23        .map(|values| values.iter().product())
24        .collect();
25    let sum = products.iter().sum();
26    SumOfProducts { products, sum }
27}
28
29/// Parses lines of whitespace-separated numbers into rows of f64 values.
30///
31/// # Errors
32///
33/// Returns an error if reading fails or any word cannot be parsed as f64.
34pub fn parse<I: Iterator<Item = io::Result<String>>>(
35    lines: I,
36) -> Result<Vec<Vec<f64>>, Box<dyn Error>> {
37    lines
38        .map(|line| -> Result<Vec<f64>, _> {
39            // Convert each line from a string to a vector of numbers.
40            let values: Result<Vec<f64>, _> =
41                line?.split_ascii_whitespace().map(str::parse).collect();
42            Ok(values?)
43        })
44        // Discard empty vectors.
45        .filter(|result| !result.as_ref().is_ok_and(Vec::is_empty))
46        .collect()
47}
48
49/// Formats the computation as aligned formulas with a sum line.
50#[must_use]
51pub fn render(output: SumOfProducts, input: &[Vec<f64>]) -> String {
52    let SumOfProducts { products, sum } = output;
53    let formulas = rendering::render_formulas(input);
54    let sum_width = sum.to_string().len();
55    let width = rendering::formula_width(&formulas) + EQ.len() + sum_width;
56    formulas
57        .iter()
58        .zip(products.iter())
59        .map(|(formula, product)| format!("{formula}{EQ}{product:>sum_width$}"))
60        .chain(iter::once(format!("{SEP:>width$}\n{sum:>width$}")))
61        .collect::<Vec<_>>()
62        .join("\n")
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn test_compute() {
71        let input = vec![vec![2.0, 3.0], vec![4.0, 5.0]];
72        let result = compute(&input);
73        assert_eq!(result.products, vec![6.0, 20.0]);
74        assert_eq!(result.sum, 26.0);
75    }
76
77    #[test]
78    fn test_parse() {
79        let lines = vec![Ok("2 3".to_string()), Ok("4 5".to_string())];
80        let result = parse(lines.into_iter()).unwrap();
81        assert_eq!(result, vec![vec![2.0, 3.0], vec![4.0, 5.0]]);
82    }
83
84    #[test]
85    fn test_parse_skips_empty_lines() {
86        let lines = vec![Ok("2 3".to_string()), Ok("".to_string()), Ok("4 5".to_string())];
87        let result = parse(lines.into_iter()).unwrap();
88        assert_eq!(result, vec![vec![2.0, 3.0], vec![4.0, 5.0]]);
89    }
90}