1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
use super::Contest;
use reqwest::blocking::Client;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::convert::TryFrom;
#[allow(non_snake_case)]
#[derive(Serialize, Deserialize)]
#[serde(tag = "status")]
enum CFResponse<T> {
OK { result: T },
FAILED { comment: String },
}
#[allow(non_snake_case)]
#[derive(Serialize, Deserialize)]
struct CFRatingChange {
contestId: usize,
contestName: String,
handle: String,
rank: usize,
ratingUpdateTimeSeconds: u64,
oldRating: i32,
newRating: i32,
}
impl TryFrom<Vec<CFRatingChange>> for Contest {
type Error = String;
fn try_from(json_contest: Vec<CFRatingChange>) -> Result<Self, Self::Error> {
let first_change = json_contest.get(0).ok_or("Empty standings")?;
let id = first_change.contestId;
let name = first_change.contestName.clone();
let time_seconds = first_change.ratingUpdateTimeSeconds;
let mut lo_rank = json_contest.len() + 1;
let mut hi_rank = json_contest.len() + 1;
let mut seen_handles = HashMap::with_capacity(json_contest.len());
let mut standings = Vec::with_capacity(json_contest.len());
for (i, mut change) in json_contest.into_iter().enumerate().rev() {
if id != change.contestId {
return Err(format!(
"Inconsistent contests ids {} and {}",
id, change.contestId
));
}
if name != change.contestName {
return Err(format!(
"Inconsistent contest names {} and {}",
name, change.contestName
));
}
if time_seconds != change.ratingUpdateTimeSeconds {
eprintln!(
"WARNING @ {}: Inconsistent contest times {} and {}",
id, time_seconds, change.ratingUpdateTimeSeconds
);
}
while let Some(j) = seen_handles.insert(change.handle.clone(), i) {
if !(id == 447 || id == 472 || id == 615) {
return Err(format!(
"Duplicate user {} at positions {} and {}",
change.handle, i, j
));
}
eprintln!(
"WARNING @ {}: duplicate user {} at positions {} and {}",
id, change.handle, i, j
);
change.handle += "_clone";
}
if lo_rank == change.rank {
if !(lo_rank < i + 2 && i < hi_rank) {
return Err(format!(
"Position {} is not between ranks {} and {}",
i + 1,
lo_rank,
hi_rank
));
}
} else {
if !(change.rank < lo_rank && lo_rank == i + 2) {
return Err(format!("Invalid start of rank {}", lo_rank));
}
hi_rank = lo_rank;
lo_rank = change.rank;
}
standings.push((change.handle, lo_rank - 1, hi_rank - 2));
}
standings.reverse();
Ok(Self {
id,
name,
time_seconds,
standings,
weight: 1.0,
})
}
}
pub fn fetch_cf_contest(client: &Client, contest_id: usize) -> Contest {
let url = format!(
"https://codeforces.com/api/contest.ratingChanges?contestId={}",
contest_id
);
let response = client
.get(&url)
.send()
.expect("HTTP error: is Codeforces.com down?");
if !response.status().is_success() {
eprintln!("HTTP status {}: is Codeforces.com down?", response.status());
}
let packet: CFResponse<Vec<CFRatingChange>> = response
.json()
.expect("Codeforces API response doesn't match the expected JSON schema");
match packet {
CFResponse::OK { result } => {
TryFrom::try_from(result).expect("Failed to parse JSON response as a valid Contest")
}
CFResponse::FAILED { comment } => panic!(comment),
}
}