use std::{collections::BTreeMap, process::exit}; use std::error::Error; use std::fs::File; use std::io::Read; use std::time::{SystemTime,Duration, UNIX_EPOCH}; use chrono::{DateTime,Utc}; use regex::Regex; fn parse( mut file: File, ) -> Result>, Box> { let mut contents = String::new(); file.read_to_string(&mut contents)?; let re = regex::Regex::new(r"(\d+) (\w+) (\d+)")?; Ok(parse_content(contents, re)) } fn parse_content( contents: String, re: Regex, ) -> BTreeMap> { dbg!(&contents); let mut metrics: BTreeMap>> = BTreeMap::new(); let show_invalid_line = | index: usize, line: &str | println!("invalid line: {} {}", index, line); for (index,line ) in contents.lines().enumerate() { if let Some(caps) = re.captures(line) { let timestamp_raw = &caps[1]; let metric_name = &caps[2]; let metric_value_raw = &caps[3]; let timestamp = timestamp_raw.parse::().unwrap_or_else(|e|{ println!("Parse timestamp {} error {}",timestamp_raw,e); 0 }); if timestamp == 0 { show_invalid_line(index, line); continue; } let metric_value = metric_value_raw.parse::().unwrap_or_else(|e|{ println!("Parse metric_value {} error {}",metric_value_raw,e); 0 as f64 }); if metric_value == 0 as f64 { show_invalid_line(index, line); continue; } let minute = UNIX_EPOCH + Duration::from_secs((timestamp - (timestamp % 60)) as u64); if let Some(metric) = metrics.get_mut(metric_name) { if let Some(metric_data) = metric.get_mut(&minute) { metric_data.push(metric_value); } else { metric.insert(minute, vec![metric_value]); } } else { let metric_time: BTreeMap > = [(minute, vec![metric_value])].into_iter().collect(); metrics.entry(metric_name.to_string()).or_insert( metric_time); } } else { show_invalid_line(index, line); } } let mut aggregated_metrics: BTreeMap> = BTreeMap::new(); metrics.into_iter().for_each(|(metric_name, time_val_list)| { time_val_list.into_iter().for_each(|(time, values)| { let average = values.iter().sum::() / values.len() as f64; if let Some(metric) = aggregated_metrics.get_mut(&metric_name) { if let Some(metric_data) = metric.get_mut(&time) { *metric_data = average; } else { metric.insert(time, average); } } else { let metric_time: BTreeMap = [(time, average)].into_iter().collect(); aggregated_metrics.entry(metric_name.to_string()).or_insert( metric_time); } }) }); aggregated_metrics } fn load_input(file_path: &str) -> Result>, Box> { let file = File::open(&file_path) .map_err(|err| format!("Error reading file: {} {}", &file_path, err))?; let metrics = parse(file) .map_err(|err| format!("Unable to parse: {} {}", &file_path, err))?; Ok(metrics) } fn show_metrics(metrics: BTreeMap>, output_path: &str) -> Vec { let mut output = Vec::new(); metrics.into_iter().for_each(|(metric_name, time_val)| for (time, value) in time_val { let output_line = format!( "{} {} {:?}", DateTime::::from(time).format("%Y-%m-%dT%H:%M:%SZ"), metric_name, value ); match output_path { "vec" => output.push(output_line), "print" | _ => println!("{}", output_line), } } ); output } fn main() { let default_input = String::from("input.txt"); match load_input(&default_input) { Ok(metrics) => { let _ = show_metrics(metrics, ""); }, Err(err) => { eprint!("Error: {}", err); exit(1); } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_load_input() -> Result<(), String>{ let default_input = String::from("input.txt"); match load_input(&default_input) { Ok(_) => Ok(()), Err(e) => Err(format!("Error: {}",e).into()), } } #[test] fn test_invalid_line_value() -> Result<(), String>{ let contents = String::from("1650973075 cpu A47\n"); let re = regex::Regex::new(r"(\d+) (\w+) (\d+)") .map_err(|err| format!("Error regex: {}", err))?; let result = parse_content(contents.clone(), re); if result.len() == 0 { Ok(()) } else { Err(format!("Error invalid line value: {}", contents).into()) } } #[test] fn test_invalid_line_time() -> Result<(), String>{ let contents = String::from("1650973075A cpu 47\n"); let re = regex::Regex::new(r"(\d+) (\w+) (\d+)") .map_err(|err| format!("Error regex: {}", err))?; let result = parse_content(contents.clone(), re); if result.len() == 0 { Ok(()) } else { Err(format!("Error invalid line time: {}", contents).into()) } } #[test] fn test_expected_metrics() { use std::io::{prelude::*, BufReader}; let default_input = String::from("input.txt"); let metrics = load_input(&default_input).unwrap_or_default(); let data_metrics = show_metrics(metrics, "vec"); let expected_output = String::from("output_expected.txt"); let file = File::open(expected_output.clone()).expect(format!("no such file: {}", expected_output).as_str()); let buf = BufReader::new(file); let lines: Vec = buf.lines().map(|l| l.expect("Could not parse line")).collect(); assert_eq!(lines.join("\n"),data_metrics.join("\n")); } }