periodical 0.3.0

Management of all kinds of time intervals, use it to manage schedules, find overlaps, and more!
Documentation
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
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
//! Absolute intervals
//!
//! Absolute intervals are set in absolute time, that is to say that are bound to [`Timestamp`](jiff::Timestamp)(s)
//! when applicable.
//!
//! The most common absolute interval objects you will encounter are
//!
//! - [`AbsBoundPair`]
//! - [`EmptiableAbsBoundPair`]
//! - [`BoundedAbsInterval`]
//! - [`HalfBoundedAbsInterval`]
//!
//! Refer to [the `intervals` module](crate::intervals) for more information about how intervals are structured.

use std::error::Error;
use std::fmt::Display;

use crate::intervals::meta::BoundInclusivity;
use crate::intervals::ops::{BoundOrd, BoundOrdering, BoundOverlapAmbiguity};

pub mod bound;
pub mod bound_pair;
pub mod bounded_interval;
pub mod emptiable_bound_pair;
pub mod emptiable_interval;
pub mod end_bound;
pub mod finite_bound;
pub mod finite_bound_position;
pub mod finite_end_bound;
pub mod finite_start_bound;
pub mod half_bounded_interval;
pub mod half_bounded_to_future_interval;
pub mod half_bounded_to_past_interval;
pub mod interval;
pub mod start_bound;

#[cfg(test)]
mod bound_pair_tests;
#[cfg(test)]
mod bound_tests;
#[cfg(test)]
mod bounded_interval_tests;
#[cfg(test)]
mod emptiable_bound_pair_tests;
#[cfg(test)]
mod emptiable_interval_tests;
#[cfg(test)]
mod end_bound_tests;
#[cfg(test)]
mod finite_bound_position_tests;
#[cfg(test)]
mod finite_bound_tests;
#[cfg(test)]
mod finite_end_bound_tests;
#[cfg(test)]
mod finite_start_bound_tests;
#[cfg(test)]
mod half_bounded_interval_tests;
#[cfg(test)]
mod half_bounded_to_future_interval_tests;
#[cfg(test)]
mod half_bounded_to_past_interval_tests;
#[cfg(test)]
mod interval_tests;
#[cfg(test)]
mod start_bound_tests;

#[doc(inline)]
pub use bound::AbsBound;
#[doc(inline)]
pub use bound_pair::{AbsBoundPair, HasAbsBoundPair};
#[doc(inline)]
pub use bounded_interval::BoundedAbsInterval;
#[doc(inline)]
pub use emptiable_bound_pair::{EmptiableAbsBoundPair, HasEmptiableAbsBoundPair};
#[doc(inline)]
pub use emptiable_interval::EmptiableAbsInterval;
#[doc(inline)]
pub use end_bound::AbsEndBound;
#[doc(inline)]
pub use finite_bound::AbsFiniteBound;
#[doc(inline)]
pub use finite_bound_position::AbsFiniteBoundPos;
#[doc(inline)]
pub use finite_end_bound::AbsFiniteEndBound;
#[doc(inline)]
pub use finite_start_bound::AbsFiniteStartBound;
#[doc(inline)]
pub use half_bounded_interval::HalfBoundedAbsInterval;
#[doc(inline)]
pub use half_bounded_to_future_interval::HalfBoundedToFutureAbsInterval;
#[doc(inline)]
pub use half_bounded_to_past_interval::HalfBoundedToPastAbsInterval;
#[doc(inline)]
pub use interval::AbsInterval;
#[doc(inline)]
pub use start_bound::AbsStartBound;

