chore: changes to work as expected
This commit is contained in:
parent
27e50dc07d
commit
252e1d8493
22
CHANGES.md
Normal file
22
CHANGES.md
Normal 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)
|
@ -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
|
A **Refactor metric-consumer** task
|
||||||
|
|
||||||
|
198
src/main.rs
198
src/main.rs
@ -1,76 +1,168 @@
|
|||||||
use std::collections::HashMap;
|
use std::{collections::BTreeMap, process::exit};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Read;
|
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(
|
fn parse(
|
||||||
file: File,
|
mut file: File,
|
||||||
) -> Result<HashMap<String, HashMap<std::time::SystemTime, f64>>, Box<dyn Error>> {
|
) -> Result<BTreeMap<String, BTreeMap<SystemTime, f64>>, Box<dyn Error>> {
|
||||||
let mut file = file;
|
|
||||||
let mut contents = String::new();
|
let mut contents = String::new();
|
||||||
file.read_to_string(&mut contents)?;
|
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 (index,line ) in contents.lines().enumerate() {
|
||||||
|
|
||||||
for line in contents.lines() {
|
|
||||||
let re = regex::Regex::new(r"(\d+) (\w+) (\d+)").unwrap();
|
|
||||||
if let Some(caps) = re.captures(line) {
|
if let Some(caps) = re.captures(line) {
|
||||||
let timestamp_raw = &caps[1];
|
let timestamp_raw = &caps[1];
|
||||||
let metric_name = &caps[2];
|
let metric_name = &caps[2];
|
||||||
let metric_value_raw = &caps[3];
|
let metric_value_raw = &caps[3];
|
||||||
|
let timestamp = timestamp_raw.parse::<i64>().unwrap_or_else(|e|{
|
||||||
let timestamp = timestamp_raw.parse::<i64>().unwrap();
|
println!("Parse timestamp {} error {}",timestamp_raw,e);
|
||||||
let metric_value = metric_value_raw.parse::<f64>().unwrap();
|
0
|
||||||
|
});
|
||||||
if !metrics.contains_key(metric_name) {
|
if timestamp == 0 {
|
||||||
metrics.insert(metric_name.to_string(), HashMap::new());
|
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);
|
let minute = UNIX_EPOCH + Duration::from_secs((timestamp - (timestamp % 60)) as u64);
|
||||||
metrics
|
if let Some(metric) = metrics.get_mut(metric_name) {
|
||||||
.get_mut(metric_name)
|
if let Some(metric_data) = metric.get_mut(&minute) {
|
||||||
.unwrap()
|
metric_data.push(metric_value);
|
||||||
.insert(minute, vec![metric_value]);
|
} else {
|
||||||
} else {
|
metric.insert(minute, vec![metric_value]);
|
||||||
println!("invalid line");
|
}
|
||||||
}
|
} 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 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
|
|
||||||
}
|
}
|
||||||
let average = sum / values.len() as f64;
|
} else {
|
||||||
|
show_invalid_line(index, line);
|
||||||
aggregated_metrics
|
|
||||||
.get_mut(&metric_name)
|
|
||||||
.unwrap()
|
|
||||||
.insert(time, average);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
fn main() {
|
||||||
let file = File::open("input.txt").expect("Unable to open file");
|
let default_input = String::from("input.txt");
|
||||||
let metrics = parse(file).expect("Unable to parse file");
|
match load_input(&default_input) {
|
||||||
for (metric_name, time_val) in metrics {
|
Ok(metrics) => {
|
||||||
for (time, value) in time_val {
|
let _ = show_metrics(metrics, "");
|
||||||
println!(
|
},
|
||||||
"{} {:?} {:.2}",
|
Err(err) => {
|
||||||
metric_name,
|
eprint!("Error: {}", err);
|
||||||
chrono::DateTime::<chrono::Utc>::from(time),
|
exit(1);
|
||||||
value
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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"));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user