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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
/*
 * --------------------
 * THIS FILE IS LICENSED UNDER MIT
 * THE FOLLOWING MESSAGE IS NOT A LICENSE
 *
 * <barrow@tilde.team> wrote this file.
 * by reading this text, you are reading "TRANS RIGHTS".
 * this file and the content within it is the gay agenda.
 * if we meet some day, and you think this stuff is worth it,
 * you can buy me a beer, tea, or something stronger.
 * -Ezra Barrow
 * --------------------
 */
#![deny(missing_docs)]
//! The `timemachine` crate handles daily time-based state transitions
//!
//! [![GitHub last commit](https://img.shields.io/github/last-commit/barrowsys/timemachine)](https://github.com/barrowsys/timemachine)
//! [![Crates.io](https://img.shields.io/crates/v/timemachine)](https://crates.io/crates/timemachine/)
//! [![Docs.rs](https://docs.rs/timemachine/badge.svg)](https://docs.rs/timemachine)
//!
//! For the main chunk of docs, see [TimeMachine](crate::TimeMachine).
//!
//! [Napchart](napchart) support can be included with crate feature `napchart`.

use std::collections::HashMap;

mod time;
pub use time::Clock;
#[doc(inline)]
pub use time::Time;

mod error;
pub use error::ErrorKind;
use error::Result;