/// Swaps an absolute finite start bound with an absolute finite end bound
///
/// # Examples
///
/// ```
/// # use std::error::Error;
/// # use jiff::Timestamp;
/// # use periodical::intervals::absolute::{
/// #     AbsFiniteBoundPos,
/// #     swap_abs_finite_start_end_bounds,
/// # };
/// let first_time = "2026-01-01 00:00:00Z".parse::<Timestamp>()?;
/// let second_time = "2026-05-01 00:00:00Z".parse::<Timestamp>()?;
///
/// let mut start = AbsFiniteBoundPos::new(first_time).to_finite_start_bound();
/// let mut end = AbsFiniteBoundPos::new(second_time).to_finite_end_bound();
///
/// swap_abs_finite_start_end_bounds(&mut start, &mut end);
///
/// assert_eq!(start.pos().time(), second_time);
/// assert_eq!(end.pos().time(), first_time);
/// # Ok::<(), Box<dyn Error>>(())
/// ```
pub fn swap_abs_finite_start_end_bounds(finite_start: &mut AbsFiniteStartBound, finite_end: &mut AbsFiniteEndBound) {
    let AbsFiniteStartBound(finite_start_pos) = finite_start;
    let AbsFiniteEndBound(finite_end_pos) = finite_end;

    std::mem::swap(finite_start_pos, finite_end_pos);
}

/// Swaps an absolute start bound with an absolute end bound
///
/// This method is primarily used in the case where a start bound and an end
/// bound are not in chronological order.
///
/// # Examples
///
/// ```
/// # use std::error::Error;
/// # use jiff::Timestamp;
/// # use periodical::intervals::absolute::{AbsFiniteBoundPos, swap_abs_start_end_bounds};
/// let start_time = "2025-01-01 16:00:00Z".parse::<Timestamp>()?;
/// let end_time = "2025-01-01 08:00:00Z".parse::<Timestamp>()?;
///
/// let mut start = AbsFiniteBoundPos::new(start_time).to_start_bound();
/// let mut end = AbsFiniteBoundPos::new(end_time).to_end_bound();
///
/// swap_abs_start_end_bounds(&mut start, &mut end);
///
/// assert_eq!(start, AbsFiniteBoundPos::new(end_time).to_start_bound());
/// assert_eq!(end, AbsFiniteBoundPos::new(start_time).to_end_bound());
/// # Ok::<(), Box<dyn Error>>(())
/// ```
pub fn swap_abs_start_end_bounds(start: &mut AbsStartBound, end: &mut AbsEndBound) {
    // We temporarily reborrow start and end for the match arms so that when a
    // pattern matches, they move out of their temporary scope and we can use
    // the original mutable references without guard patterns shenanigans.
    // When destructuring, however, the scope of the reborrowed value extends up to
    // where it is used within the body, So we always finish our business with
    // the reborrowed values first before accessing the original ones.
    match (&mut *start, &mut *end) {
        (AbsStartBound::InfinitePast, AbsEndBound::InfiniteFuture) => {},
        (AbsStartBound::InfinitePast, AbsEndBound::Finite(finite_end)) => {
            *start = finite_end.pos().to_start_bound();
            *end = AbsEndBound::InfiniteFuture;
        },
        (AbsStartBound::Finite(finite_start), AbsEndBound::InfiniteFuture) => {
            *end = finite_start.pos().to_end_bound();
            *start = AbsStartBound::InfinitePast;
        },
        (AbsStartBound::Finite(finite_start), AbsEndBound::Finite(finite_end)) => {
            swap_abs_finite_start_end_bounds(finite_start, finite_end);
        },
    }
}

/// Possible problems that can prevent creating an interval from the given start
/// and end bounds
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AbsStartEndBoundsCheckForIntervalCreationError {
    /// Start bound is past the end bound
    StartPastEnd,
    /// Both bounds are on the same time but don't have only inclusive bound inclusivities
    SameTimeButNotDoublyInclusive,
}

impl Display for AbsStartEndBoundsCheckForIntervalCreationError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::StartPastEnd => write!(f, "Start bound is past the end bound"),
            Self::SameTimeButNotDoublyInclusive => write!(
                f,
                "Both bounds are on the same time but don't have only inclusive bound inclusivities"
            ),
        }
    }
}

impl Error for AbsStartEndBoundsCheckForIntervalCreationError {}

