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#[derive(Error, Debug)]
12pub enum HfTimeError {
13 #[error("loop length must be greater than length and non zero")]
15 InvalidLength,
16}
17
18#[derive(Error, Debug)]
20pub enum HfTimeConfigurationError {
21 #[error("start date is out of bound")]
23 InvalidStartDate,
24}
25
26#[derive(Copy, Clone, Debug)]
29pub struct HfDuration {
30 value: i64,
31}
32
33impl HfDuration {
34 #[must_use]
36 pub const fn from_milliseconds(value: i64) -> Self {
37 Self { value }
38 }
39
40 #[must_use]
42 pub const fn from_seconds(value: i64) -> Self {
43 Self {
44 value: value * 1000,
45 }
46 }
47
48 #[must_use]
50 pub const fn as_milliseconds(self) -> i64 {
51 self.value
52 }
53
54 #[must_use]
56 pub const fn as_seconds(self) -> i64 {
57 self.value / 1000
58 }
59
60 #[must_use]
62 pub const fn num_seconds(self) -> i64 {
63 self.value / 1000
64 }
65
66 #[must_use]
68 pub const fn num_minutes(self) -> i64 {
69 self.num_seconds() / 60
70 }
71
72 #[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#[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 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 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 #[must_use]
157 pub const fn irl_length(&self) -> i64 {
158 self.irl_length
159 }
160
161 #[must_use]
163 pub const fn ig_length(&self) -> i64 {
164 self.ig_length
165 }
166
167 #[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#[derive(Debug)]
179pub struct HfTime {
180 time: i64,
181 config: HfTimeConfiguration,
182}
183
184pub enum HfStatus {
186 Paused,
188 Running,
190}
191
192impl HfTime {
193 #[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 #[must_use]
204 pub fn now(config: HfTimeConfiguration) -> Self {
205 let start = Utc::now();
206 Self::new(start, config)
207 }
208
209 #[must_use]
211 const fn as_millis(&self) -> i64 {
212 self.time
213 }
214
215 #[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 #[must_use]
223 pub const fn as_duration(&self) -> Duration {
224 Duration::milliseconds(self.as_millis())
225 }
226
227 #[must_use]
229 pub const fn as_hf_duration(&self) -> HfDuration {
230 HfDuration {
231 value: self.as_hf_millis(),
232 }
233 }
234
235 #[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 #[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 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 let mut nb_loop = self.time / self.config.irl_length;
366 nb_loop += rhs.value / self.config.ig_length;
367
368 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}