diff --git a/server/src/entrypoints/types.rs b/server/src/entrypoints/types.rs index 1debcbc..e91d47c 100644 --- a/server/src/entrypoints/types.rs +++ b/server/src/entrypoints/types.rs @@ -112,6 +112,9 @@ pub struct Streak { streak_type: String, current: u32, longest: u32, + achived: bool, + start_time: chrono::DateTime, + end_time: chrono::DateTime, } impl Streak { @@ -129,23 +132,31 @@ impl Streak { .unwrap_or_default(); let current_time_string = time_period.time_string(current_time as u64); let previous_period_string = time_period.time_string(previous_period); - if current_time_string == time_period_string - || previous_period_string == time_period_string - { - return Self { - name, - streak_type, - current, - longest, - }; + + let mut result = Self { + name, + streak_type, + current, + longest, + achived: time_period_string == current_time_string, + start_time: time_period + .start_period(current_time as u64) + .unwrap_or_default(), + end_time: time_period + .end_period(current_time as u64) + .unwrap_or_default(), }; - } - Self { - name, - streak_type, - current: 0, - longest, + if current_time_string != time_period_string + && previous_period_string != time_period_string + { + result.current = 0; + } + + result + } else { + // TODO: probably we need to return error here for user request + Self::default() } } } @@ -156,7 +167,7 @@ pub struct UserProfile { pub rating: u32, pub contributions: u32, pub leaderboard_places: HashMap, - pub streaks: HashMap, + pub streaks: Vec, } impl From for UserProfile { @@ -175,15 +186,12 @@ impl From for UserProfile { .streaks .into_iter() .map(|streak| { - ( - streak.name.clone(), - Streak::new( - streak.name, - streak.amount as u32, - streak.best as u32, - streak.latest_time_string, - streak.streak_type, - ), + Streak::new( + streak.name, + streak.amount as u32, + streak.best as u32, + streak.latest_time_string, + streak.streak_type, ) }) .collect(), diff --git a/shared/src/pr.rs b/shared/src/pr.rs index eafb388..369c407 100644 --- a/shared/src/pr.rs +++ b/shared/src/pr.rs @@ -134,6 +134,7 @@ impl PRWithRating { let percentage = (self.percentage_multiplier + 100) as f64; ((score as f64 * percentage / 100.0).ceil()) as u32 } + pub fn score(&self) -> Option { self.score .iter() diff --git a/shared/src/timeperiod.rs b/shared/src/timeperiod.rs index 2473f04..0479bef 100644 --- a/shared/src/timeperiod.rs +++ b/shared/src/timeperiod.rs @@ -1,4 +1,4 @@ -use chrono::{DateTime, Datelike, Days, Months}; +use chrono::{DateTime, Datelike, Days, Months, NaiveDate, TimeZone, Utc}; use near_sdk::Timestamp; use strum::EnumIter; @@ -80,6 +80,69 @@ impl TimePeriod { Some(result) } + + pub fn end_period(&self, timestamp: Timestamp) -> Option> { + let date_time = DateTime::::from_timestamp_nanos(timestamp as i64).date_naive(); + + match self { + TimePeriod::Day => (date_time + chrono::Duration::days(1)) + .and_hms_opt(0, 0, 0) + .map(|d| d.and_utc()), + TimePeriod::Week => { + let iso_week = date_time.iso_week(); + NaiveDate::from_isoywd_opt( + iso_week.year(), + iso_week.week() + 1, + chrono::Weekday::Mon, + ) + .and_then(|d| d.and_hms_opt(0, 0, 0).map(|d| d.and_utc())) + } + TimePeriod::Month => if date_time.month() == 12 { + Utc.with_ymd_and_hms(date_time.year() + 1, 1, 1, 0, 0, 0) + } else { + Utc.with_ymd_and_hms(date_time.year(), date_time.month() + 1, 1, 0, 0, 0) + } + .earliest(), + TimePeriod::Quarter => { + let current_quarter = (date_time.month() - 1) / 3 + 1; + if current_quarter == 4 { + Utc.with_ymd_and_hms(date_time.year() + 1, 1, 1, 0, 0, 0) + } else { + Utc.with_ymd_and_hms(date_time.year(), current_quarter * 3 + 1, 1, 0, 0, 0) + } + .earliest() + } + TimePeriod::Year => Utc + .with_ymd_and_hms(date_time.year() + 1, 1, 1, 0, 0, 0) + .earliest(), + TimePeriod::AllTime => None, + } + } + + pub fn start_period(&self, timestamp: Timestamp) -> Option> { + let date_time = DateTime::::from_timestamp_nanos(timestamp as i64).date_naive(); + + match self { + TimePeriod::Day => date_time.and_hms_opt(0, 0, 0).map(|d| d.and_utc()), + TimePeriod::Week => { + let iso_week = date_time.iso_week(); + NaiveDate::from_isoywd_opt(iso_week.year(), iso_week.week(), chrono::Weekday::Mon) + .and_then(|d| d.and_hms_opt(0, 0, 0).map(|d| d.and_utc())) + } + TimePeriod::Month => Utc + .with_ymd_and_hms(date_time.year(), date_time.month(), 1, 0, 0, 0) + .earliest(), + TimePeriod::Quarter => { + let current_quarter = (date_time.month() - 1) / 3 + 1; + Utc.with_ymd_and_hms(date_time.year(), current_quarter * 3 - 2, 1, 0, 0, 0) + .earliest() + } + TimePeriod::Year => Utc + .with_ymd_and_hms(date_time.year(), 1, 1, 0, 0, 0) + .earliest(), + TimePeriod::AllTime => None, + } + } } // Helper function to convert timestamp to quarter string @@ -103,3 +166,206 @@ fn timestamp_to_month_string(timestamp: u64) -> TimePeriodString { let date = DateTime::from_timestamp_nanos(timestamp as i64); format!("{:02}{:04}", date.month(), date.year()) } + +#[cfg(test)] +mod tests { + use super::*; + use chrono::{TimeZone, Utc}; + + #[test] + fn test_from_streak_type() { + assert_eq!(TimePeriod::from_streak_type("daily"), Some(TimePeriod::Day)); + assert_eq!( + TimePeriod::from_streak_type("weekly"), + Some(TimePeriod::Week) + ); + assert_eq!( + TimePeriod::from_streak_type("monthly"), + Some(TimePeriod::Month) + ); + assert_eq!( + TimePeriod::from_streak_type("quarterly"), + Some(TimePeriod::Quarter) + ); + assert_eq!( + TimePeriod::from_streak_type("yearly"), + Some(TimePeriod::Year) + ); + assert_eq!( + TimePeriod::from_streak_type("all-time"), + Some(TimePeriod::AllTime) + ); + assert_eq!(TimePeriod::from_streak_type("unknown"), None); + } + + #[test] + fn test_time_string() { + let timestamp = Utc + .with_ymd_and_hms(2023, 6, 20, 0, 0, 0) + .unwrap() + .timestamp_nanos_opt() + .unwrap() as Timestamp; + + assert_eq!(TimePeriod::Day.time_string(timestamp), "20062023"); + assert_eq!(TimePeriod::Week.time_string(timestamp), "2023W25"); + assert_eq!(TimePeriod::Month.time_string(timestamp), "062023"); + assert_eq!(TimePeriod::Quarter.time_string(timestamp), "2023Q2"); + assert_eq!(TimePeriod::Year.time_string(timestamp), "2023"); + assert_eq!( + TimePeriod::AllTime.time_string(timestamp), + "all-time".to_string() + ); + } + + #[test] + fn test_previous_period() { + let timestamp = Utc + .with_ymd_and_hms(2023, 6, 20, 0, 0, 0) + .unwrap() + .timestamp_nanos_opt() + .unwrap() as Timestamp; + + let previous_day = Utc + .with_ymd_and_hms(2023, 6, 19, 0, 0, 0) + .unwrap() + .timestamp_nanos_opt() + .unwrap() as Timestamp; + assert_eq!( + TimePeriod::Day.previous_period(timestamp), + Some(previous_day) + ); + + let previous_week = Utc + .with_ymd_and_hms(2023, 6, 13, 0, 0, 0) + .unwrap() + .timestamp_nanos_opt() + .unwrap() as Timestamp; + assert_eq!( + TimePeriod::Week.previous_period(timestamp), + Some(previous_week) + ); + + let previous_month = Utc + .with_ymd_and_hms(2023, 5, 20, 0, 0, 0) + .unwrap() + .timestamp_nanos_opt() + .unwrap() as Timestamp; + assert_eq!( + TimePeriod::Month.previous_period(timestamp), + Some(previous_month) + ); + + let previous_quarter = Utc + .with_ymd_and_hms(2023, 3, 20, 0, 0, 0) + .unwrap() + .timestamp_nanos_opt() + .unwrap() as Timestamp; + assert_eq!( + TimePeriod::Quarter.previous_period(timestamp), + Some(previous_quarter) + ); + + let previous_year = Utc + .with_ymd_and_hms(2022, 6, 20, 0, 0, 0) + .unwrap() + .timestamp_nanos_opt() + .unwrap() as Timestamp; + assert_eq!( + TimePeriod::Year.previous_period(timestamp), + Some(previous_year) + ); + + assert_eq!(TimePeriod::AllTime.previous_period(timestamp), None); + } + + #[test] + fn test_timestamp_to_day_string() { + let timestamp = Utc + .with_ymd_and_hms(2023, 6, 20, 0, 0, 0) + .unwrap() + .timestamp_nanos_opt() + .unwrap() as Timestamp; + assert_eq!(timestamp_to_day_string(timestamp), "20062023"); + } + + #[test] + fn test_timestamp_to_week_string() { + let timestamp = Utc + .with_ymd_and_hms(2023, 6, 20, 0, 0, 0) + .unwrap() + .timestamp_nanos_opt() + .unwrap() as Timestamp; + assert_eq!(timestamp_to_week_string(timestamp), "2023W25"); + } + + #[test] + fn test_timestamp_to_quarter_string() { + let timestamp = Utc + .with_ymd_and_hms(2023, 6, 20, 0, 0, 0) + .unwrap() + .timestamp_nanos_opt() + .unwrap() as Timestamp; + assert_eq!(timestamp_to_quarter_string(timestamp), "2023Q2"); + } + + #[test] + fn test_timestamp_to_month_string() { + let timestamp = Utc + .with_ymd_and_hms(2023, 6, 20, 0, 0, 0) + .unwrap() + .timestamp_nanos_opt() + .unwrap() as Timestamp; + assert_eq!(timestamp_to_month_string(timestamp), "062023"); + } + + #[test] + fn test_end_period() { + let timestamp = Utc + .with_ymd_and_hms(2023, 5, 16, 5, 13, 0) + .unwrap() + .timestamp_nanos_opt() + .unwrap() as Timestamp; + + let end_day = Utc.with_ymd_and_hms(2023, 5, 17, 0, 0, 0).unwrap(); + assert_eq!(TimePeriod::Day.end_period(timestamp), Some(end_day)); + + let end_week = Utc.with_ymd_and_hms(2023, 5, 22, 0, 0, 0).unwrap(); + assert_eq!(TimePeriod::Week.end_period(timestamp), Some(end_week)); + + let end_month = Utc.with_ymd_and_hms(2023, 6, 1, 0, 0, 0).unwrap(); + assert_eq!(TimePeriod::Month.end_period(timestamp), Some(end_month)); + + let end_quarter = Utc.with_ymd_and_hms(2023, 7, 1, 0, 0, 0).unwrap(); + assert_eq!(TimePeriod::Quarter.end_period(timestamp), Some(end_quarter)); + + let end_year = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(); + assert_eq!(TimePeriod::Year.end_period(timestamp), Some(end_year)); + } + + #[test] + fn test_start_period() { + let timestamp = Utc + .with_ymd_and_hms(2023, 5, 16, 5, 13, 0) + .unwrap() + .timestamp_nanos_opt() + .unwrap() as Timestamp; + + let start_day = Utc.with_ymd_and_hms(2023, 5, 16, 0, 0, 0).unwrap(); + assert_eq!(TimePeriod::Day.start_period(timestamp), Some(start_day)); + + let start_week = Utc.with_ymd_and_hms(2023, 5, 15, 0, 0, 0).unwrap(); + assert_eq!(TimePeriod::Week.start_period(timestamp), Some(start_week)); + + let start_month = Utc.with_ymd_and_hms(2023, 5, 1, 0, 0, 0).unwrap(); + assert_eq!(TimePeriod::Month.start_period(timestamp), Some(start_month)); + + let start_quarter = Utc.with_ymd_and_hms(2023, 4, 1, 0, 0, 0).unwrap(); + assert_eq!( + TimePeriod::Quarter.start_period(timestamp), + Some(start_quarter) + ); + + let start_year = Utc.with_ymd_and_hms(2023, 1, 1, 0, 0, 0).unwrap(); + assert_eq!(TimePeriod::Year.start_period(timestamp), Some(start_year)); + } +}