1use anyhow::{anyhow, Context, Result};
2use chrono::{DateTime, FixedOffset, TimeZone, Utc};
3
4use crate::{
5 constants::GEOLOCATION_API_URL,
6 types::user_settings::{City, Units, UserSetting},
7 ErrorMessageType,
8};
9
10enum EventInfo<T: TimeZone> {
11 Sunrise(DateTime<T>),
12 Sunset(DateTime<T>),
13}
14impl<T: TimeZone> std::fmt::Display for EventInfo<T>
15where
16 T::Offset: std::fmt::Display,
17{
18 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19 let local_time = match self {
20 EventInfo::Sunrise(sunrise_time) => sunrise_time.format("Sunrise: %I:%M %p"),
21 EventInfo::Sunset(sunset_time) => sunset_time.format("Sunset: %I:%M %p"),
22 };
23 write!(f, "{}", local_time)
24 }
25}
26
27fn convert_utc_to_local_time(
31 sunrise_timestamp: i64,
32 sunset_timestamp: i64,
33 timezone: i32,
34) -> Result<(EventInfo<FixedOffset>, EventInfo<FixedOffset>)> {
35 let timezone = FixedOffset::east_opt(timezone).context("Failed to read timezone value.")?;
36
37 let current_time = Utc::now().with_timezone(&timezone);
38 let sunrise = DateTime::<Utc>::from_timestamp(sunrise_timestamp, 0)
39 .context("Failed to read sunrise time.")?
40 .with_timezone(&timezone);
41 let sunset = DateTime::<Utc>::from_timestamp(sunset_timestamp, 0)
42 .context("Failed to read sunset Time.")?
43 .with_timezone(&timezone);
44
45 if current_time < sunrise {
47 Ok((EventInfo::Sunrise(sunrise), EventInfo::Sunset(sunset)))
48 } else if current_time < sunset {
49 Ok((EventInfo::Sunset(sunset), EventInfo::Sunrise(sunrise)))
50 } else {
51 Ok((EventInfo::Sunrise(sunrise), EventInfo::Sunset(sunset)))
52 }
53}
54
55async fn get_response(url: String) -> Result<String> {
57 let resp = reqwest::get(&url).await?;
58 let text = resp.text().await?;
59 Ok(text)
60}
61
62pub async fn print_weather_information() -> Result<()> {
64 use crate::{
65 constants::{API_JSON_NAME, USER_SETTING_JSON_NAME, WEATHER_API_URL},
66 read_json_file, read_json_response, replace_url_placeholders,
67 types::{response_types::WeatherApiResponse, user_settings::ApiSetting},
68 URLPlaceholder,
69 };
70
71 let api_json_data = read_json_file::<ApiSetting>(API_JSON_NAME)?;
72 let setting_json_data = read_json_file::<UserSetting>(USER_SETTING_JSON_NAME)?;
73
74 let url = match (&setting_json_data.city, &setting_json_data.units) {
75 (Some(city), Some(unit)) => {
76 let units = unit.to_string();
77
78 replace_url_placeholders(
79 WEATHER_API_URL,
80 &[
81 URLPlaceholder {
82 placeholder: "{LAT_VALUE}".to_string(),
83 value: city.lat.to_string(),
84 },
85 URLPlaceholder {
86 placeholder: "{LON_VALUE}".to_string(),
87 value: city.lon.to_string(),
88 },
89 URLPlaceholder {
90 placeholder: "{API_KEY}".to_string(),
91 value: api_json_data.key,
92 },
93 URLPlaceholder {
94 placeholder: "{UNIT}".to_string(),
95 value: units,
96 },
97 ],
98 )
99 }
100 _ => {
101 return Err(anyhow!(
102 "Failed to read user setting! Please run 'set-location' command to configure settings."
103 ))
104 }
105 };
106
107 let response = get_response(url).await?;
108 let response_data = read_json_response::<WeatherApiResponse>(
109 &response,
110 ErrorMessageType::ApiResponseRead,
111 "WeatherApiResponse",
112 )?;
113
114 let upcoming_event = convert_utc_to_local_time(
115 response_data.sys.sunrise as i64,
116 response_data.sys.sunset as i64,
117 response_data.timezone,
118 )?;
119
120 {
122 let selected_city = setting_json_data
138 .city
139 .context("Failed to read city setting data.")?;
140 let selected_unit = setting_json_data
141 .units
142 .context("Failed to read unit setting data.")?;
143 let wind_unit = match selected_unit {
144 Units::Standard => "m/s",
145 Units::Metric => "m/s",
146 Units::Imperial => "mph",
147 };
148
149 let output_messages = [
150 String::new(),
151 format!("{} ({})", selected_city.name, selected_city.country),
152 format!(
153 "{temp}° / {main} ({description})",
154 temp = response_data.main.temp,
155 main = response_data.weather[0].main,
156 description = response_data.weather[0].description
157 ),
158 format!(
159 "H: {max}°, L: {min}°",
160 max = response_data.main.temp_max,
161 min = response_data.main.temp_min
162 ),
163 format!(
164 "\n- Wind Speed: {speed} {wind_speed_unit},",
165 speed = response_data.wind.speed,
166 wind_speed_unit = wind_unit
167 ),
168 format!(
169 "- Humidity: {humidity} %,",
170 humidity = response_data.main.humidity
171 ),
172 format!(
173 "- Pressure: {pressure} hPa",
174 pressure = response_data.main.pressure
175 ),
176 format!("- {}", upcoming_event.0),
177 format!(" ({})", upcoming_event.1),
178 ];
179
180 for item in output_messages {
181 println!("{}", item);
182 }
183
184 Ok(())
185 }
186}
187
188fn display_cities(city_slice: &[City]) {
190 println!("\n* City list:");
191 for (index, city) in city_slice.iter().enumerate() {
192 println!("{}) {}", index + 1, city);
193 }
194}
195
196fn read_user_input(messages: &[&str]) -> Result<String> {
198 use std::io;
199
200 for &message in messages {
201 println!("{}", message);
202 }
203
204 let mut user_input: String = String::new();
205 io::stdin().read_line(&mut user_input)?;
206 println!();
207
208 if user_input.is_empty() {
209 Err(anyhow!("Input is empty!"))
210 } else {
211 Ok(user_input)
212 }
213}
214
215fn select_user_preferences(cities: &[City]) -> Result<(String, Units)> {
217 use crate::user_setup::update_user_settings;
218
219 let city: &City = {
220 let user_input = read_user_input(&["Please select your city."])?;
221
222 let parsed_input: usize = user_input.trim().parse()?;
223 if parsed_input > cities.len() {
224 return Err(anyhow!("Invalid city index."));
225 }
226
227 &cities[parsed_input - 1]
228 };
229
230 let units: Units = {
231 let user_input = read_user_input(&[
232 "* Select your preferred unit.",
233 "* MORE INFO: https://openweathermap.org/weather-data",
234 "1) Standard",
235 "2) Metric",
236 "3) Imperial",
237 ])?;
238
239 let parsed_input: usize = user_input
240 .trim()
241 .parse()
242 .context("Failed to parse the input. Make sure it's a valid positive number.")?;
243
244 match parsed_input {
245 1 => Units::Standard,
246 2 => Units::Metric,
247 3 => Units::Imperial,
248 _ => return Err(anyhow!("Input is out of range!")),
249 }
250 };
251
252 let user_setting = UserSetting {
253 city: Some(City {
254 name: city.name.clone(),
255 lat: city.lat,
256 lon: city.lon,
257 country: city.country.clone(),
258 }),
259 units: Some(units.clone()),
260 };
261
262 update_user_settings(&user_setting)?;
263
264 Ok((city.name.clone(), units))
265}
266
267pub async fn search_city(query: &str) -> Result<()> {
269 use serde_json::Value;
270
271 use crate::{
272 constants::API_JSON_NAME, get_file_read_error_message, read_json_file,
273 replace_url_placeholders, types::user_settings::ApiSetting, URLPlaceholder,
274 };
275
276 if query.is_empty() {
277 return Err(anyhow!("Query cannot be empty."));
278 }
279
280 let api_json_data = read_json_file::<ApiSetting>(API_JSON_NAME)?;
281
282 let url = replace_url_placeholders(
283 GEOLOCATION_API_URL,
284 &[
285 URLPlaceholder {
286 placeholder: "{QUERY}".to_string(),
287 value: query.to_string(),
288 },
289 URLPlaceholder {
290 placeholder: "{API_KEY}".to_string(),
291 value: api_json_data.key.clone(),
292 },
293 ],
294 );
295 let response = get_response(url).await?;
296 let data: Value =
297 serde_json::from_str(&response).context("The given JSON input may be invalid.")?;
298
299 if let Some(401) = data["cod"].as_i64() {
301 return Err(anyhow!(get_file_read_error_message(
302 ErrorMessageType::InvalidApiKey,
303 None
304 )));
305 }
306
307 let mut cities: Vec<City> = vec![];
308
309 for city in data.as_array().unwrap() {
310 cities.push(City {
311 name: city["name"].as_str().unwrap().to_string(),
312 lat: city["lat"].as_f64().unwrap(),
313 lon: city["lon"].as_f64().unwrap(),
314 country: city["country"].as_str().unwrap().to_string(),
315 });
316 }
317 display_cities(&cities);
318
319 match select_user_preferences(&cities) {
320 Ok((city_name, unit_name)) => {
321 println!("{} is now your city!", city_name);
322 println!("I'll use {} for you.", unit_name);
323 }
324 Err(e) => {
325 println!("ERROR: {}", e);
326 }
327 };
328
329 Ok(())
330}