/// Checks whether the given finite start and finite end bounds are fit for creating an interval
///
/// # Errors
///
/// Returns [`StartPastEnd`](AbsStartEndBoundsCheckForIntervalCreationError::StartPastEnd)
/// if the start bound is positioned after the end bound.
///
/// Returns [`SameTimeButNotDoublyInclusive`](AbsStartEndBoundsCheckForIntervalCreationError::SameTimeButNotDoublyInclusive)
/// if the bounds are positioned on the same time but are not doubly inclusive.
///
/// # Examples
///
/// ## Start past end
///
/// ```
/// # use std::error::Error;
/// # use jiff::Timestamp;
/// # use periodical::intervals::absolute::{
/// #     AbsFiniteBoundPos,
/// #     AbsStartEndBoundsCheckForIntervalCreationError,
/// #     check_abs_finite_start_end_bounds_for_interval_creation,
/// # };
/// let start = AbsFiniteBoundPos::new("2026-01-01 16:00:00Z".parse::<Timestamp>()?)
///     .to_finite_start_bound();
/// let end =
///     AbsFiniteBoundPos::new("2026-01-01 08:00:00Z".parse::<Timestamp>()?).to_finite_end_bound();
///
/// assert_eq!(
///     check_abs_finite_start_end_bounds_for_interval_creation(&start, &end),
///     Err(AbsStartEndBoundsCheckForIntervalCreationError::StartPastEnd)
/// );
/// # Ok::<(), Box<dyn Error>>(())
/// ```
///
/// ## Same time but not doubly inclusive
///
/// ```
/// # use std::error::Error;
/// # use jiff::Timestamp;
/// # use periodical::intervals::absolute::{
/// #     AbsFiniteBoundPos,
/// #     AbsStartEndBoundsCheckForIntervalCreationError,
/// #     check_abs_finite_start_end_bounds_for_interval_creation,
/// # };
/// # use periodical::intervals::meta::BoundInclusivity;
/// let time = "2026-01-01 08:00:00Z".parse::<Timestamp>()?;
/// let start =
///     AbsFiniteBoundPos::new_with_incl(time, BoundInclusivity::Exclusive).to_finite_start_bound();
/// let end = AbsFiniteBoundPos::new(time).to_finite_end_bound();
///
/// assert_eq!(
///     check_abs_finite_start_end_bounds_for_interval_creation(&start, &end),
///     Err(AbsStartEndBoundsCheckForIntervalCreationError::SameTimeButNotDoublyInclusive)
/// );
/// # Ok::<(), Box<dyn Error>>(())
/// ```
///
/// ## OK
///
/// ```
/// # use std::error::Error;
/// # use jiff::Timestamp;
/// # use periodical::intervals::absolute::{
/// #     AbsFiniteBoundPos,
/// #     check_abs_finite_start_end_bounds_for_interval_creation,
/// # };
/// let start = AbsFiniteBoundPos::new("2026-01-01 08:00:00Z".parse::<Timestamp>()?)
///     .to_finite_start_bound();
/// let end =
///     AbsFiniteBoundPos::new("2026-01-01 16:00:00Z".parse::<Timestamp>()?).to_finite_end_bound();
///
/// assert_eq!(
///     check_abs_finite_start_end_bounds_for_interval_creation(&start, &end),
///     Ok(())
/// );
/// # Ok::<(), Box<dyn Error>>(())
/// ```
pub fn check_abs_finite_start_end_bounds_for_interval_creation(
    start: &AbsFiniteStartBound,
    end: &AbsFiniteEndBound,
) -> Result<(), AbsStartEndBoundsCheckForIntervalCreationError> {
    match start.bound_cmp(end) {
        BoundOrdering::Less => Ok(()),
        BoundOrdering::Equal(Some(BoundOverlapAmbiguity::StartEnd(start_incl, end_incl))) => {
            if start_incl == BoundInclusivity::Inclusive && end_incl == BoundInclusivity::Inclusive {
                Ok(())
            } else {
                Err(AbsStartEndBoundsCheckForIntervalCreationError::SameTimeButNotDoublyInclusive)
            }
        },
        BoundOrdering::Equal(_) => unreachable!(),
        BoundOrdering::Greater => Err(AbsStartEndBoundsCheckForIntervalCreationError::StartPastEnd),
    }
}

