1#![allow(clippy::cast_precision_loss)]
5
6use crate::{
7 error::{Err, Result},
8 pieces::Color::{self, Black, White},
9};
10use compact_str::{format_compact, CompactString};
11use std::{collections::BTreeMap, str::FromStr};
12
13#[derive(Clone, Debug)]
15pub struct Timers {
16 pub white: Timer,
17 pub black: Timer,
18}
19
20impl Timers {
21 #[must_use]
23 pub fn new(allocated: u32, increment: u32) -> Self {
24 Self {
25 white: Timer::new(allocated, increment),
26 black: Timer::new(allocated, increment),
27 }
28 }
29
30 pub fn pgn_tag(&self) -> Result<String> {
36 if self.white.allocated != self.black.allocated
37 || self.white.increment != self.black.increment
38 {
39 return Err(Err::DifferentTimeControlsError(
40 self.white.clone(),
41 self.black.clone(),
42 ));
43 }
44
45 Ok(self.white.pgn_tag())
46 }
47
48 #[must_use]
50 pub const fn get(&self, color: Color) -> &Timer {
51 match color {
52 White => &self.white,
53 Black => &self.black,
54 }
55 }
56
57 pub fn get_mut(&mut self, color: Color) -> &mut Timer {
59 match color {
60 White => &mut self.white,
61 Black => &mut self.black,
62 }
63 }
64
65 #[must_use]
67 pub fn get_history(&self, ply_number: &u16) -> Option<&f32> {
68 self.white
69 .history
70 .get(ply_number)
71 .or_else(|| self.black.history.get(ply_number))
72 }
73}
74
75#[derive(Clone)]
79pub struct Timer {
80 pub allocated: u32,
81 pub increment: u32,
82 pub remaining: f32,
83 pub history: BTreeMap<u16, f32>,
86}
87
88impl core::fmt::Debug for Timer {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 write!(
91 f,
92 "Timer(allocated={}, increment={}, remaining={})",
93 self.allocated, self.increment, self.remaining
94 )
95 }
96}
97
98impl Timer {
99 #[must_use]
101 pub const fn new(allocated: u32, increment: u32) -> Self {
102 Self {
103 allocated,
104 increment,
105 remaining: allocated as f32,
106 history: BTreeMap::new(),
107 }
108 }
109
110 pub fn from_pgn_tag(controls: &str) -> Result<Self> {
116 let (allocated, increment) = Self::read_control(controls)?;
117
118 Ok(Self::new(allocated, increment))
119 }
120
121 #[must_use]
123 pub fn pgn_tag(&self) -> String {
124 let mut output = format!("{}", self.allocated);
125
126 if self.increment > 0 {
127 output.push_str(&format!("+{}", self.increment));
128 }
129
130 output
131 }
132
133 pub fn zero(&mut self) {
135 self.remaining = 0.0;
136 }
137
138 pub fn read_control(controls: &str) -> Result<(u32, u32)> {
144 let allocated: &str;
145 let increment: &str;
146
147 if let Some(tup) = controls.split_once('+') {
148 (allocated, increment) = tup;
149 } else {
150 (allocated, increment) = (controls, "0");
151 }
152
153 let Ok(allocated) = allocated.parse() else {
154 return Err(Err::ParseError("allocated", allocated.into()));
155 };
156 let Ok(increment) = increment.parse() else {
157 return Err(Err::ParseError("increment", increment.into()));
158 };
159
160 Ok((allocated, increment))
161 }
162
163 pub fn update(&mut self, ply_number: u16, update: Update) {
165 match update {
166 Update::ElapsedInMove(elapsed) => {
167 self.remaining -= elapsed;
168
169 if self.remaining > 0.0 {
170 self.remaining += self.increment as f32;
171 } else if self.remaining < 0.0 {
172 self.remaining = 0.0;
173 }
174 }
175 Update::Remaining(remaining) => self.remaining = remaining,
176 }
177
178 self.history.insert(ply_number, self.remaining);
179 }
180
181 #[must_use]
183 pub fn annotation(&self) -> CompactString {
184 Update::write_annotation(self.remaining)
185 }
186
187 #[must_use]
189 pub fn timeout(&self) -> bool {
190 self.remaining == 0.0
191 }
192}
193
194impl FromStr for Timer {
195 type Err = Err;
196
197 fn from_str(s: &str) -> std::prelude::v1::Result<Self, Self::Err> {
198 Self::from_pgn_tag(s)
199 }
200}
201
202impl TryFrom<&str> for Timer {
203 type Error = Err;
204
205 fn try_from(value: &str) -> std::prelude::v1::Result<Self, Self::Error> {
206 Self::from_pgn_tag(value)
207 }
208}
209
210#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
213pub enum Update {
214 ElapsedInMove(f32),
215 Remaining(f32),
216}
217
218impl Update {
219 pub fn read_annotation(annotation: &str) -> Result<Self> {
225 let mut as_str = annotation;
226 if let Some(stripped) = as_str.strip_prefix("[%clk ") {
227 as_str = stripped;
228 }
229 if let Some(stripped) = as_str.strip_suffix(']') {
230 as_str = stripped;
231 }
232 let mut split_str = as_str.split(':');
233
234 let Some(hours_str) = split_str.next() else {
235 return Err(Err::ParseError("timer annotation", annotation.into()));
236 };
237 let Ok(hours) = hours_str.parse::<f32>() else {
238 return Err(Err::ParseError("timer annotation", annotation.into()));
239 };
240
241 let Some(minutes_str) = split_str.next() else {
242 return Err(Err::ParseError("timer annotation", annotation.into()));
243 };
244 let Ok(minutes) = minutes_str.parse::<f32>() else {
245 return Err(Err::ParseError("timer annotation", annotation.into()));
246 };
247
248 let Some(seconds_str) = split_str.next() else {
249 return Err(Err::ParseError("timer annotation", annotation.into()));
250 };
251 let Ok(seconds) = seconds_str.parse::<f32>() else {
252 return Err(Err::ParseError("timer annotation", annotation.into()));
253 };
254
255 Ok(Self::Remaining(
256 hours.mul_add(3600.0, minutes * 60.0) + seconds,
257 ))
258 }
259
260 #[must_use]
262 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
263 pub fn write_annotation(time: f32) -> CompactString {
264 let mut parts: f32 = time;
265
266 let hours = (parts / 3600.0).floor() as usize;
267 parts %= 3600.0;
268
269 let minutes = (parts / 60.0).floor() as u8;
270 parts %= 60.0;
271
272 format_compact!("[%clk {hours}:{minutes:02}:{parts:04.1}]")
273 }
274
275 #[must_use]
277 pub const fn value(&self) -> f32 {
278 let (Self::ElapsedInMove(r) | Self::Remaining(r)) = self;
279 *r
280 }
281}
282
283#[cfg(test)]
284#[allow(clippy::float_cmp)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn update_timer() {
290 let mut timer = Timer::new(400, 20);
291 timer.update(5, Update::ElapsedInMove(53.456));
292 assert_eq!(timer.remaining, 366.544);
293 }
294
295 #[test]
296 fn make_annotation() {
297 let mut timer = Timer::new(400, 20);
298 timer.update(5, Update::ElapsedInMove(53.456));
299 assert_eq!("[%clk 0:06:06.5]", timer.annotation());
300 }
301
302 #[test]
303 fn test_read_annotation() {
304 assert_eq!(
305 [366.5, 11532.2, 7203.2, 0.0],
306 [
307 "[%clk 0:06:06.5]",
308 "3:12:12.2",
309 "[%clk 2:00:03.2]",
310 "0:00:00.0",
311 ]
312 .map(|ann| {
313 let Update::Remaining(val) = Update::read_annotation(ann).unwrap() else {
314 panic!("Expected Update::Remaining")
315 };
316
317 val
318 })
319 );
320 }
321
322 #[test]
323 fn test_write_annotation() {
324 assert_eq!(
325 [366.5, 11532.2, 7203.2, 0.0].map(Update::write_annotation),
326 [
327 "[%clk 0:06:06.5]",
328 "[%clk 3:12:12.2]",
329 "[%clk 2:00:03.2]",
330 "[%clk 0:00:00.0]",
331 ]
332 );
333 }
334}