multi_skill/data_processing/
cf_api.rs1use super::Contest;
2use reqwest::blocking::Client;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::convert::TryFrom;
6
7#[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#[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 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 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 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
124pub 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}