chore: changes to work as expected

This commit is contained in:
Jesús Pérez 2024-10-17 15:27:08 +01:00
parent 27e50dc07d
commit 252e1d8493
No known key found for this signature in database
3 changed files with 174 additions and 54 deletions

22
CHANGES.md Normal file
View File

@ -0,0 +1,22 @@
# Backend internal interview (rust)
## CHANGES: fixed branch
- Moving from **HashMap** to **BTreeMap** to preseve loading lines order.
- Fix **lines parser** in Maps **metrics** and in **aggregated_metrics** to be handled properly.
- Fix **unwrap / expect** for obvious reasons, add simple error message via **map_err**, **unwrap_or_else** and **closure function**.
- Fix some source code lines order, output format and conversions.
- Change some **iterators** to <u>functional mode</u>.
- Separate tasks to independent funtions for better isolation, less responsibilities and help on **tests** handling.
- Adding **output_path** to help on result output options.
- Following **unit tests** are implemented:
- <u>test_load_input</u> test load input file **input.txt**.
- <u>test_invalid_line_value</u> test use invalid value in line parsing.
- <u>test_invalid_line</u> test use invlid value in line parsing.
- <u>test_expected_metrics</u> test load input data from **input.txt** compare with **output_expected.txt**.
> Code simply **works as expected**
> It is not be able to **process multiple metrics in parallel** yet.
> A full refactoring has to be done for <u>better quality, maintenance and be more readable</u>. (Structs, implementaitions, settings for multiple inputs, etc).
Next round in: [Improve branch](/NewRelic/be-technical-interview-rust/src/branch/improved)

View File

@ -1,4 +1,10 @@
# Backend internal interview (rust)
# Backend internal interview (rust) - basic-fixed
> Code simply **works as expected**
> It is not be able to **process multiple metrics in parallel** yet.
> A full refactoring has to be done for <u>better quality, maintenance and be more readable</u>. (Structs, implementaitions, settings for multiple inputs, etc).
See [main chainges](CHAINGES.md)
A **Refactor metric-consumer** task

View File

@ -1,76 +1,168 @@
use std::collections::HashMap;
use std::{collections::BTreeMap, process::exit};
use std::error::Error;
use std::fs::File;
use std::io::Read;
use std::time::{Duration, UNIX_EPOCH};
use std::time::{SystemTime,Duration, UNIX_EPOCH};
use chrono::{DateTime,Utc};
use regex::Regex;
fn parse(
file: File,
) -> Result<HashMap<String, HashMap<std::time::SystemTime, f64>>, Box<dyn Error>> {
let mut file = file;
mut file: File,
) -> Result<BTreeMap<String, BTreeMap<SystemTime, f64>>, Box<dyn Error>> {
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<String, BTreeMap<SystemTime, f64>> {
dbg!(&contents);
let mut metrics: BTreeMap<String, BTreeMap<SystemTime, Vec<f64>>> = BTreeMap::new();
let show_invalid_line = | index: usize, line: &str | println!("invalid line: {} {}", index, line);
let mut metrics: HashMap<String, HashMap<std::time::SystemTime, Vec<f64>>> = HashMap::new();
for line in contents.lines() {
let re = regex::Regex::new(r"(\d+) (\w+) (\d+)").unwrap();
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::<i64>().unwrap();
let metric_value = metric_value_raw.parse::<f64>().unwrap();
if !metrics.contains_key(metric_name) {
metrics.insert(metric_name.to_string(), HashMap::new());
}
let timestamp = timestamp_raw.parse::<i64>().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::<f64>().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);
metrics
.get_mut(metric_name)
.unwrap()
.insert(minute, vec![metric_value]);
} else {
println!("invalid line");
}
}
let mut aggregated_metrics: HashMap<String, HashMap<std::time::SystemTime, f64>> =
HashMap::new();
for (metric_name, time_val_list) in metrics {
aggregated_metrics.insert(metric_name.clone(), HashMap::new());
for (time, values) in time_val_list {
let mut sum = 0.0;
for v in values.iter() {
sum += *v
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 <SystemTime, Vec<f64>> = [(minute, vec![metric_value])].into_iter().collect();
metrics.entry(metric_name.to_string()).or_insert( metric_time);
}
let average = sum / values.len() as f64;
aggregated_metrics
.get_mut(&metric_name)
.unwrap()
.insert(time, average);
} else {
show_invalid_line(index, line);
}
}
Ok(aggregated_metrics)
let mut aggregated_metrics: BTreeMap<String, BTreeMap<SystemTime, f64>> = 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::<f64>() / 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 <SystemTime,f64> = [(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<BTreeMap<String, BTreeMap<SystemTime, f64>>, Box<dyn Error>> {
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<String, BTreeMap<SystemTime, f64>>, output_path: &str) -> Vec<String> {
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::<Utc>::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 file = File::open("input.txt").expect("Unable to open file");
let metrics = parse(file).expect("Unable to parse file");
for (metric_name, time_val) in metrics {
for (time, value) in time_val {
println!(
"{} {:?} {:.2}",
metric_name,
chrono::DateTime::<chrono::Utc>::from(time),
value
);
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<String> = buf.lines().map(|l| l.expect("Could not parse line")).collect();
assert_eq!(lines.join("\n"),data_metrics.join("\n"));
}
}