multi_skill/data_processing/
cf_api.rs

1use super::Contest;
2use reqwest::blocking::Client;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::convert::TryFrom;
6
7/// General response from the Codeforces API.
8/// Codeforces documentation: https://codeforces.com/apiHelp
9#[allow(non_snake_case)]
10#[derive(Serialize, Deserialize)]
11#[serde(tag = "status")]
12enum CFResponse<T> {
13    OK { result: T },
14    FAILED { comment: String },
15}
16
17/// A RatingChange object from the Codeforces API.
18/// Codeforces documentation: https://codeforces.com/apiHelp/objects#RatingChange
19#[serde(rename_all = "camelCase")]
20#[derive(Serialize, Deserialize)]
21struct CFRatingChange {
22    contest_id: usize,
23    contest_name: String,
24    handle: String,
25    rank: usize,
26    rating_update_time_seconds: u64,
27    old_rating: i32,
28    new_rating: i32,
29}
30
31fn codeforces_human_url(contest_id: usize) -> String {
32    format!("https://codeforces.com/contest/{}/standings", contest_id)
33}
34
35fn codeforces_api_url(contest_id: usize) -> String {
36    format!(
37        "https://codeforces.com/api/contest.ratingChanges?contestId={}",
38        contest_id
39    )
40}
41
42impl TryFrom<Vec<CFRatingChange>> for Contest {
43    type Error = String;
44
45    /// Checks the integrity of our API response and convert it into a more convenient format.
46    fn try_from(json_contest: Vec<CFRatingChange>) -> Result<Self, Self::Error> {
47        let first_change = json_contest.get(0).ok_or("Empty standings")?;
48        let id = first_change.contest_id;
49        let name = first_change.contest_name.clone();
50        let time_seconds = first_change.rating_update_time_seconds;
51
52        let mut lo_rank = json_contest.len() + 1;
53        let mut hi_rank = json_contest.len() + 1;
54        let mut seen_handles = HashMap::with_capacity(json_contest.len());
55        let mut standings = Vec::with_capacity(json_contest.len());
56
57        for (i, mut change) in json_contest.into_iter().enumerate().rev() {
58            if id != change.contest_id {
59                return Err(format!(
60                    "Inconsistent contests ids {} and {}",
61                    id, change.contest_id
62                ));
63            }
64            if name != change.contest_name {
65                return Err(format!(
66                    "Inconsistent contest names {} and {}",
67                    name, change.contest_name
68                ));
69            }
70            if time_seconds != change.rating_update_time_seconds {
71                // I don't know why but contests 61,318,347,373,381,400,404,405
72                // each contain one discrepancy, usually 4 hours late
73                eprintln!(
74                    "WARNING @ {}: Inconsistent contest times {} and {}",
75                    id, time_seconds, change.rating_update_time_seconds
76                );
77            }
78            while let Some(j) = seen_handles.insert(change.handle.clone(), i) {
79                // I don't know why but contests 447,472,615 have duplicate users
80                if !(id == 447 || id == 472 || id == 615) {
81                    return Err(format!(
82                        "Duplicate user {} at positions {} and {}",
83                        change.handle, i, j
84                    ));
85                }
86                eprintln!(
87                    "WARNING @ {}: duplicate user {} at positions {} and {}",
88                    id, change.handle, i, j
89                );
90                change.handle += "_clone";
91            }
92
93            if lo_rank == change.rank {
94                if !(lo_rank < i + 2 && i < hi_rank) {
95                    return Err(format!(
96                        "Position {} is not between ranks {} and {}",
97                        i + 1,
98                        lo_rank,
99                        hi_rank
100                    ));
101                }
102            } else {
103                if !(change.rank < lo_rank && lo_rank == i + 2) {
104                    return Err(format!("Invalid start of rank {}", lo_rank));
105                }
106                hi_rank = lo_rank;
107                lo_rank = change.rank;
108            }
109
110            standings.push((change.handle, lo_rank - 1, hi_rank - 2));
111        }
112        standings.reverse();
113
114        Ok(Self {
115            name,
116            url: Some(codeforces_human_url(id)),
117            weight: 1.0,
118            time_seconds,
119            standings,
120        })
121    }
122}
123
124/// Retrieve a contest with a particular ID. If there's a cached entry with the same name in the
125/// json/ directly, that will be used. This way, you can process your own custom contests.
126/// If there is no cached entry, this function will attempt to retrieve one from Codeforces.
127/// Codeforces documentation: https://codeforces.com/apiHelp/methods#contest.ratingChanges
128pub fn fetch_cf_contest(client: &Client, contest_id: usize) -> Contest {
129    let response = client
130        .get(&codeforces_api_url(contest_id))
131        .send()
132        .expect("Connection error: is Codeforces.com down?")
133        .error_for_status()
134        .expect("Status error: is Codeforces.com down?");
135    let packet: CFResponse<Vec<CFRatingChange>> = response
136        .json()
137        .expect("Codeforces API response doesn't match the expected JSON schema");
138    match packet {
139        CFResponse::OK { result } => {
140            TryFrom::try_from(result).expect("Failed to parse JSON response as a valid Contest")
141        }
142        CFResponse::FAILED { comment } => panic!(comment),
143    }
144}