unobtanium 3.0.0

Opinioated Web search engine library with crawler and viewer companion.
Documentation
use chrono::TimeZone;
use chrono::DateTime;
use chrono::TimeDelta;
use chrono::offset::Utc;
use serde::{Serialize,Deserialize};

use std::mem::swap;
use std::ops::Sub;
use std::time::Duration;

/// A Timespan is an optional `start` time and optional `end` time grouped together.
///
/// Both Timestamps are in Utc Time.
///
/// `None` is used to represent open ended timespans:
/// * The start being None means the Timespan extends infinitely into the past
/// * The end being None means the Timespan extends infinitely into the future
///
/// The `start` always being before `end` is guaranteed.
///
/// Also see [UncheckedUtcTimespan].
#[derive(Clone,Debug,Default,PartialEq,Eq,Serialize,Deserialize)]
#[serde(try_from="UncheckedUtcTimespan")]
pub struct UtcTimespan {
	start: Option<DateTime<Utc>>,
	end: Option<DateTime<Utc>>,
}

/// Creation and manipulation methods
impl UtcTimespan {

	/// Construct a new, empty Timespan
	///
	/// It is open ended, both in the past and future.
	pub fn infinite() -> Self {
		UtcTimespan { start: None, end: None }
	}

	/// Construct a new Timespan from point in time a to point in time b
	///
	/// if b is before a the two get swapped.
	pub fn new(a: Option<DateTime<Utc>>, b: Option<DateTime<Utc>>) -> Self {
		let mut ts = UtcTimespan { start: a, end: b };
		ts.fix_if_reversed();
		return ts;
	}

	/// Same as [new()][Self::new] but errors with a human readable message
	/// if end is before start.
	pub fn new_with_loud_error(
		start: Option<DateTime<Utc>>, end: Option<DateTime<Utc>>
	) -> Result<Self,String> {
		let ts = UtcTimespan { start, end };
		if ts.is_reversed() {
			return Err("Timespan: End can't be before Start if both are defined.".to_string());
		} else {
			return Ok(ts);
		}
	}

	/// Sets the start of this Timespan.
	///
	/// If the new start is after the end, the end datetime will be set
	/// equal to the start datetime.
	pub fn set_start(&mut self, new_start: Option<DateTime<Utc>>) {
		self.start = new_start;
		if self.is_reversed() {
			self.end = self.start;
		}
	}

	/// Sets the end of this Timespan.
	///
	/// If the new end is before the start, the end datetime will be set
	/// equal to the start datetime.
	pub fn set_end(&mut self, new_end: Option<DateTime<Utc>>) {
		self.end = new_end;
		if self.is_reversed() {
			self.start = self.end;
		}
	}

}

/// Querying methods
impl UtcTimespan {
	/// Query the start of the timespan
	pub fn start(&self) -> &Option<DateTime<Utc>> {
		&self.start
	}
	
	/// Query the end of the timespan
	pub fn end(&self) -> &Option<DateTime<Utc>> {
		&self.end
	}

	/// Query the time-delta between start and end, always positive.
	///
	/// Returns `None` if `start`, `end` or both are `None`.
	pub fn delta(&self) -> Option<TimeDelta> {
		match self {
		    UtcTimespan { start: Some(start), end: Some(end) } =>
			    Some((*end).sub(start)),
		    _ => None
		}
	}

	/// Convenience warpper around [delta()][Self::delta] to produce a Duration.
	pub fn duration(&self) -> Option<Duration> {
		self.delta().map(|d| d.abs().to_std().expect("time delta must be absolute"))
	}
	
	/// Wheter the Timespan has either no start or no end.
	pub fn is_infinite(&self) -> bool {
		return self.start.is_none() || self.end.is_none();
	}

	/// Wheter the Timespan has no start and no end.
	pub fn is_all_of_time(&self) -> bool {
		return self.start.is_none() && self.end.is_none();
	}

	/// Whether both start and end are defined and have an equal datetime value.
	pub fn is_point(&self) -> bool {
		return self.start == self.end && self.start.is_some();
	}

	/// Wheter the given time is whitin the own timespan.
	pub fn contains_point(&self, time: &DateTime<Utc>) -> bool {
		if self.is_point_before_start(time) {
			return false;
		}
		if self.is_point_after_end(time) {
			return false;
		}
		return true;
	}

