diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..d91ecfd --- /dev/null +++ b/src/config.rs @@ -0,0 +1,16 @@ +#[derive(Debug, PartialEq)] +pub struct Config { + pub input_file_path: String, +} + +impl Config { + pub fn build(mut arguments: impl Iterator) -> Result { + arguments.next(); + let error_message = "Usage: billion_row_challenge "; + let input_file_path = match arguments.next() { + Some(argument) => argument, + None => return Err(error_message), + }; + Ok(Config { input_file_path }) + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..13172c8 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,27 @@ +use std::error::Error; +use std::fmt::{Display, Formatter}; + +#[derive(Debug)] +pub enum RunError { + InputOutputError(std::io::Error), + Other(Box), +} + +impl Display for RunError { + fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { + match self { + RunError::InputOutputError(error) => { + write!(formatter, "{error}") + } + RunError::Other(error) => write!(formatter, "{error}"), + } + } +} + +impl Error for RunError {} + +impl From for RunError { + fn from(error: std::io::Error) -> Self { + RunError::InputOutputError(error) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..09bc69c --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,19 @@ +use config::Config; +use error::RunError; +use std::fs; +use weather::WeatherStations; + +pub mod config; +pub mod error; +pub mod weather; + +pub fn run(config: &Config) -> Result<(), RunError> { + let file_content = fs::read_to_string(&config.input_file_path)?; + let mut lines: Vec = file_content.lines().map(|line| line.to_string()).collect(); + let mut weather_stations = WeatherStations::default(); + for line in lines.iter_mut() { + weather_stations.add_measurement(line); + } + println!("{}", weather_stations.output()); + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index e7a11a9..a89669f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,31 @@ +use std::env; +use std::io::ErrorKind; +use std::process; + +use billion_row_challenge::config::Config; +use billion_row_challenge::error::RunError; + fn main() { - println!("Hello, world!"); + let config = Config::build(env::args()).unwrap_or_else(|error| { + eprintln!("{error}"); + process::exit(1); + }); + match billion_row_challenge::run(&config) { + Ok(_) => (), + Err(error) => match error { + RunError::InputOutputError(error) => { + match error.kind() { + ErrorKind::NotFound => { + eprintln!("Error: File `{}` not found.", config.input_file_path) + } + _ => eprintln!("Error: {error}"), + } + process::exit(1); + } + RunError::Other(error) => { + eprintln!("Error: {error}"); + process::exit(1); + } + }, + } } diff --git a/src/weather.rs b/src/weather.rs new file mode 100644 index 0000000..0c868a5 --- /dev/null +++ b/src/weather.rs @@ -0,0 +1,171 @@ +use std::collections::HashMap; +use std::str::FromStr; + +fn round_towards_positive(value: f64, decimal_places: u32) -> f64 { + let factor = 10_f64.powi(decimal_places as i32); + (value * factor).ceil() / factor +} + +#[derive(Debug, Default, PartialEq, Clone)] +pub struct WeatherStationMeasurement { + pub name: String, + pub temperature: f64, +} + +impl FromStr for WeatherStationMeasurement { + type Err = &'static str; + + /// Parses a string `string` to return a value of [`WeatherStationMeasurement`] + /// + /// If parsing succeeds, return the value inside [`Ok`], otherwise + /// when the string is ill-formatted return an error specific to the + /// inside [`Err`]. + /// + /// # Examples + /// + /// ``` + /// use std::str::FromStr; + /// use billion_row_challenge::weather::WeatherStationMeasurement; + /// + /// let string = "Kunming;19.8"; + /// let expected_result = WeatherStationMeasurement { + /// name: String::from("Kunming"), + /// temperature: 19.8, + /// }; + /// let actual_result = WeatherStationMeasurement::from_str(string).unwrap(); + /// assert_eq!(actual_result, expected_result); + /// ``` + fn from_str(string: &str) -> Result { + let mut result = WeatherStationMeasurement::default(); + let mut parts = string.split(';'); + result.name = parts.next().unwrap_or("station").to_string(); + result.temperature = parts.next().unwrap_or("0").trim().parse().unwrap_or(0.0); + Ok(result) + } +} + +#[derive(Debug, Default, PartialEq, Clone)] +pub struct WeatherStationMeasurements { + pub name: String, + pub minimum: f64, + pub maximum: f64, + pub count: usize, + pub sum: f64, +} + +impl WeatherStationMeasurements { + pub fn average(&self) -> f64 { + if self.count == 0 { + return 0.0; + } + self.sum / (self.count as f64) + } + + /// Returns a string representation of the [`WeatherStationMeasurements`] instance in the format: + /// ```text + /// name=minimum/average/maximum + /// ``` + /// # Examples + /// ``` + /// use billion_row_challenge::weather::WeatherStationMeasurements; + /// + /// let weather_station = WeatherStationMeasurements { + /// name: String::from("Bosaso"), + /// minimum: -15.0, + /// maximum: 20.3, + /// count: 10, + /// sum: 100.0, + /// }; + /// let expected_output = "Bosaso=-15.0/10.0/20.3"; + /// let actual_output = weather_station.output(); + /// assert_eq!(actual_output, expected_output); + pub fn output(&self) -> String { + format!( + "{}={:.1}/{:.1}/{:.1}", + self.name, + self.minimum, + round_towards_positive(self.average(), 1), + self.maximum + ) + } +} + +#[derive(Debug, Default, PartialEq, Clone)] +pub struct WeatherStations { + pub stations: HashMap, +} + +impl WeatherStations { + pub fn add_measurement(&mut self, line: &str) { + let weather_station_measurement = + WeatherStationMeasurement::from_str(line).unwrap_or_default(); + match self.stations.get_mut(&weather_station_measurement.name) { + Some(value) => { + value.maximum = value.maximum.max(weather_station_measurement.temperature); + value.minimum = value.minimum.min(weather_station_measurement.temperature); + value.count += 1; + value.sum += weather_station_measurement.temperature; + } + None => { + let name = weather_station_measurement.name; + self.stations.insert( + name.clone(), + WeatherStationMeasurements { + name, + minimum: weather_station_measurement.temperature, + maximum: weather_station_measurement.temperature, + count: 1, + sum: weather_station_measurement.temperature, + }, + ); + } + } + } + + /// Returns a string representation of the [`WeatherStations`] instance in the format: + /// ```text + /// {station1, station2, station3} + /// ``` + /// # Examples + /// ``` + /// use billion_row_challenge::weather::WeatherStations; + /// use billion_row_challenge::weather::WeatherStationMeasurements; + /// use std::collections::HashMap; + /// + /// let mut weather_stations = WeatherStations { + /// stations: HashMap::new(), + /// }; + /// weather_stations.stations.insert( + /// String::from("Bosaso"), + /// WeatherStationMeasurements { + /// name: String::from("Bosaso"), + /// minimum: -15.0, + /// maximum: 20.0, + /// count: 10, + /// sum: 100.0, + /// }, + /// ); + /// weather_stations.stations.insert( + /// String::from("Petropavlovsk-Kamchatsky"), + /// WeatherStationMeasurements { + /// name: String::from("Petropavlovsk-Kamchatsky"), + /// minimum: -10.0, + /// maximum: 10.0, + /// count: 0, + /// sum: 0.0, + /// }, + /// ); + /// let expected_output = "{Bosaso=-15.0/10.0/20.0, Petropavlovsk-Kamchatsky=-10.0/0.0/10.0}"; + /// let actual_output = weather_stations.output(); + /// assert_eq!(actual_output, expected_output); + pub fn output(&self) -> String { + let mut outputs: Vec = vec![]; + let mut station_names: Vec<&String> = self.stations.keys().collect(); + station_names.sort_unstable(); + for station_name in station_names.into_iter() { + let weather_station = self.stations.get(station_name).unwrap(); + outputs.push(weather_station.output()); + } + format!("{{{}}}", outputs.join(", ")) + } +}