/// Checks whether the given start and end bounds are fit for creating an interval
///
/// # Errors
///
/// Returns [`StartPastEnd`](AbsStartEndBoundsCheckForIntervalCreationError::StartPastEnd)
/// if the start bound is positioned after the end bound.
///
/// Returns [`SameTimeButNotDoublyInclusive`](AbsStartEndBoundsCheckForIntervalCreationError::SameTimeButNotDoublyInclusive)
/// if the bounds are positioned on the same time but are not doubly inclusive.
///
/// # Examples
///
/// ## Start past end
///
/// ```
/// # use std::error::Error;
/// # use jiff::Timestamp;
/// # use periodical::intervals::absolute::{
/// #     AbsFiniteBoundPos,
/// #     AbsStartEndBoundsCheckForIntervalCreationError,
/// #     check_abs_start_end_bounds_for_interval_creation,
/// # };
/// let start =
///     AbsFiniteBoundPos::new("2026-01-01 16:00:00Z".parse::<Timestamp>()?).to_start_bound();
/// let end = AbsFiniteBoundPos::new("2026-01-01 08:00:00Z".parse::<Timestamp>()?).to_end_bound();
///
/// assert_eq!(
///     check_abs_start_end_bounds_for_interval_creation(&start, &end),
///     Err(AbsStartEndBoundsCheckForIntervalCreationError::StartPastEnd)
/// );
/// # Ok::<(), Box<dyn Error>>(())
/// ```
///
/// ## Same time but not doubly inclusive
///
/// ```
/// # use std::error::Error;
/// # use jiff::Timestamp;
/// # use periodical::intervals::absolute::{
/// #     AbsFiniteBoundPos,
/// #     AbsStartEndBoundsCheckForIntervalCreationError,
/// #     check_abs_start_end_bounds_for_interval_creation,
/// # };
/// # use periodical::intervals::meta::BoundInclusivity;
/// let time = "2026-01-01 08:00:00Z".parse::<Timestamp>()?;
/// let start =
///     AbsFiniteBoundPos::new_with_incl(time, BoundInclusivity::Exclusive).to_start_bound();
/// let end = AbsFiniteBoundPos::new(time).to_end_bound();
///
/// assert_eq!(
///     check_abs_start_end_bounds_for_interval_creation(&start, &end),
///     Err(AbsStartEndBoundsCheckForIntervalCreationError::SameTimeButNotDoublyInclusive)
/// );
/// # Ok::<(), Box<dyn Error>>(())
/// ```
///
/// ## OK
///
/// ```
/// # use std::error::Error;
/// # use jiff::Timestamp;
/// # use periodical::intervals::absolute::{
/// #     AbsFiniteBoundPos,
/// #     check_abs_start_end_bounds_for_interval_creation,
/// # };
/// let start =
///     AbsFiniteBoundPos::new("2026-01-01 08:00:00Z".parse::<Timestamp>()?).to_start_bound();
/// let end = AbsFiniteBoundPos::new("2026-01-01 16:00:00Z".parse::<Timestamp>()?).to_end_bound();
///
/// assert_eq!(
///     check_abs_start_end_bounds_for_interval_creation(&start, &end),
///     Ok(())
/// );
/// # Ok::<(), Box<dyn Error>>(())
/// ```
pub fn check_abs_start_end_bounds_for_interval_creation(
    start: &AbsStartBound,
    end: &AbsEndBound,
) -> Result<(), AbsStartEndBoundsCheckForIntervalCreationError> {
    match (start, end) {
        (AbsStartBound::InfinitePast, _) | (_, AbsEndBound::InfiniteFuture) => Ok(()),
        (AbsStartBound::Finite(finite_start), AbsEndBound::Finite(finite_end)) => {
            check_abs_finite_start_end_bounds_for_interval_creation(finite_start, finite_end)
        },
    }
}