/// A time-based mod 24(\*60\*60) state machine.
///
/// State transitions are added with [`add_transition`](#method.add_transition),
/// and you can get the state at a given time with [`get_state`](#method.get_state).
///
/// If your state type implements [Default](core::default::Default),
/// you can also use [`get_state_or_default`](#method.get_state_or_default)
/// and [`map_states_or_default`](#method.map_states_or_default).
///
/// If the crate feature `napchart` is enabled, you can also use
/// [`from_napchart`](#method.from_napchart).
#[derive(Debug, Default)]
pub struct TimeMachine<S: Clone> {
    edges: HashMap<Time, S>,
}
impl<S: Clone> TimeMachine<S> {
    /// Creates a new, empty timemachine.
    ///
    /// Hypothetically you can call TimeMachine::default() but the compiler gets mad if your state
    /// type doesn't also implement default.
    ///
    /// ```
    /// use timemachine::TimeMachine;
    /// use timemachine::Time;
    /// use timemachine::ErrorKind;
    ///
    /// let mut tm: TimeMachine<i32> = TimeMachine::new();
    ///
    /// assert_eq!(tm.get_state(&Time::midnight()), Err(ErrorKind::EmptyTimeMachine));
    /// ```
    pub fn new() -> Self {
        Self {
            edges: HashMap::new(),
        }
    }
    /// Maps a TimeMachine\<S\> into a TimeMachine\<R\> with a mapping function.
    ///
    /// ```
    /// use timemachine::TimeMachine;
    /// use timemachine::Time;
    ///
    /// let mut tm: TimeMachine<i32> = TimeMachine::new();
    /// // Adds a transition to state `-50` at midnight.
    /// tm.add_transition(Time::midnight(), -50);
    /// // Adds a transition to state `73` at 6AM.
    /// tm.add_transition(Time::new_h(6), 73);
    /// // Adds a transition to state `25` at noon.
    /// tm.add_transition(Time::noon(), 25);
    /// // Adds a transition to state `-37` at 6PM.
    /// tm.add_transition(Time::new_h(18), -37);
    ///
    /// // Map the TimeMachine<i32> to a TimeMachine<bool> by mapping
    /// // positive values to true and negative values to false
    /// let mut tm: TimeMachine<bool> = tm.map_states(|s| s >= 0);
    ///
    /// assert_eq!(tm.get_state(&Time::new_h(1)).unwrap(), false);
    /// assert_eq!(tm.get_state(&Time::new_h(7)).unwrap(), true);
    /// assert_eq!(tm.get_state(&Time::new_h(13)).unwrap(), true);
    /// assert_eq!(tm.get_state(&Time::new_h(19)).unwrap(), false);
    /// ```
    pub fn map_states<F, R>(self, mapfn: F) -> TimeMachine<R>
    where
        F: Fn(S) -> R,
        R: Clone,
    {
        TimeMachine {
            edges: self.edges.into_iter().map(|(k, v)| (k, mapfn(v))).collect(),
        }
    }
    /// Adds a transition to the given state at the given time to the timemachine.
    ///
    /// ```
    /// use timemachine::TimeMachine;
    /// use timemachine::Time;
    ///
    /// let mut tm: TimeMachine<bool> = TimeMachine::new();
    /// // Adds a transition to state `false` at midnight.
    /// tm.add_transition(Time::midnight(), false);
    /// // Adds a transition to state `true` at noon.
    /// tm.add_transition(Time::noon(), true);
    /// assert_eq!(tm.get_state(&Time::new_h(6)).unwrap(), false);
    /// assert_eq!(tm.get_state(&Time::new_h(18)).unwrap(), true);
    /// ```
    pub fn add_transition(&mut self, time: Time, state: S) {
        self.edges.insert(time, state);
    }
    fn get_edges(&self, time: &Time) -> Result<((Time, S), (Time, S))> {
        if self.edges.is_empty() {
            return Err(ErrorKind::EmptyTimeMachine);
        }
        let mut prev_edge = Time::midnight();
        let mut next_edge = Time::from_minutes(-1);
        let mut last_time = prev_edge.clone();
        let mut first_time = next_edge.clone();
        for edge in self.edges.keys() {
            if time >= edge && edge >= &prev_edge {
                prev_edge = edge.clone();
            }
            if time < edge && edge < &next_edge {
                next_edge = edge.clone();
            }
            if edge > &last_time {
                last_time = edge.clone();
            }
            if edge < &first_time {
                first_time = edge.clone();
            }
        }
        let prev_ret = if let Some(state) = self.edges.get(&prev_edge) {
            (prev_edge.clone(), state.clone())
        } else {
            (
                last_time.clone(),
                self.edges.get(&last_time).unwrap().clone(),
            )
        };
        let next_ret = if let Some(state) = self.edges.get(&next_edge) {
            (next_edge.clone(), state.clone())
        } else {
            (
                first_time.clone(),
                self.edges.get(&first_time).unwrap().clone(),
            )
        };
        Ok((prev_ret, next_ret))
    }
    /// Returns the state that the timemachine is in at the given time.
    ///
    /// ```
    /// use timemachine::TimeMachine;
    /// use timemachine::Time;
    ///
    /// let mut tm: TimeMachine<bool> = TimeMachine::new();
    /// tm.add_transition(Time::midnight(), false);
    /// tm.add_transition(Time::noon(), true);
    ///
    /// let state_3am = tm.get_state(&Time::new_h(3)).unwrap();
    /// assert_eq!(state_3am, false);
    ///
    /// let state_6am = tm.get_state(&Time::new_h(6)).unwrap();
    /// assert_eq!(state_6am, false);
    ///
    /// let state_6pm = tm.get_state(&Time::new_h(18)).unwrap();
    /// assert_eq!(state_6pm, true);
    ///
    /// let state_9pm = tm.get_state(&Time::new_h(21)).unwrap();
    /// assert_eq!(state_9pm, true);
    /// ```
    pub fn get_state(&self, time: &Time) -> Result<S> {
        Ok(self.get_edges(time)?.0 .1)
    }
    /// Returns a tuple of the current state, the progress through the current state,
    /// and the next state.
    /// This is useful for things like interpolating a value between two states.
    ///
    /// ```
    /// use timemachine::TimeMachine;
    /// use timemachine::Time;
    ///
    /// let mut tm: TimeMachine<bool> = TimeMachine::new();
    /// tm.add_transition(Time::midnight(), false);
    /// tm.add_transition(Time::noon(), true);
    ///
    /// // At 3AM, our current state is false, we are 25% through the current state,
    /// // and our next state is true
    /// let progress_3am = tm.get_state_progress(&Time::new_h(3)).unwrap();
    /// assert_eq!(progress_3am, (false, 0.25f64, true));
    ///
    /// // At 6AM, our current state is false, we are 50% through the current state,
    /// // and our next state is true
    /// let progress_6am = tm.get_state_progress(&Time::new_h(6)).unwrap();
    /// assert_eq!(progress_6am, (false, 0.5f64, true));
    ///
    /// // At 6PM, our current state is true, we are 50% through the current state,
    /// // and our next state is false
    /// let progress_6pm = tm.get_state_progress(&Time::new_h(18)).unwrap();
    /// assert_eq!(progress_6pm, (true, 0.5f64, false));
    ///
    /// // At 9PM, our current state is true, we are 75% through the current state,
    /// // and our next state is false
    /// let progress_9pm = tm.get_state_progress(&Time::new_h(21)).unwrap();
    /// assert_eq!(progress_9pm, (true, 0.75f64, false));
    /// ```
    pub fn get_state_progress(&self, _time: &Time) -> Result<(S, f64, S)> {
        let ((pedge, pstate), (nedge, nstate)) = self.get_edges(_time)?;
        let seconds_between: f64 = pedge.secs_until(&nedge).into();
        let seconds_elapsed: f64 = pedge.secs_until(_time).into();
        let progress = seconds_elapsed / seconds_between;
        Ok((pstate, progress, nstate))
    }
}
/// Extra functions if your state type implements [Default](core::default::Default)
impl<S: Clone + Default> TimeMachine<S> {
    /// Maps a TimeMachine\<S\> into a TimeMachine\<R: Default\> with a mapping function.
    ///
    /// If the mapping function returns Some, the state is set to that value.
    /// If the mapping function returns None, the state is set to default().
    ///
    /// ```
    /// use timemachine::TimeMachine;
    /// use timemachine::Time;
    /// use std::convert::TryInto;
    ///
    /// let mut tm: TimeMachine<i8> = TimeMachine::new();
    /// // Adds a transition to state `-50` at midnight.
    /// tm.add_transition(Time::midnight(), -50);
    /// // Adds a transition to state `73` at 6AM.
    /// tm.add_transition(Time::new_h(6), 73);
    /// // Adds a transition to state `25` at noon.
    /// tm.add_transition(Time::noon(), 25);
    /// // Adds a transition to state `-37` at 6PM.
    /// tm.add_transition(Time::new_h(18), -37);
    ///
    /// // Map the TimeMachine<i8> to a TimeMachine<u8> by mapping
    /// // positive values to themselves and negative values to default
    /// let mut tm: TimeMachine<u8> = tm.map_states_or_default(|s| s.try_into().ok());
    ///
    /// assert_eq!(tm.get_state(&Time::new_h(1)).unwrap(), 0);
    /// assert_eq!(tm.get_state(&Time::new_h(7)).unwrap(), 73);
    /// assert_eq!(tm.get_state(&Time::new_h(13)).unwrap(), 25);
    /// assert_eq!(tm.get_state(&Time::new_h(19)).unwrap(), 0);
    /// ```
    pub fn map_states_or_default<F, R>(self, mapfn: F) -> TimeMachine<R>
    where
        F: Fn(S) -> Option<R>,
        R: Clone + Default,
    {
        TimeMachine {
            edges: self
                .edges
                .into_iter()
                .map(|(k, v)| (k, mapfn(v).unwrap_or_default()))
                .collect(),
        }
    }
    /// Returns the state that the timemachine is in at the given time.
    /// If the timemachine is empty, it will return S.default().
    ///
    /// This method does not handle any errors except EmptyTimeMachine.
    ///
    /// ```
    /// use timemachine::TimeMachine;
    /// use timemachine::Time;
    /// use timemachine::ErrorKind;
    ///
    /// let mut tm: TimeMachine<i32> = TimeMachine::new();
    ///
    /// assert_eq!(tm.get_state(&Time::midnight()), Err(ErrorKind::EmptyTimeMachine));
    /// assert_eq!(tm.get_state_or_default(&Time::midnight()), Ok(0));
    /// ```
    pub fn get_state_or_default(&self, time: &Time) -> Result<S> {
        if self.edges.is_empty() {
            Ok(S::default())
        } else {
            self.get_state(time)
        }
    }
}
#[cfg(feature = "napchart")]
impl TimeMachine<Option<napchart::ElementData>> {
    /// Creates a TimeMachine\<Option\<<napchart::ElementData>\>\> from a <napchart::ChartLane>.
    ///
    /// Elements in the lane will be added as Some([ElementData](napchart::ElementData)).
    /// Empty space in the lane will be added as None.
    ///
    /// You can use [`map_states`](#method.map_states) or
    /// [`map_states_or_default`](#method.map_states_or_default)
    /// to turn this into something more useful.
    /// ```
    /// use napchart::api::BlockingClient;
    /// use timemachine::TimeMachine;
    /// use timemachine::Time;
    ///
    /// let client = BlockingClient::default();
    /// // chart link: https://napchart.com/jex3y
    /// let chart = client.get("jex3y").unwrap();
    /// let lane = chart.lanes.get(0).unwrap();
    /// let mut tm = TimeMachine::from_napchart(&lane)
    ///     // Map Option<ElementData> to String by taking data.color, otherwise "blank"
    ///     .map_states(|e| match e {
    ///         Some(data) => data.color,
    ///         None => String::from("blank"),
    ///     });
    ///
    /// let state_4am = tm.get_state(&Time::new_h(4)).unwrap();
    /// assert_eq!(state_4am, String::from("red"));
    ///
    /// let state_noon = tm.get_state(&Time::new_h(12)).unwrap();
    /// assert_eq!(state_noon, String::from("blue"));
    ///
    /// let state_8pm = tm.get_state(&Time::new_h(20)).unwrap();
    /// assert_eq!(state_8pm, String::from("blank"));
    /// ```
    pub fn from_napchart(lane: &napchart::ChartLane) -> TimeMachine<Option<napchart::ElementData>> {
        let mut tm = TimeMachine::new();
        for elem in lane.elements_iter() {
            tm.add_transition(
                Time::from_minutes(elem.start as i16),
                Some(elem.data.clone()),
            );
            if !tm.edges.contains_key(&Time::from_minutes(elem.end as i16)) {
                tm.add_transition(Time::from_minutes(elem.end as i16), None);
            }
        }
        tm
    }
}