scores

football-data.org terminal interface
Log | Files | Refs | README | LICENSE

display.rs (9276B)


      1 use crate::models::{Match, StandingsResponse};
      2 use chrono::Local;
      3 use prettytable::{Cell, Row, Table, format};
      4 
      5 const STATUS_ICONS: &[(&str, &str)] = &[("FINISHED", "✓"), ("SCHEDULED", "⏰"), ("IN_PLAY", "▶")];
      6 
      7 const DEFAULT_STATUS_ICON: &str = "•";
      8 
      9 pub fn filter_matches(
     10     matches: Vec<Match>,
     11     team_filter: Option<&str>,
     12     show_all: bool,
     13 ) -> Vec<Match> {
     14     let today_start = Local::now()
     15         .date_naive()
     16         .and_hms_opt(0, 0, 0)
     17         .unwrap()
     18         .and_local_timezone(Local)
     19         .unwrap();
     20 
     21     matches
     22         .into_iter()
     23         .filter(|m| {
     24             if !show_all {
     25                 if let Some(mt) = m.utc_date {
     26                     if mt < today_start {
     27                         return false;
     28                     }
     29                 } else {
     30                     return false;
     31                 }
     32             }
     33 
     34             if let Some(team) = team_filter {
     35                 let team_lower = team.to_lowercase();
     36                 let home_match = m
     37                     .home_team
     38                     .name
     39                     .as_ref()
     40                     .is_some_and(|n| n.to_lowercase().contains(&team_lower))
     41                     || m.home_team
     42                         .tla
     43                         .as_ref()
     44                         .is_some_and(|t| t.to_lowercase().contains(&team_lower));
     45                 let away_match = m
     46                     .away_team
     47                     .name
     48                     .as_ref()
     49                     .is_some_and(|n| n.to_lowercase().contains(&team_lower))
     50                     || m.away_team
     51                         .tla
     52                         .as_ref()
     53                         .is_some_and(|t| t.to_lowercase().contains(&team_lower));
     54 
     55                 home_match || away_match
     56             } else {
     57                 true
     58             }
     59         })
     60         .collect()
     61 }
     62 
     63 pub fn display_table(matches: &[Match], show_competition: bool) {
     64     if matches.is_empty() {
     65         println!("No matches found.");
     66         return;
     67     }
     68 
     69     let mut table = Table::new();
     70     table.set_format(*format::consts::FORMAT_BOX_CHARS);
     71 
     72     let mut header = vec![
     73         Cell::new("MD").style_spec("Fb"),
     74         Cell::new("Date & Time").style_spec("Fb"),
     75     ];
     76     if show_competition {
     77         header.push(Cell::new("Competition").style_spec("Fb"));
     78     }
     79     header.extend([
     80         Cell::new("Home Team").style_spec("Fb"),
     81         Cell::new("Score").style_spec("Fb"),
     82         Cell::new("Away Team").style_spec("Fb"),
     83         Cell::new("Status").style_spec("Fb"),
     84     ]);
     85     table.add_row(Row::new(header));
     86 
     87     for m in matches {
     88         let matchday = m
     89             .matchday
     90             .map_or_else(|| "-".to_string(), |md| md.to_string());
     91 
     92         let date = m.utc_date.map_or_else(
     93             || "TBD".to_string(),
     94             |dt| {
     95                 dt.with_timezone(&Local)
     96                     .format("%b %d, %Y %H:%M %:z")
     97                     .to_string()
     98             },
     99         );
    100 
    101         let score = match (m.score.full_time.home, m.score.full_time.away) {
    102             (Some(h), Some(a)) => format!("{h} - {a}"),
    103             _ => "vs".to_string(),
    104         };
    105 
    106         let status = m.status.as_deref().unwrap_or("UNKNOWN");
    107         let status_icon = STATUS_ICONS
    108             .iter()
    109             .find(|(s, _)| *s == status)
    110             .map_or(DEFAULT_STATUS_ICON, |(_, icon)| *icon);
    111 
    112         let home_team = m.home_team.name.as_deref().unwrap_or("Unknown");
    113         let away_team = m.away_team.name.as_deref().unwrap_or("Unknown");
    114 
    115         let mut cells = vec![Cell::new(&matchday), Cell::new(&date)];
    116         if show_competition {
    117             let competition = m
    118                 .competition
    119                 .as_ref()
    120                 .and_then(|c| c.name.as_deref())
    121                 .unwrap_or("-");
    122             cells.push(Cell::new(competition));
    123         }
    124         cells.extend([
    125             Cell::new(home_team).style_spec("r"),
    126             Cell::new(&score).style_spec("Fyc"),
    127             Cell::new(away_team).style_spec("l"),
    128             Cell::new(&format!("{status_icon} {status}")),
    129         ]);
    130         table.add_row(Row::new(cells));
    131     }
    132 
    133     table.printstd();
    134 }
    135 
    136 pub fn display_standings(standings_response: StandingsResponse) {
    137     let mut printed = false;
    138     for standing in standings_response.standings {
    139         if standing.table.is_empty() || printed {
    140             continue;
    141         }
    142 
    143         let mut table = Table::new();
    144         table.set_format(*format::consts::FORMAT_BOX_CHARS);
    145 
    146         table.add_row(Row::new(vec![
    147             Cell::new("Pos").style_spec("Fb"),
    148             Cell::new("Team").style_spec("Fb"),
    149             Cell::new("P").style_spec("Fb"),
    150             Cell::new("W").style_spec("Fb"),
    151             Cell::new("D").style_spec("Fb"),
    152             Cell::new("L").style_spec("Fb"),
    153             Cell::new("GF").style_spec("Fb"),
    154             Cell::new("GA").style_spec("Fb"),
    155             Cell::new("GD").style_spec("Fb"),
    156             Cell::new("Pts").style_spec("Fb"),
    157         ]));
    158 
    159         for entry in &standing.table {
    160             let gd_str = if entry.goal_difference >= 0 {
    161                 format!("+{}", entry.goal_difference)
    162             } else {
    163                 entry.goal_difference.to_string()
    164             };
    165 
    166             table.add_row(Row::new(vec![
    167                 Cell::new(&entry.position.to_string()).style_spec("c"),
    168                 Cell::new(&entry.team.name),
    169                 Cell::new(&entry.played_games.to_string()).style_spec("c"),
    170                 Cell::new(&entry.won.to_string()).style_spec("c"),
    171                 Cell::new(&entry.draw.to_string()).style_spec("c"),
    172                 Cell::new(&entry.lost.to_string()).style_spec("c"),
    173                 Cell::new(&entry.goals_for.to_string()).style_spec("c"),
    174                 Cell::new(&entry.goals_against.to_string()).style_spec("c"),
    175                 Cell::new(&gd_str).style_spec("c"),
    176                 Cell::new(&entry.points.to_string()).style_spec("Fbc"),
    177             ]));
    178         }
    179 
    180         table.printstd();
    181         printed = true;
    182     }
    183 }
    184 
    185 #[cfg(test)]
    186 mod tests {
    187     use super::*;
    188     use crate::models::{Score, ScoreDetail, Team};
    189     use chrono::{TimeZone, Utc};
    190 
    191     fn create_match(date: Option<chrono::DateTime<Utc>>, home: &str, away: &str) -> Match {
    192         Match {
    193             utc_date: date,
    194             status: Some("SCHEDULED".to_string()),
    195             matchday: Some(1),
    196             home_team: Team {
    197                 name: Some(home.to_string()),
    198                 tla: Some(home[..3].to_string()),
    199             },
    200             away_team: Team {
    201                 name: Some(away.to_string()),
    202                 tla: Some(away[..3].to_string()),
    203             },
    204             score: Score {
    205                 full_time: ScoreDetail::default(),
    206             },
    207             competition: None,
    208         }
    209     }
    210 
    211     #[test]
    212     fn test_filter_matches_empty() {
    213         let matches = vec![];
    214         let result = filter_matches(matches, None, false);
    215         assert!(result.is_empty());
    216     }
    217 
    218     #[test]
    219     fn test_filter_matches_no_filter_show_all() {
    220         let past = Utc.with_ymd_and_hms(2020, 1, 1, 12, 0, 0).unwrap();
    221         let future = Utc.with_ymd_and_hms(2030, 1, 1, 12, 0, 0).unwrap();
    222 
    223         let matches = vec![
    224             create_match(Some(past), "Arsenal", "Chelsea"),
    225             create_match(Some(future), "Liverpool", "Man City"),
    226         ];
    227 
    228         let result = filter_matches(matches, None, true);
    229         assert_eq!(result.len(), 2);
    230     }
    231 
    232     #[test]
    233     fn test_filter_matches_no_filter_show_future_only() {
    234         let past = Utc.with_ymd_and_hms(2020, 1, 1, 12, 0, 0).unwrap();
    235         let future = Utc.with_ymd_and_hms(2030, 1, 1, 12, 0, 0).unwrap();
    236 
    237         let matches = vec![
    238             create_match(Some(past), "Arsenal", "Chelsea"),
    239             create_match(Some(future), "Liverpool", "Man City"),
    240         ];
    241 
    242         let result = filter_matches(matches, None, false);
    243         assert_eq!(result.len(), 1);
    244         assert_eq!(result[0].home_team.name, Some("Liverpool".to_string()));
    245     }
    246 
    247     #[test]
    248     fn test_filter_matches_with_team_filter() {
    249         let future = Utc.with_ymd_and_hms(2030, 1, 1, 12, 0, 0).unwrap();
    250 
    251         let matches = vec![
    252             create_match(Some(future), "Arsenal", "Chelsea"),
    253             create_match(Some(future), "Liverpool", "Arsenal"),
    254             create_match(Some(future), "Man City", "Chelsea"),
    255         ];
    256 
    257         let result = filter_matches(matches, Some("Arsenal"), true);
    258         assert_eq!(result.len(), 2);
    259     }
    260 
    261     #[test]
    262     fn test_filter_matches_team_filter_case_insensitive() {
    263         let future = Utc.with_ymd_and_hms(2030, 1, 1, 12, 0, 0).unwrap();
    264 
    265         let matches = vec![create_match(Some(future), "Arsenal", "Chelsea")];
    266 
    267         let result = filter_matches(matches.clone(), Some("arsenal"), true);
    268         assert_eq!(result.len(), 1);
    269 
    270         let result = filter_matches(matches, Some("ARSENAL"), true);
    271         assert_eq!(result.len(), 1);
    272     }
    273 
    274     #[test]
    275     fn test_filter_matches_team_filter_by_tla() {
    276         let future = Utc.with_ymd_and_hms(2030, 1, 1, 12, 0, 0).unwrap();
    277 
    278         let matches = vec![create_match(Some(future), "Arsenal", "Chelsea")];
    279 
    280         let result = filter_matches(matches, Some("ARS"), true);
    281         assert_eq!(result.len(), 1);
    282     }
    283 }