/// Prepares a finite start and a finite end bound to be used for an interval
///
/// Checks whether the bounds are fit for creating an interval and automatically corrects
/// any problem.
///
/// If the start bound is past the end bound, they are swapped.
/// If the bounds are positioned on the same time but are not doubly inclusive, their bound inclusivities
/// are set to [`Inclusive`](BoundInclusivity::Inclusive).
///
/// Returns whether a change has occurred.
///
/// # Examples
///
/// ```
/// # use std::error::Error;
/// # use jiff::Timestamp;
/// # use periodical::intervals::absolute::{
/// #     AbsFiniteBoundPos,
/// #     AbsStartEndBoundsCheckForIntervalCreationError,
/// #     prepare_abs_finite_start_end_bounds_for_interval_creation,
/// # };
/// let mut start = AbsFiniteBoundPos::new("2026-01-01 16:00:00Z".parse::<Timestamp>()?)
///     .to_finite_start_bound();
/// let mut end =
///     AbsFiniteBoundPos::new("2026-01-01 08:00:00Z".parse::<Timestamp>()?).to_finite_end_bound();
///
/// prepare_abs_finite_start_end_bounds_for_interval_creation(&mut start, &mut end);
///
/// assert_eq!(
///     start.pos().time(),
///     "2026-01-01 08:00:00Z".parse::<Timestamp>()?
/// );
/// assert_eq!(
///     end.pos().time(),
///     "2026-01-01 16:00:00Z".parse::<Timestamp>()?
/// );
/// # Ok::<(), Box<dyn Error>>(())
/// ```
pub fn prepare_abs_finite_start_end_bounds_for_interval_creation(
    start: &mut AbsFiniteStartBound,
    end: &mut AbsFiniteEndBound,
) -> bool {
    match check_abs_finite_start_end_bounds_for_interval_creation(start, end) {
        Ok(()) => false,
        Err(AbsStartEndBoundsCheckForIntervalCreationError::StartPastEnd) => {
            swap_abs_finite_start_end_bounds(start, end);
            true
        },
        Err(AbsStartEndBoundsCheckForIntervalCreationError::SameTimeButNotDoublyInclusive) => {
            let AbsFiniteStartBound(finite_start) = start;
            let AbsFiniteEndBound(finite_end) = end;

            finite_start.set_inclusivity(BoundInclusivity::Inclusive);
            finite_end.set_inclusivity(BoundInclusivity::Inclusive);

            true
        },
    }
}

/// Prepares a start and an end bound to be used for an interval
///
/// Checks whether the bounds are fit for creating an interval and automatically corrects
/// any problem.
///
/// If the start bound is past the end bound, they are swapped.
/// If the bounds are positioned on the same time but are not doubly inclusive, their bound inclusivities
/// are set to [`Inclusive`](BoundInclusivity::Inclusive).
///
/// Returns whether a change has occurred.
///
/// # Examples
///
/// ```
/// # use std::error::Error;
/// # use jiff::Timestamp;
/// # use periodical::intervals::absolute::{
/// #     AbsFiniteBoundPos,
/// #     AbsStartEndBoundsCheckForIntervalCreationError,
/// #     prepare_abs_start_end_bounds_for_interval_creation,
/// # };
/// let mut start =
///     AbsFiniteBoundPos::new("2026-01-01 16:00:00Z".parse::<Timestamp>()?).to_start_bound();
/// let mut end =
///     AbsFiniteBoundPos::new("2026-01-01 08:00:00Z".parse::<Timestamp>()?).to_end_bound();
///
/// prepare_abs_start_end_bounds_for_interval_creation(&mut start, &mut end);
///
/// assert_eq!(
///     start,
///     AbsFiniteBoundPos::new("2026-01-01 08:00:00Z".parse::<Timestamp>()?).to_start_bound()
/// );
/// assert_eq!(
///     end,
///     AbsFiniteBoundPos::new("2026-01-01 16:00:00Z".parse::<Timestamp>()?).to_end_bound()
/// );
/// # Ok::<(), Box<dyn Error>>(())
/// ```
pub fn prepare_abs_start_end_bounds_for_interval_creation(start: &mut AbsStartBound, end: &mut AbsEndBound) -> bool {
    match check_abs_start_end_bounds_for_interval_creation(start, end) {
        Ok(()) => false,
        Err(AbsStartEndBoundsCheckForIntervalCreationError::StartPastEnd) => {
            swap_abs_start_end_bounds(start, end);
            true
        },
        Err(AbsStartEndBoundsCheckForIntervalCreationError::SameTimeButNotDoublyInclusive) => {
            if let AbsStartBound::Finite(AbsFiniteStartBound(finite_start_mut)) = start {
                finite_start_mut.set_inclusivity(BoundInclusivity::Inclusive);
            }

            if let AbsEndBound::Finite(AbsFiniteEndBound(finite_end_mut)) = end {
                finite_end_mut.set_inclusivity(BoundInclusivity::Inclusive);
            }

            true
        },
    }
}