	/// Wheter a given point in time is before the start this timespan
	pub fn is_point_before_start(&self, time: &DateTime<Utc>) -> bool {
		if let Some(ref start) = self.start {
			return time < start;
		}
		return false;
	}

	/// Wheter a given point in time is after the start of this timespan
	pub fn is_point_after_start(&self, time: &DateTime<Utc>) -> bool {
		if let Some(ref start) = self.start {
			return start < time;
		}
		return true;
	}

	/// Wheter a given point in time is before the end this timespan
	pub fn is_point_before_end(&self, time: &DateTime<Utc>) -> bool {
		if let Some(ref end) = self.end {
			return time < end;
		}
		return true;
	}

	/// Wheter a given point in time is after the end of this timespan
	pub fn is_point_after_end(&self, time: &DateTime<Utc>) -> bool {
		if let Some(ref end) = self.end {
			return end < time;
		}
		return false;
	}

	/// Wheter the other timespan is fully contained in our own timespan.
	pub fn contains_span(&self, other: &UtcTimespan) -> bool {
		match other {
			UtcTimespan { start: None, end: None } => {
				return self.is_all_of_time();
			},
			UtcTimespan { start: None, end: Some(end) } => {
				return self.start.is_none() && !self.is_point_after_end(end);
			},
			UtcTimespan { start: Some(start), end: None } => {
				return !self.is_point_before_start(start) && self.end.is_none();
			},
			UtcTimespan { start: Some(start), end: Some(end) } => {
				return !self.is_point_before_start(start) && !self.is_point_after_end(end);
			},
		}
	}

	/// Returns the timespan matching the overlap between this and the other given
	/// 
	pub fn calculate_overlap(&self, other: &UtcTimespan) -> Option<UtcTimespan> {
		let span = UtcTimespan {
			start: max_of(self.start.as_ref(), other.start.as_ref()),
			end: min_of(self.end.as_ref(), other.end.as_ref()),
		};
		// If the smaller end and the bigger start end up with
		// a reversed timespan, we know that there is no intersection.
		if span.is_reversed() {
		    return None;
		} else {
			return Some(span);
		}
	}
}

fn min_of<Tz: TimeZone>(
	a: Option<&DateTime<Tz>>,
	b: Option<&DateTime<Tz>>
) -> Option<DateTime<Tz>> {
	match (a,b) {
		(Some(a), Some(b)) => if a < b { Some(a) } else { Some(b) },
		(Some(a), None) => Some(a),
		(_, b) => b,
	}.cloned()
}

fn max_of<Tz: TimeZone>(
	a: Option<&DateTime<Tz>>,
	b: Option<&DateTime<Tz>>
) -> Option<DateTime<Tz>> {
	match (a,b) {
		(Some(a), Some(b)) => if a > b { Some(a) } else { Some(b) },
		(Some(a), None) => Some(a),
		(_, b) => b,
	}.cloned()
}

/// Private helper methods
impl UtcTimespan {
	fn is_reversed(&self) -> bool {
		if let Some(ref start) = self.start {
			if let Some(ref end) = self.end {
				return start > end;
			}
		}
		return false;
	}

	fn fix_if_reversed(&mut self) {
		if self.is_reversed() {
			swap(&mut self.start, &mut self.end);
		}
	}

}


/// Same as [UtcTimespan], but without the guarantee of
/// `end` alwys being after `start`.
///
/// Provided is a TryFrom and an sliently swapping option.
#[derive(Clone,Debug,Default,PartialEq,Eq,Serialize,Deserialize)]
pub struct UncheckedUtcTimespan {
	pub start: Option<DateTime<Utc>>,
	pub end: Option<DateTime<Utc>>,
}

impl UncheckedUtcTimespan{

	/// Uses the [UtcTimespan::new] function to generate
	pub fn to_checked_with_silent_swap(self) -> UtcTimespan {
		UtcTimespan::new(self.start, self.end)
	}

	/// Uses the [UtcTimespan::new] function to generate
	pub fn to_checked_with_loud_error(self) -> Result<UtcTimespan,String> {
		UtcTimespan::new_with_loud_error(self.start, self.end)
	}

}

impl TryFrom<UncheckedUtcTimespan> for UtcTimespan {
	type Error = String;
	
	fn try_from(value: UncheckedUtcTimespan) -> Result<Self, Self::Error> {
		value.to_checked_with_loud_error()
	}
}