1use std::{fmt, ops::RangeInclusive};
4
5use chrono::{DateTime, Duration, SecondsFormat, TimeZone, Utc};
6use snafu::Snafu;
7
8use crate::{
9 coverage::IndexIntervalId,
10 metadata::index::{
11 IndexKind, IndexValue, IndexValueError, TimeIndexGranularity, validate_index_range,
12 },
13};
14
15const SIGN_BIT: u64 = 0x8000_0000_0000_0000;
16const SECONDS_PER_MINUTE: u64 = 60;
17const SECONDS_PER_HOUR: u64 = 60 * 60;
18const SECONDS_PER_DAY: u64 = 24 * 60 * 60;
19
20#[derive(Debug, Snafu, PartialEq, Eq)]
22#[non_exhaustive]
23pub enum IndexIntervalMappingError {
24 #[snafu(display("Invalid ordered index value: {source}"))]
26 IndexValue {
27 source: IndexValueError,
29 },
30 #[snafu(display("Timestamp index granularity must be nonzero"))]
32 ZeroTimeIndexGranularity,
33 #[snafu(display("Ordered range end cannot be adjusted to its predecessor: {end}"))]
35 RangeEndUnderflow {
36 end: IndexValue,
38 },
39 #[snafu(display(
41 "Index interval ID {index_interval_id} is outside the logical {kind} index domain"
42 ))]
43 IntervalIdOutsideDomain {
44 kind: &'static str,
46 index_interval_id: IndexIntervalId,
48 },
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct IndexInterval {
54 start: IndexValue,
55 end: IndexValue,
56 end_inclusive: bool,
57}
58
59impl IndexInterval {
60 fn new(start: IndexValue, end: IndexValue, end_inclusive: bool) -> Self {
61 Self {
62 start,
63 end,
64 end_inclusive,
65 }
66 }
67
68 pub fn start(&self) -> &IndexValue {
70 &self.start
71 }
72
73 pub fn end(&self) -> &IndexValue {
75 &self.end
76 }
77
78 pub fn end_inclusive(&self) -> bool {
80 self.end_inclusive
81 }
82}
83
84impl fmt::Display for IndexInterval {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 let close = if self.end_inclusive { ']' } else { ')' };
87 match (&self.start, &self.end) {
88 (IndexValue::Timestamp(start), IndexValue::Timestamp(end)) => write!(
89 f,
90 "[{}, {}{close}",
91 start.to_rfc3339_opts(SecondsFormat::AutoSi, true),
92 end.to_rfc3339_opts(SecondsFormat::AutoSi, true)
93 ),
94 (IndexValue::Int64(start), IndexValue::Int64(end)) => {
95 write!(f, "[{start}, {end}{close}")
96 }
97 (IndexValue::UInt64(start), IndexValue::UInt64(end)) => {
98 write!(f, "[{start}, {end}{close}")
99 }
100 _ => unreachable!("index interval endpoints share one index domain"),
101 }
102 }
103}
104
105fn time_index_granularity_seconds(
106 index_granularity: &TimeIndexGranularity,
107) -> Result<u64, IndexIntervalMappingError> {
108 let (value, multiplier) = match *index_granularity {
109 TimeIndexGranularity::Seconds(value) => (value, 1),
110 TimeIndexGranularity::Minutes(value) => (value, SECONDS_PER_MINUTE),
111 TimeIndexGranularity::Hours(value) => (value, SECONDS_PER_HOUR),
112 TimeIndexGranularity::Days(value) => (value, SECONDS_PER_DAY),
113 };
114 if value == 0 {
115 return Err(IndexIntervalMappingError::ZeroTimeIndexGranularity);
116 }
117 Ok(u64::from(value) * multiplier)
118}
119
120fn signed_index_interval_id(ordinal: i64) -> IndexIntervalId {
121 (ordinal as u64) ^ SIGN_BIT
122}
123
124pub fn index_interval_id_from_epoch_secs(
128 index_granularity: &TimeIndexGranularity,
129 seconds: i64,
130) -> Result<IndexIntervalId, IndexIntervalMappingError> {
131 let width = i128::from(time_index_granularity_seconds(index_granularity)?);
132 let ordinal = i128::from(seconds).div_euclid(width) as i64;
133 Ok(signed_index_interval_id(ordinal))
134}
135
136fn timestamp_index_interval_id(
137 index_granularity: &TimeIndexGranularity,
138 value: DateTime<Utc>,
139) -> Result<IndexIntervalId, IndexIntervalMappingError> {
140 index_interval_id_from_epoch_secs(index_granularity, value.timestamp())
141}
142
143fn int64_index_interval_id(value: i64, index_granularity: u64) -> IndexIntervalId {
144 let ordinal = i128::from(value).div_euclid(i128::from(index_granularity)) as i64;
145 signed_index_interval_id(ordinal)
146}
147
148pub fn index_interval_id_for_value(
150 kind: &IndexKind,
151 value: &IndexValue,
152) -> Result<IndexIntervalId, IndexIntervalMappingError> {
153 value
154 .validate_kind(kind)
155 .map_err(|source| IndexIntervalMappingError::IndexValue { source })?;
156
157 match (kind, value) {
158 (
159 IndexKind::Timestamp {
160 index_granularity, ..
161 },
162 IndexValue::Timestamp(value),
163 ) => timestamp_index_interval_id(index_granularity, *value),
164 (IndexKind::Int64 { index_granularity }, IndexValue::Int64(value)) => {
165 Ok(int64_index_interval_id(*value, index_granularity.get()))
166 }
167 (IndexKind::UInt64 { index_granularity }, IndexValue::UInt64(value)) => {
168 Ok(*value / index_granularity.get())
169 }
170 _ => unreachable!("value domain was validated above"),
171 }
172}
173
174pub fn index_interval_for_id(
176 kind: &IndexKind,
177 index_interval_id: IndexIntervalId,
178) -> Result<IndexInterval, IndexIntervalMappingError> {
179 let outside_domain = || IndexIntervalMappingError::IntervalIdOutsideDomain {
180 kind: kind.name(),
181 index_interval_id,
182 };
183
184 match kind {
185 IndexKind::Timestamp {
186 index_granularity, ..
187 } => {
188 let ordinal = i128::from((index_interval_id ^ SIGN_BIT) as i64);
189 let width = i128::from(time_index_granularity_seconds(index_granularity)?);
190 let domain_start = i128::from(DateTime::<Utc>::MIN_UTC.timestamp());
191 let domain_end = i128::from(DateTime::<Utc>::MAX_UTC.timestamp()) + 1;
192 let start = (ordinal * width).max(domain_start);
193 let end = ((ordinal + 1) * width).min(domain_end);
194 if start >= end {
195 return Err(outside_domain());
196 }
197
198 let start = Utc
199 .timestamp_opt(start as i64, 0)
200 .single()
201 .ok_or_else(&outside_domain)?;
202 let end_inclusive = end == domain_end;
203 let end = if end_inclusive {
204 DateTime::<Utc>::MAX_UTC
205 } else {
206 Utc.timestamp_opt(end as i64, 0)
207 .single()
208 .ok_or_else(&outside_domain)?
209 };
210 Ok(IndexInterval::new(start.into(), end.into(), end_inclusive))
211 }
212 IndexKind::Int64 { index_granularity } => {
213 let ordinal = i128::from((index_interval_id ^ SIGN_BIT) as i64);
214 let width = i128::from(index_granularity.get());
215 let domain_start = i128::from(i64::MIN);
216 let domain_end = i128::from(i64::MAX) + 1;
217 let start = (ordinal * width).max(domain_start);
218 let end = ((ordinal + 1) * width).min(domain_end);
219 if start >= end {
220 return Err(outside_domain());
221 }
222
223 let end_inclusive = end == domain_end;
224 Ok(IndexInterval::new(
225 IndexValue::Int64(start as i64),
226 IndexValue::Int64(if end_inclusive { i64::MAX } else { end as i64 }),
227 end_inclusive,
228 ))
229 }
230 IndexKind::UInt64 { index_granularity } => {
231 let width = u128::from(index_granularity.get());
232 let domain_end = u128::from(u64::MAX) + 1;
233 let start = u128::from(index_interval_id) * width;
234 let end = ((u128::from(index_interval_id) + 1) * width).min(domain_end);
235 if start >= end {
236 return Err(outside_domain());
237 }
238
239 let end_inclusive = end == domain_end;
240 Ok(IndexInterval::new(
241 IndexValue::UInt64(start as u64),
242 IndexValue::UInt64(if end_inclusive { u64::MAX } else { end as u64 }),
243 end_inclusive,
244 ))
245 }
246 }
247}
248
249pub fn index_interval_id_range(
251 kind: &IndexKind,
252 start: &IndexValue,
253 end: &IndexValue,
254) -> Result<RangeInclusive<IndexIntervalId>, IndexIntervalMappingError> {
255 validate_index_range(kind, start, end)
256 .map_err(|source| IndexIntervalMappingError::IndexValue { source })?;
257
258 let first = index_interval_id_for_value(kind, start)?;
259 Ok(first..=index_interval_id_for_value(kind, &value_before(end)?)?)
260}
261
262pub fn index_interval_id_for_exclusive_end(
264 kind: &IndexKind,
265 end: &IndexValue,
266) -> Result<IndexIntervalId, IndexIntervalMappingError> {
267 end.validate_kind(kind)
268 .map_err(|source| IndexIntervalMappingError::IndexValue { source })?;
269 index_interval_id_for_value(kind, &value_before(end)?)
270}
271
272fn value_before(end: &IndexValue) -> Result<IndexValue, IndexIntervalMappingError> {
273 Ok(match end {
274 IndexValue::Timestamp(end) => {
275 IndexValue::Timestamp(end.checked_sub_signed(Duration::nanoseconds(1)).ok_or(
276 IndexIntervalMappingError::RangeEndUnderflow {
277 end: IndexValue::Timestamp(*end),
278 },
279 )?)
280 }
281 IndexValue::Int64(end) => IndexValue::Int64(end.checked_sub(1).ok_or({
282 IndexIntervalMappingError::RangeEndUnderflow {
283 end: IndexValue::Int64(*end),
284 }
285 })?),
286 IndexValue::UInt64(end) => IndexValue::UInt64(end.checked_sub(1).ok_or({
287 IndexIntervalMappingError::RangeEndUnderflow {
288 end: IndexValue::UInt64(*end),
289 }
290 })?),
291 })
292}
293
294#[cfg(test)]
295mod tests {
296 use std::num::NonZeroU64;
297
298 use chrono::TimeZone;
299
300 use super::*;
301
302 fn timestamp_kind(index_granularity: TimeIndexGranularity) -> IndexKind {
303 IndexKind::Timestamp {
304 index_granularity,
305 timezone: None,
306 }
307 }
308
309 #[test]
310 fn timestamp_mapping_is_ordered_across_epoch() {
311 let kind = timestamp_kind(TimeIndexGranularity::Seconds(1));
312 let before = Utc.timestamp_opt(-1, 0).single().unwrap().into();
313 let epoch = Utc.timestamp_opt(0, 0).single().unwrap().into();
314 let after = Utc.timestamp_opt(1, 0).single().unwrap().into();
315
316 assert_eq!(
317 index_interval_id_for_value(&kind, &before).unwrap(),
318 SIGN_BIT - 1
319 );
320 assert_eq!(
321 index_interval_id_for_value(&kind, &epoch).unwrap(),
322 SIGN_BIT
323 );
324 assert_eq!(
325 index_interval_id_for_value(&kind, &after).unwrap(),
326 SIGN_BIT + 1
327 );
328 }
329
330 #[test]
331 fn timestamp_mapping_uses_euclidean_intervals_before_epoch() {
332 let index_granularity = TimeIndexGranularity::Minutes(1);
333 assert_eq!(
334 index_interval_id_from_epoch_secs(&index_granularity, -61).unwrap(),
335 SIGN_BIT - 2
336 );
337 assert_eq!(
338 index_interval_id_from_epoch_secs(&index_granularity, -60).unwrap(),
339 SIGN_BIT - 1
340 );
341 assert_eq!(
342 index_interval_id_from_epoch_secs(&index_granularity, -1).unwrap(),
343 SIGN_BIT - 1
344 );
345 assert_eq!(
346 index_interval_id_from_epoch_secs(&index_granularity, 0).unwrap(),
347 SIGN_BIT
348 );
349 }
350
351 #[test]
352 fn int64_mapping_handles_zero_and_extremes() {
353 for width in [1, 3, u64::MAX] {
354 let kind = IndexKind::Int64 {
355 index_granularity: NonZeroU64::new(width).unwrap(),
356 };
357 let values = [i64::MIN, -1, 0, 1, i64::MAX];
358 let interval_ids: Vec<_> = values
359 .into_iter()
360 .map(|value| index_interval_id_for_value(&kind, &value.into()).unwrap())
361 .collect();
362 assert!(interval_ids.windows(2).all(|pair| pair[0] <= pair[1]));
363 }
364
365 let unit = IndexKind::Int64 {
366 index_granularity: NonZeroU64::new(1).unwrap(),
367 };
368 assert_eq!(
369 index_interval_id_for_value(&unit, &i64::MIN.into()).unwrap(),
370 0
371 );
372 assert_eq!(
373 index_interval_id_for_value(&unit, &0i64.into()).unwrap(),
374 SIGN_BIT
375 );
376 assert_eq!(
377 index_interval_id_for_value(&unit, &i64::MAX.into()).unwrap(),
378 u64::MAX
379 );
380 }
381
382 #[test]
383 fn uint64_mapping_is_exact_through_max() {
384 let unit_granularity_kind = IndexKind::UInt64 {
385 index_granularity: NonZeroU64::new(1).unwrap(),
386 };
387 for value in [0, i64::MAX as u64 + 1, u64::MAX] {
388 assert_eq!(
389 index_interval_id_for_value(&unit_granularity_kind, &value.into()).unwrap(),
390 value
391 );
392 }
393
394 let ten_value_granularity_kind = IndexKind::UInt64 {
395 index_granularity: NonZeroU64::new(10).unwrap(),
396 };
397 assert_eq!(
398 index_interval_id_for_value(&ten_value_granularity_kind, &u64::MAX.into()).unwrap(),
399 u64::MAX / 10
400 );
401 }
402
403 #[test]
404 fn index_intervals_use_configured_index_units() {
405 let signed_unit = IndexKind::Int64 {
406 index_granularity: NonZeroU64::new(1).unwrap(),
407 };
408 let signed_unit_interval_id =
409 index_interval_id_for_value(&signed_unit, &50_464i64.into()).unwrap();
410 assert_eq!(
411 index_interval_for_id(&signed_unit, signed_unit_interval_id)
412 .unwrap()
413 .to_string(),
414 "[50464, 50465)"
415 );
416
417 let signed = IndexKind::Int64 {
418 index_granularity: NonZeroU64::new(10).unwrap(),
419 };
420 let signed_interval_id = index_interval_id_for_value(&signed, &(-11i64).into()).unwrap();
421 assert_eq!(
422 index_interval_for_id(&signed, signed_interval_id)
423 .unwrap()
424 .to_string(),
425 "[-20, -10)"
426 );
427
428 let unsigned = IndexKind::UInt64 {
429 index_granularity: NonZeroU64::new(10).unwrap(),
430 };
431 let unsigned_interval_id =
432 index_interval_id_for_value(&unsigned, &50_464u64.into()).unwrap();
433 assert_eq!(
434 index_interval_for_id(&unsigned, unsigned_interval_id)
435 .unwrap()
436 .to_string(),
437 "[50460, 50470)"
438 );
439
440 let timestamp = timestamp_kind(TimeIndexGranularity::Hours(1));
441 let epoch = Utc.timestamp_opt(0, 0).single().unwrap();
442 let timestamp_interval_id = index_interval_id_for_value(×tamp, &epoch.into()).unwrap();
443 assert_eq!(
444 index_interval_for_id(×tamp, timestamp_interval_id)
445 .unwrap()
446 .to_string(),
447 "[1970-01-01T00:00:00Z, 1970-01-01T01:00:00Z)"
448 );
449
450 let before_epoch = Utc.timestamp_opt(-1, 0).single().unwrap();
451 let before_epoch_interval_id =
452 index_interval_id_for_value(×tamp, &before_epoch.into()).unwrap();
453 assert_eq!(
454 index_interval_for_id(×tamp, before_epoch_interval_id)
455 .unwrap()
456 .to_string(),
457 "[1969-12-31T23:00:00Z, 1970-01-01T00:00:00Z)"
458 );
459 }
460
461 #[test]
462 fn index_intervals_clip_at_domain_maximum() -> Result<(), IndexIntervalMappingError> {
463 let signed = IndexKind::Int64 {
464 index_granularity: NonZeroU64::new(10).unwrap(),
465 };
466 let signed_range = index_interval_for_id(
467 &signed,
468 index_interval_id_for_value(&signed, &i64::MAX.into()).unwrap(),
469 )?;
470 assert_eq!(signed_range.end(), &IndexValue::Int64(i64::MAX));
471 assert!(signed_range.end_inclusive());
472 let signed_min_range = index_interval_for_id(
473 &signed,
474 index_interval_id_for_value(&signed, &i64::MIN.into()).unwrap(),
475 )?;
476 assert_eq!(signed_min_range.start(), &IndexValue::Int64(i64::MIN));
477 assert!(!signed_min_range.end_inclusive());
478
479 let unsigned = IndexKind::UInt64 {
480 index_granularity: NonZeroU64::new(10).unwrap(),
481 };
482 let unsigned_range = index_interval_for_id(
483 &unsigned,
484 index_interval_id_for_value(&unsigned, &u64::MAX.into()).unwrap(),
485 )?;
486 assert_eq!(unsigned_range.end(), &IndexValue::UInt64(u64::MAX));
487 assert!(unsigned_range.end_inclusive());
488
489 let timestamp = timestamp_kind(TimeIndexGranularity::Days(u32::MAX));
490 let timestamp_range = index_interval_for_id(
491 ×tamp,
492 index_interval_id_for_value(
493 ×tamp,
494 &IndexValue::Timestamp(DateTime::<Utc>::MAX_UTC),
495 )?,
496 )?;
497 assert_eq!(
498 timestamp_range.end(),
499 &IndexValue::Timestamp(DateTime::<Utc>::MAX_UTC)
500 );
501 assert!(timestamp_range.end_inclusive());
502 let timestamp_min_range = index_interval_for_id(
503 ×tamp,
504 index_interval_id_for_value(
505 ×tamp,
506 &IndexValue::Timestamp(DateTime::<Utc>::MIN_UTC),
507 )?,
508 )?;
509 assert_eq!(
510 timestamp_min_range.start(),
511 &IndexValue::Timestamp(DateTime::<Utc>::MIN_UTC)
512 );
513
514 Ok(())
515 }
516
517 #[test]
518 fn index_interval_rejects_unreachable_interval_id() {
519 let kind = IndexKind::UInt64 {
520 index_granularity: NonZeroU64::new(2).unwrap(),
521 };
522 assert!(matches!(
523 index_interval_for_id(&kind, u64::MAX),
524 Err(IndexIntervalMappingError::IntervalIdOutsideDomain { .. })
525 ));
526 }
527
528 #[test]
529 fn half_open_integer_ranges_do_not_cross_end_boundary() {
530 let signed = IndexKind::Int64 {
531 index_granularity: NonZeroU64::new(10).unwrap(),
532 };
533 let range = index_interval_id_range(&signed, &0i64.into(), &20i64.into()).unwrap();
534 assert_eq!(range, SIGN_BIT..=SIGN_BIT + 1);
535 assert_eq!(
536 index_interval_id_for_exclusive_end(&signed, &20i64.into()).unwrap(),
537 SIGN_BIT + 1
538 );
539
540 let unsigned = IndexKind::UInt64 {
541 index_granularity: NonZeroU64::new(10).unwrap(),
542 };
543 assert_eq!(
544 index_interval_id_range(&unsigned, &0u64.into(), &20u64.into()).unwrap(),
545 0..=1
546 );
547 assert_eq!(
548 index_interval_id_range(&unsigned, &0u64.into(), &1u64.into()).unwrap(),
549 0..=0
550 );
551 }
552
553 #[test]
554 fn half_open_timestamp_range_preserves_nanoseconds() {
555 let kind = timestamp_kind(TimeIndexGranularity::Seconds(1));
556 let start = Utc.timestamp_opt(0, 0).single().unwrap();
557 let boundary = Utc.timestamp_opt(2, 0).single().unwrap();
558
559 assert_eq!(
560 index_interval_id_range(&kind, &start.into(), &boundary.into()).unwrap(),
561 SIGN_BIT..=SIGN_BIT + 1
562 );
563 assert_eq!(
564 index_interval_id_range(
565 &kind,
566 &start.into(),
567 &(boundary + Duration::nanoseconds(1)).into(),
568 )
569 .unwrap(),
570 SIGN_BIT..=SIGN_BIT + 2
571 );
572 }
573
574 #[test]
575 fn invalid_domains_ranges_and_zero_time_granularities_are_errors() {
576 let kind = timestamp_kind(TimeIndexGranularity::Seconds(0));
577 let epoch = Utc.timestamp_opt(0, 0).single().unwrap();
578 assert_eq!(
579 index_interval_id_for_value(&kind, &epoch.into()),
580 Err(IndexIntervalMappingError::ZeroTimeIndexGranularity)
581 );
582
583 let unsigned = IndexKind::UInt64 {
584 index_granularity: NonZeroU64::new(1).unwrap(),
585 };
586 assert!(matches!(
587 index_interval_id_range(&unsigned, &0i64.into(), &1i64.into()),
588 Err(IndexIntervalMappingError::IndexValue { .. })
589 ));
590 assert!(matches!(
591 index_interval_id_range(&unsigned, &1u64.into(), &1u64.into()),
592 Err(IndexIntervalMappingError::IndexValue { .. })
593 ));
594 assert!(matches!(
595 index_interval_id_for_exclusive_end(&unsigned, &0u64.into()),
596 Err(IndexIntervalMappingError::RangeEndUnderflow { .. })
597 ));
598 }
599}