// 41.78, -71.41 // https://api.solunar.org/solunar/41.78,-71.41,20211029,-4 use cachememory::Cache; use chrono::{Date, DateTime, Duration, NaiveTime, Offset, Timelike, Utc}; use chrono_tz::Tz; use geo_types::{Latitude, Longitude}; use reqwest; const ENDPOINT: &str = "https://api.solunar.org/solunar"; #[derive(Clone, Debug, PartialEq)] pub struct SunMoon { pub sunrise: DateTime<Tz>, pub sunset: DateTime<Tz>, pub moonrise: Option<DateTime<Tz>>, pub moonset: Option<DateTime<Tz>>, pub moonphase: MoonPhase, } impl SunMoon { fn from_js(day: Date<Tz>, val: SunMoonJs) -> Self { fn new_time(day: Date<Tz>, val: String) -> Option<DateTime<Tz>> { NaiveTime::parse_from_str(&val, "%H:%M") .map(|tempo| day.and_hms(tempo.hour(), tempo.minute(), 0)) .ok() } let sunrise = new_time(day.clone(), val.sunrise).unwrap(); let sunset = new_time(day.clone(), val.sunset).unwrap(); let moonrise = val.moonrise.and_then(|v| new_time(day.clone(), v)); let moonset = val.moonset.and_then(|v| new_time(day.clone(), v)); Self { sunrise, sunset, moonrise, moonset, moonphase: val.moonphase, } } } #[derive(Clone, Debug, Deserialize)] pub(crate) struct SunMoonJs { #[serde(alias = "sunRise")] sunrise: String, #[serde(alias = "sunSet")] sunset: String, #[serde(alias = "moonRise")] moonrise: Option<String>, #[serde(alias = "moonSet")] moonset: Option<String>, #[serde(alias = "moonPhase")] moonphase: MoonPhase, } #[derive(Clone, Debug, Deserialize, PartialEq)] pub enum MoonPhase { #[serde(alias = "New Moon")] NewMoon, #[serde(alias = "Waxing Crescent")] WaxingCrescent, #[serde(alias = "First Quarter")] FirstQuarter, #[serde(alias = "Waxing Gibbous")] WaxingGibbous, #[serde(alias = "Full Moon")] FullMoon, #[serde(alias = "Waning Gibbous")] WaningGibbous, #[serde(alias = "Last Quarter")] LastQuarter, #[serde(alias = "Waning Crescent")] WaningCrescent, } pub struct SunMoonClient { client: reqwest::Client, cache: Cache<SunMoonJs>, } impl SunMoonClient { pub fn new() -> Self { Self { client: reqwest::Client::new(), cache: Cache::new(), } } pub async fn demandu( &self, latitude: Latitude, longitude: Longitude, day: Date<Tz>, ) -> SunMoon { let date = day.format("%Y%m%d"); let url = format!( "{}/{},{},{},{}", ENDPOINT, latitude, longitude, date, day.offset().fix().local_minus_utc() / 3600 ); let js = self .cache .find(&url, async { let response = self.client.get(&url).send().await.unwrap(); let tempolimo = response .headers() .get(reqwest::header::EXPIRES) .and_then(|header| header.to_str().ok()) .and_then(|tempolimo_str| DateTime::parse_from_rfc2822(tempolimo_str).ok()) .map(|dt_local| DateTime::<Utc>::from(dt_local)) .unwrap_or(Utc::now() + Duration::seconds(3600)); let soluna: SunMoonJs = response.json().await.unwrap(); (tempolimo, soluna) }) .await; SunMoon::from_js(day, js) } } #[cfg(test)] mod test { use super::*; use chrono::TimeZone; use chrono_tz::America::New_York; use serde_json; const EXAMPLE: &str = "{\"sunRise\":\"7:15\",\"sunTransit\":\"12:30\",\"sunSet\":\"17:45\",\"moonRise\":null,\"moonTransit\":\"7:30\",\"moonUnder\":\"19:54\",\"moonSet\":\"15:02\",\"moonPhase\":\"Waning Crescent\",\"moonIllumination\":0.35889454647387764,\"sunRiseDec\":7.25,\"sunTransitDec\":12.5,\"sunSetDec\":17.75,\"moonRiseDec\":null,\"moonSetDec\":15.033333333333333,\"moonTransitDec\":7.5,\"moonUnderDec\":19.9,\"minor1Start\":null,\"minor1Stop\":null,\"minor2StartDec\":14.533333333333333,\"minor2Start\":\"14:32\",\"minor2StopDec\":15.533333333333333,\"minor2Stop\":\"15:32\",\"major1StartDec\":6.5,\"major1Start\":\"06:30\",\"major1StopDec\":8.5,\"major1Stop\":\"08:30\",\"major2StartDec\":18.9,\"major2Start\":\"18:54\",\"major2StopDec\":20.9,\"major2Stop\":\"20:54\",\"dayRating\":1,\"hourlyRating\":{\"0\":20,\"1\":20,\"2\":0,\"3\":0,\"4\":0,\"5\":0,\"6\":20,\"7\":40,\"8\":40,\"9\":20,\"10\":0,\"11\":0,\"12\":0,\"13\":0,\"14\":0,\"15\":20,\"16\":20,\"17\":20,\"18\":40,\"19\":20,\"20\":20,\"21\":20,\"22\":0,\"23\":0}}"; #[test] fn it_parses_a_response() { let day = New_York.ymd(2021, 10, 29); let sun_moon_js: SunMoonJs = serde_json::from_str(EXAMPLE).unwrap(); let sun_moon = SunMoon::from_js(day.clone(), sun_moon_js); assert_eq!( sun_moon, SunMoon { sunrise: day.and_hms(7, 15, 0), sunset: day.and_hms(17, 45, 0), moonrise: None, moonset: Some(day.and_hms(15, 02, 0)), moonphase: MoonPhase::WaningCrescent, } ); } }