Skip to main content

horfimbor_time/
lib.rs

1#![deny(missing_docs)]
2#![doc = include_str!("../README.md")]
3
4use chrono::{DateTime, Duration, Utc};
5use core::ops::Add;
6use serde::{Deserialize, Serialize};
7use std::ops::{Mul, Sub};
8use thiserror::Error;
9
10/// `HfTime` can fail to construct.
11#[derive(Error, Debug)]
12pub enum HfTimeError {
13    /// in game time must be slower than real time
14    #[error("loop length must be greater than length and non zero")]
15    InvalidLength,
16}
17
18/// `HfTimeConfiguration` can be invalid.
19#[derive(Error, Debug)]
20pub enum HfTimeConfigurationError {
21    /// in game time must be slower than real time
22    #[error("start date is out of bound")]
23    InvalidStartDate,
24}
25
26/// the in-game time is just a wrapper around an integer representing the milliseconds
27/// since the beginning of the game
28#[derive(Copy, Clone, Debug)]
29pub struct HfDuration {
30    value: i64,
31}
32
33impl HfDuration {
34    /// the baseline for a web game is the millisecond
35    #[must_use]
36    pub const fn from_milliseconds(value: i64) -> Self {
37        Self { value }
38    }
39
40    /// can be easier to work with seconds
41    #[must_use]
42    pub const fn from_seconds(value: i64) -> Self {
43        Self {
44            value: value * 1000,
45        }
46    }
47
48    /// the baseline for a web game is the millisecond
49    #[must_use]
50    pub const fn as_milliseconds(self) -> i64 {
51        self.value
52    }
53
54    /// can be easier to work with seconds
55    #[must_use]
56    pub const fn as_seconds(self) -> i64 {
57        self.value / 1000
58    }
59
60    /// function to match Duration api
61    #[must_use]
62    pub const fn num_seconds(self) -> i64 {
63        self.value / 1000
64    }
65
66    /// function to match Duration api
67    #[must_use]
68    pub const fn num_minutes(self) -> i64 {
69        self.num_seconds() / 60
70    }
71
72    /// function to match Duration api
73    #[must_use]
74    pub const fn num_hours(self) -> i64 {
75        self.num_minutes() / 60
76    }
77}
78
79impl Add<Self> for HfDuration {
80    type Output = Self;
81
82    fn add(self, rhs: Self) -> Self::Output {
83        Self {
84            value: self.value + rhs.value,
85        }
86    }
87}
88
89impl Sub<Self> for HfDuration {
90    type Output = Self;
91
92    fn sub(self, rhs: Self) -> Self::Output {
93        Self {
94            value: self.value - rhs.value,
95        }
96    }
97}
98
99impl Mul<i64> for HfDuration {
100    type Output = i64;
101
102    fn mul(self, rhs: i64) -> Self::Output {
103        self.value * rhs
104    }
105}
106
107/// configuration is shared across all service for the same server
108/// it defines how long the game is up and when it started
109#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
110pub struct HfTimeConfiguration {
111    start_time: i64,
112    irl_length: i64,
113    ig_length: i64,
114}
115
116impl Default for HfTimeConfiguration {
117    fn default() -> Self {
118        Self {
119            start_time: 0,
120            irl_length: 1_000_000,
121            ig_length: 100,
122        }
123    }
124}
125
126impl HfTimeConfiguration {
127    /// # Errors
128    ///
129    /// Will return `Err` if configuration is invalid
130    pub fn new(
131        irl_length: Duration,
132        ig_length: Duration,
133        start_time: DateTime<Utc>,
134    ) -> Result<Self, HfTimeError> {
135        if irl_length.le(&ig_length) || irl_length.is_zero() || ig_length.is_zero() {
136            return Err(HfTimeError::InvalidLength);
137        }
138
139        Ok(Self {
140            start_time: start_time.timestamp_millis(),
141            irl_length: irl_length.num_milliseconds(),
142            ig_length: ig_length.num_milliseconds(),
143        })
144    }
145
146    /// get start date as UTC value
147    /// # Errors
148    ///
149    /// Will return `Err` if the start date cannot be converted to UTC
150    pub fn start_time(&self) -> Result<DateTime<Utc>, HfTimeConfigurationError> {
151        DateTime::from_timestamp_millis(self.start_time)
152            .ok_or(HfTimeConfigurationError::InvalidStartDate)
153    }
154
155    /// get irl duration in milliseconds
156    #[must_use]
157    pub const fn irl_length(&self) -> i64 {
158        self.irl_length
159    }
160
161    /// get in game duration in milliseconds
162    #[must_use]
163    pub const fn ig_length(&self) -> i64 {
164        self.ig_length
165    }
166
167    /// return the in game time between 2 irl datetime
168    #[must_use]
169    pub fn diff_hf_millis(&self, start: DateTime<Utc>, end: DateTime<Utc>) -> HfDuration {
170        let start = HfTime::new(start, *self);
171        let end = HfTime::new(end, *self);
172
173        end.as_hf_duration() - start.as_hf_duration()
174    }
175}
176
177/// `HfTime` allow to convert in-game time and irl time based on a config
178#[derive(Debug)]
179pub struct HfTime {
180    time: i64,
181    config: HfTimeConfiguration,
182}
183
184/// `HfStatus` return the current status of the time, and the duration until the switch
185pub enum HfStatus {
186    /// the game is paused
187    Paused,
188    /// the game time is running
189    Running,
190}
191
192impl HfTime {
193    /// it is possible to create an `HfTime` from any point in time
194    #[must_use]
195    pub const fn new(time: DateTime<Utc>, config: HfTimeConfiguration) -> Self {
196        Self {
197            time: time.timestamp_millis() - config.start_time,
198            config,
199        }
200    }
201
202    /// reduce the boilerplate
203    #[must_use]
204    pub fn now(config: HfTimeConfiguration) -> Self {
205        let start = Utc::now();
206        Self::new(start, config)
207    }
208
209    /// return the irl time since the beginning.config
210    #[must_use]
211    const fn as_millis(&self) -> i64 {
212        self.time
213    }
214
215    /// allow to display when an event will finnish
216    #[must_use]
217    pub const fn as_datetime(&self) -> Option<DateTime<Utc>> {
218        DateTime::from_timestamp_millis(self.time + self.config.start_time)
219    }
220
221    /// return the time passed when the game is up since the beginning.config
222    #[must_use]
223    pub const fn as_duration(&self) -> Duration {
224        Duration::milliseconds(self.as_millis())
225    }
226
227    /// return the time passed when the game is up since the beginning.config
228    #[must_use]
229    pub const fn as_hf_duration(&self) -> HfDuration {
230        HfDuration {
231            value: self.as_hf_millis(),
232        }
233    }
234
235    /// return the status with duration before change
236    #[must_use]
237    pub const fn hf_status(&self) -> (HfStatus, Duration) {
238        let rest = self.time % self.config.irl_length;
239
240        if rest > self.config.ig_length {
241            return (
242                HfStatus::Paused,
243                Duration::milliseconds(self.config.irl_length - rest),
244            );
245        }
246        (
247            HfStatus::Running,
248            Duration::milliseconds(self.config.ig_length - rest),
249        )
250    }
251
252    /// return duration and hfDuration before date
253    #[must_use]
254    pub fn remaining(&self, until: DateTime<Utc>) -> (Duration, HfDuration) {
255        let end = Self::new(until, self.config);
256
257        (
258            end.as_duration() - self.as_duration(),
259            end.as_hf_duration() - self.as_hf_duration(),
260        )
261    }
262
263    const fn as_hf_millis(&self) -> i64 {
264        let nb_loop = self.time / self.config.irl_length;
265        let rest = self.time % self.config.irl_length;
266        if rest > self.config.ig_length {
267            return (nb_loop + 1) * self.config.ig_length;
268        }
269        nb_loop * self.config.ig_length + rest
270    }
271}
272
273#[cfg(test)]
274mod test_new {
275    use super::*;
276
277    #[test]
278    fn test_first_iterations() {
279        let config = HfTimeConfiguration::new(
280            Duration::milliseconds(10),
281            Duration::milliseconds(3),
282            DateTime::default(),
283        )
284        .expect("cannot create configuration");
285
286        // example of how we want HfTime to pass.
287        let vals = vec![
288            (1, 1),
289            (2, 2),
290            (3, 3),
291            (4, 3),
292            (5, 3),
293            (6, 3),
294            (7, 3),
295            (8, 3),
296            (9, 3),
297            (10, 3),
298            (11, 4),
299            (12, 5),
300            (13, 6),
301            (14, 6),
302        ];
303
304        for v in vals.iter() {
305            let from_time = HfTime::new(
306                DateTime::from_timestamp_millis(v.0).expect("cannot create timestamp"),
307                config,
308            );
309            assert_eq!(from_time.as_hf_millis(), v.1);
310        }
311    }
312
313    #[test]
314    fn test_creation_from_time() {
315        let config = HfTimeConfiguration::new(
316            Duration::seconds(1),
317            Duration::milliseconds(500),
318            DateTime::default(),
319        )
320        .expect("cannot create configuration");
321
322        let from_time = HfTime::new(
323            DateTime::from_timestamp_millis(1200).expect("cannot create timestamp"),
324            config,
325        );
326        assert_eq!(from_time.as_millis(), 1200);
327        assert_eq!(from_time.as_hf_millis(), 700);
328    }
329
330    #[test]
331    fn test_creation_with_start_time() {
332        let config = HfTimeConfiguration::new(
333            Duration::seconds(1),
334            Duration::milliseconds(500),
335            DateTime::default(),
336        )
337        .expect("cannot create configuration");
338
339        let from_time = HfTime::new(
340            DateTime::from_timestamp_millis(1200).expect("cannot create timestamp"),
341            config,
342        );
343        assert_eq!(from_time.as_millis(), 1200);
344        assert_eq!(from_time.as_hf_millis(), 700);
345    }
346}
347
348impl Add<Duration> for HfTime {
349    type Output = Self;
350
351    #[allow(clippy::cast_possible_truncation)]
352    fn add(self, rhs: Duration) -> Self {
353        Self {
354            time: self.time + rhs.num_milliseconds(),
355            config: self.config,
356        }
357    }
358}
359
360impl Add<HfDuration> for HfTime {
361    type Output = Self;
362
363    fn add(self, rhs: HfDuration) -> Self {
364        // easy we compute the number of played loop irl + to add
365        let mut nb_loop = self.time / self.config.irl_length;
366        nb_loop += rhs.value / self.config.ig_length;
367
368        // if we are after the end of game time, we jump to start of game time
369        let mut irl_rest = self.time % self.config.irl_length;
370        if irl_rest > self.config.ig_length {
371            nb_loop += 1;
372            irl_rest = 0;
373        }
374
375        let mut ig_rest = rhs.value % self.config.ig_length;
376
377        if irl_rest + ig_rest > self.config.ig_length {
378            nb_loop += 1;
379            ig_rest = irl_rest + ig_rest - self.config.ig_length;
380            irl_rest = 0;
381        }
382
383        let time = nb_loop * self.config.irl_length + irl_rest + ig_rest;
384
385        Self {
386            time,
387            config: self.config,
388        }
389    }
390}
391
392#[cfg(test)]
393mod test_add {
394    use super::*;
395
396    #[test]
397    fn test_add_only_full_loop() {
398        let config = HfTimeConfiguration::new(
399            Duration::milliseconds(120),
400            Duration::milliseconds(60),
401            DateTime::default(),
402        )
403        .expect("cannot create configuration");
404        let mut time = HfTime::new(
405            DateTime::from_timestamp_millis(0).expect("cannot create timestamp"),
406            config,
407        );
408
409        time = time + Duration::milliseconds(120 * 5);
410        assert_eq!(time.as_hf_millis(), 60 * 5);
411        assert_eq!(time.as_millis(), 120 * 5);
412
413        time = time + HfDuration::from_milliseconds(60 * 3);
414        assert_eq!(time.as_hf_millis(), 60 * 8);
415        assert_eq!(time.as_millis(), 120 * 8);
416    }
417
418    #[test]
419    fn test_add_full_loop_during_length() {
420        let config = HfTimeConfiguration::new(
421            Duration::milliseconds(100),
422            Duration::milliseconds(30),
423            DateTime::default(),
424        )
425        .expect("cannot create configuration");
426        let mut time = HfTime::new(
427            DateTime::from_timestamp_millis(15).expect("cannot create timestamp"),
428            config,
429        );
430
431        time = time + Duration::milliseconds(100 * 2);
432        assert_eq!(time.as_hf_millis(), 75);
433        assert_eq!(time.as_millis(), 215);
434
435        time = time + HfDuration::from_milliseconds(30);
436        assert_eq!(time.as_hf_millis(), 105);
437        assert_eq!(time.as_millis(), 315);
438    }
439
440    #[test]
441    fn test_add_full_loop_after_length() {
442        let config = HfTimeConfiguration::new(
443            Duration::milliseconds(100),
444            Duration::milliseconds(30),
445            DateTime::default(),
446        )
447        .expect("cannot create configuration");
448        let mut time = HfTime::new(
449            DateTime::from_timestamp_millis(50).expect("cannot create timestamp"),
450            config,
451        );
452
453        time = time + Duration::milliseconds(100);
454        assert_eq!(time.as_hf_millis(), 60);
455        assert_eq!(time.as_millis(), 150);
456
457        time = time + HfDuration::from_milliseconds(30);
458        assert_eq!(time.as_hf_millis(), 90);
459        assert_eq!(time.as_millis(), 300);
460    }
461
462    #[test]
463    fn test_add_partial_after_length() {
464        let config = HfTimeConfiguration::new(
465            Duration::milliseconds(1000),
466            Duration::milliseconds(100),
467            DateTime::default(),
468        )
469        .expect("cannot create configuration");
470        let mut time = HfTime::new(
471            DateTime::from_timestamp_millis(500).expect("cannot create timestamp"),
472            config,
473        );
474
475        time = time + HfDuration::from_milliseconds(10);
476        assert_eq!(time.as_hf_millis(), 110);
477        assert_eq!(time.as_millis(), 1010);
478    }
479
480    #[test]
481    fn test_add_superior_date_add_negative_start_date() {
482        let config = HfTimeConfiguration::new(
483            Duration::seconds(900),
484            Duration::seconds(600),
485            DateTime::from_timestamp_millis(-55222041600000).unwrap(),
486        )
487        .expect("cannot create configuration");
488
489        let bug_time = DateTime::from_timestamp_millis(56997116612120 - 55222041600000).unwrap();
490
491        let hf_time = HfTime::new(bug_time, config);
492        let hf_duration = HfDuration::from_seconds(100usize as i64);
493
494        let end = hf_time + hf_duration;
495
496        let hf_result = end.as_datetime().expect("no datetime from hf result");
497
498        assert!(
499            hf_result.clone() >= bug_time.clone(),
500            "we must have {} >= {}",
501            hf_result,
502            bug_time
503        )
504    }
505}
506
507#[cfg(test)]
508mod test_creation_after_epoch {
509    use super::*;
510    use chrono::TimeZone;
511
512    #[test]
513    fn test_create_millennium() {
514        let config = HfTimeConfiguration::new(
515            Duration::seconds(3600 * 24),
516            Duration::seconds(3600 * 2),
517            Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap(),
518        )
519        .expect("cannot create configuration");
520
521        let time = HfTime::new(Utc::now(), config);
522
523        assert!(time.as_millis() < 20 * 365 * 24 * 60 * 60 * 1000)
524    }
525}