ops: add resources with availability windows and capacity
This commit is contained in:
parent
9d8773d6ab
commit
1d44a6d8ea
|
|
@ -193,6 +193,9 @@ version = "0.1.0"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "apsis-resource"
|
name = "apsis-resource"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"apsis-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "apsis-rf"
|
name = "apsis-rf"
|
||||||
|
|
|
||||||
|
|
@ -11,3 +11,4 @@ publish = false
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
apsis-core.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
//! Errors reported while building or querying a resource.
|
||||||
|
|
||||||
|
use core::fmt;
|
||||||
|
|
||||||
|
/// The category of a resource error.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum ResourceErrorKind {
|
||||||
|
/// The end of an interval precedes its start.
|
||||||
|
ReversedInterval,
|
||||||
|
/// A time or duration is not finite.
|
||||||
|
NotFinite,
|
||||||
|
/// A duration is negative.
|
||||||
|
NegativeDuration,
|
||||||
|
/// The capacity is not positive and finite.
|
||||||
|
InvalidCapacity,
|
||||||
|
/// No resource has the requested identifier.
|
||||||
|
NoSuchResource,
|
||||||
|
/// A resource with the identifier already exists.
|
||||||
|
DuplicateResource,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ResourceErrorKind {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let text = match self {
|
||||||
|
Self::ReversedInterval => "the interval ends before it starts",
|
||||||
|
Self::NotFinite => "a time is not finite",
|
||||||
|
Self::NegativeDuration => "the duration is negative",
|
||||||
|
Self::InvalidCapacity => "the capacity is not usable",
|
||||||
|
Self::NoSuchResource => "no such resource",
|
||||||
|
Self::DuplicateResource => "a resource with that identifier exists",
|
||||||
|
};
|
||||||
|
f.write_str(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A failure while building or querying a resource.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ResourceError {
|
||||||
|
kind: ResourceErrorKind,
|
||||||
|
detail: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResourceError {
|
||||||
|
/// Creates an error with the given category.
|
||||||
|
pub fn new(kind: ResourceErrorKind) -> Self {
|
||||||
|
Self { kind, detail: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attaches a description of the values that caused the failure.
|
||||||
|
pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
|
||||||
|
self.detail = Some(detail.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the category of this failure.
|
||||||
|
pub fn kind(&self) -> ResourceErrorKind {
|
||||||
|
self.kind
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the detail text attached to this error, if any.
|
||||||
|
pub fn detail(&self) -> Option<&str> {
|
||||||
|
self.detail.as_deref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ResourceError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match &self.detail {
|
||||||
|
Some(detail) => write!(f, "{}: {detail}", self.kind),
|
||||||
|
None => write!(f, "{}", self.kind),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::error::Error for ResourceError {}
|
||||||
|
|
@ -1 +1,45 @@
|
||||||
//! Resource modeling for satellite operations.
|
//! Resource modeling for satellite operations.
|
||||||
|
//!
|
||||||
|
//! A resource is anything a task can occupy: a satellite, a ground station, a
|
||||||
|
//! transmitter, an onboard recorder. This crate models the two things a
|
||||||
|
//! scheduler needs to know about one: when it is available, and how much of
|
||||||
|
//! it a task consumes.
|
||||||
|
//!
|
||||||
|
//! Availability is expressed as a set of disjoint intervals on a single time
|
||||||
|
//! axis, called windows. A resource is usable at an instant when that instant
|
||||||
|
//! falls inside one of its windows. The windows may come from anywhere: the
|
||||||
|
//! access analysis in `apsis-access` produces them for a ground station, and
|
||||||
|
//! an operator enters maintenance periods by hand.
|
||||||
|
//!
|
||||||
|
//! Capacity is a positive number whose meaning is the caller's. A satellite
|
||||||
|
//! that can run one observation at a time has a capacity of one; a recorder
|
||||||
|
//! has a capacity in megabits; a power bus has one in watts. Two tasks
|
||||||
|
//! conflict only when their demands together exceed the capacity, which lets
|
||||||
|
//! the same type express exclusive and shared resources.
|
||||||
|
//!
|
||||||
|
//! Times are in seconds from an origin chosen by the caller, so the same
|
||||||
|
//! model serves a scenario clock, an elapsed time, and a Julian date offset.
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! use apsis_resource::{Resource, ResourceId, Window};
|
||||||
|
//!
|
||||||
|
//! // A station available from 100 to 200 seconds.
|
||||||
|
//! let mut resource = Resource::new(ResourceId::new(0), 1.0);
|
||||||
|
//! resource.add_window(Window::new(100.0, 200.0)).expect("the window is usable");
|
||||||
|
//!
|
||||||
|
//! assert!(resource.is_available_at(150.0));
|
||||||
|
//! assert!(!resource.is_available_at(250.0));
|
||||||
|
//! assert!(resource.is_available_over(120.0, 80.0));
|
||||||
|
//! assert!(!resource.is_available_over(150.0, 100.0));
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
#![deny(missing_docs)]
|
||||||
|
|
||||||
|
mod error;
|
||||||
|
mod resource;
|
||||||
|
mod window;
|
||||||
|
|
||||||
|
pub use error::{ResourceError, ResourceErrorKind};
|
||||||
|
pub use resource::{Resource, ResourceId, ResourceSet};
|
||||||
|
pub use window::{Interval, Window, merge_windows, subtract_window};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,339 @@
|
||||||
|
//! Resources, their availability, and their capacity.
|
||||||
|
|
||||||
|
use crate::error::{ResourceError, ResourceErrorKind};
|
||||||
|
use crate::window::{Interval, Window, merge_windows, subtract_window};
|
||||||
|
|
||||||
|
/// An identifier for a resource.
|
||||||
|
///
|
||||||
|
/// The identifier is an index chosen by the caller. It is deliberately opaque
|
||||||
|
/// to this crate so that a caller can use whatever numbering its own model
|
||||||
|
/// provides.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
|
pub struct ResourceId(pub usize);
|
||||||
|
|
||||||
|
impl ResourceId {
|
||||||
|
/// Creates an identifier.
|
||||||
|
pub const fn new(index: usize) -> Self {
|
||||||
|
Self(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the index.
|
||||||
|
pub const fn index(self) -> usize {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::fmt::Display for ResourceId {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
write!(f, "resource {}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A resource with availability windows and a capacity.
|
||||||
|
///
|
||||||
|
/// The capacity is a positive number whose unit the caller chooses. A task
|
||||||
|
/// that occupies more than the capacity of a resource cannot be scheduled on
|
||||||
|
/// it at all; two tasks conflict when their demands together exceed the
|
||||||
|
/// capacity at some instant.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct Resource {
|
||||||
|
/// The identifier of this resource.
|
||||||
|
pub id: ResourceId,
|
||||||
|
/// The capacity, in a unit the caller chooses.
|
||||||
|
///
|
||||||
|
/// A capacity of one with unit demands gives an exclusive resource, where
|
||||||
|
/// no two tasks overlap. A larger capacity, or fractional demands, gives
|
||||||
|
/// a shared resource.
|
||||||
|
capacity: f64,
|
||||||
|
/// The availability windows, kept merged and in time order.
|
||||||
|
windows: Vec<Window>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Resource {
|
||||||
|
/// Creates a resource with no availability and the given capacity.
|
||||||
|
///
|
||||||
|
/// A resource with no windows is never available, so a caller adds
|
||||||
|
/// windows before using it. That is the safe default: a resource whose
|
||||||
|
/// availability was forgotten is unavailable rather than always
|
||||||
|
/// available.
|
||||||
|
pub const fn new(id: ResourceId, capacity: f64) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
capacity,
|
||||||
|
windows: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the capacity.
|
||||||
|
pub const fn capacity(&self) -> f64 {
|
||||||
|
self.capacity
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the capacity.
|
||||||
|
///
|
||||||
|
/// Returns an error when the capacity is not positive and finite.
|
||||||
|
pub fn set_capacity(&mut self, capacity: f64) -> Result<(), ResourceError> {
|
||||||
|
if !capacity.is_finite() || capacity <= 0.0 {
|
||||||
|
return Err(ResourceError::new(ResourceErrorKind::InvalidCapacity)
|
||||||
|
.with_detail(format!("capacity {capacity}")));
|
||||||
|
}
|
||||||
|
self.capacity = capacity;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` when the capacity is usable.
|
||||||
|
pub fn has_valid_capacity(&self) -> bool {
|
||||||
|
self.capacity.is_finite() && self.capacity > 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the availability windows, merged and in time order.
|
||||||
|
pub fn windows(&self) -> &[Window] {
|
||||||
|
&self.windows
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of availability windows.
|
||||||
|
pub fn window_count(&self) -> usize {
|
||||||
|
self.windows.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` when the resource has no availability at all.
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.windows.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replaces the availability with a list of windows.
|
||||||
|
///
|
||||||
|
/// The windows are merged, so the stored form is canonical whatever order
|
||||||
|
/// the caller supplies them in.
|
||||||
|
///
|
||||||
|
/// Returns an error when a window is not usable.
|
||||||
|
pub fn set_windows(&mut self, windows: &[Window]) -> Result<(), ResourceError> {
|
||||||
|
self.windows = merge_windows(windows)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds one availability window.
|
||||||
|
///
|
||||||
|
/// The window is merged into the existing set.
|
||||||
|
///
|
||||||
|
/// Returns an error when the window is not usable.
|
||||||
|
pub fn add_window(&mut self, window: Window) -> Result<(), ResourceError> {
|
||||||
|
window.validate()?;
|
||||||
|
let mut all = self.windows.clone();
|
||||||
|
all.push(window);
|
||||||
|
self.windows = merge_windows(&all)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes an availability window.
|
||||||
|
///
|
||||||
|
/// The window is subtracted from the existing set, which may split a
|
||||||
|
/// stored window in two.
|
||||||
|
///
|
||||||
|
/// Returns an error when the window is not usable.
|
||||||
|
pub fn remove_window(&mut self, removed: Window) -> Result<(), ResourceError> {
|
||||||
|
self.windows = subtract_window(&self.windows, removed)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` when the resource is available at an instant.
|
||||||
|
///
|
||||||
|
/// An instantaneous query is a query of zero duration, which is satisfied
|
||||||
|
/// when the instant is inside a window. At the end of a window the
|
||||||
|
/// resource is not available, because the window is half-open.
|
||||||
|
pub fn is_available_at(&self, time: f64) -> bool {
|
||||||
|
self.windows
|
||||||
|
.iter()
|
||||||
|
.any(|window| window.interval.contains(time))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` when the resource is available for a whole interval.
|
||||||
|
///
|
||||||
|
/// A resource can satisfy a request of zero duration whenever it is
|
||||||
|
/// available at the requested instant, so a zero duration is checked as
|
||||||
|
/// an instant.
|
||||||
|
pub fn is_available_over(&self, start: f64, duration: f64) -> bool {
|
||||||
|
if !start.is_finite() || !duration.is_finite() || duration < 0.0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if duration == 0.0 {
|
||||||
|
return self.is_available_at(start);
|
||||||
|
}
|
||||||
|
let requested = Interval::new(start, start + duration);
|
||||||
|
self.windows
|
||||||
|
.iter()
|
||||||
|
.any(|window| window.interval.contains_interval(&requested))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the availability that falls inside an interval.
|
||||||
|
///
|
||||||
|
/// The result is the intersection of the stored windows with the query,
|
||||||
|
/// which is what a scheduler needs to know when it asks what part of its
|
||||||
|
/// planning horizon a resource is usable for.
|
||||||
|
pub fn availability_within(&self, query: Interval) -> Vec<Window> {
|
||||||
|
self.windows
|
||||||
|
.iter()
|
||||||
|
.filter_map(|window| window.interval.intersection(&query))
|
||||||
|
.map(Window::from_interval)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the total availability, in seconds.
|
||||||
|
///
|
||||||
|
/// The windows are disjoint, so the durations add.
|
||||||
|
pub fn total_availability(&self) -> f64 {
|
||||||
|
self.windows.iter().map(Window::duration).sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the first window that can hold a request.
|
||||||
|
///
|
||||||
|
/// This is the earliest time at which the resource can satisfy a request
|
||||||
|
/// of the given duration, which is what a greedy scheduler asks. Returns
|
||||||
|
/// `None` when no window is long enough.
|
||||||
|
pub fn earliest_fit(&self, duration: f64) -> Option<Window> {
|
||||||
|
if !duration.is_finite() || duration < 0.0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
self.windows
|
||||||
|
.iter()
|
||||||
|
.find(|window| window.duration() >= duration)
|
||||||
|
.map(|window| Window::new(window.start(), window.start() + duration))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the first window that can hold a request starting no earlier
|
||||||
|
/// than a given time.
|
||||||
|
///
|
||||||
|
/// Returns `None` when no window is both late enough and long enough.
|
||||||
|
pub fn earliest_fit_after(&self, not_before: f64, duration: f64) -> Option<Window> {
|
||||||
|
if !not_before.is_finite() || !duration.is_finite() || duration < 0.0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
self.windows.iter().find_map(|window| {
|
||||||
|
let start = window.start().max(not_before);
|
||||||
|
let end = start + duration;
|
||||||
|
(end <= window.end()).then(|| Window::new(start, end))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the time at which the resource stops being available.
|
||||||
|
///
|
||||||
|
/// Returns `None` when the resource is not available at the given
|
||||||
|
/// instant, and infinity when the window that contains it is unbounded.
|
||||||
|
pub fn busy_until(&self, time: f64) -> Option<f64> {
|
||||||
|
self.windows
|
||||||
|
.iter()
|
||||||
|
.find(|window| window.interval.contains(time))
|
||||||
|
.map(|window| window.end())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A collection of resources, indexed by identifier.
|
||||||
|
///
|
||||||
|
/// The set keeps the resources in the order they were added and provides the
|
||||||
|
/// lookups a scheduler needs: by identifier, and by the resource that has a
|
||||||
|
/// window covering an instant.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Default)]
|
||||||
|
pub struct ResourceSet {
|
||||||
|
resources: Vec<Resource>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResourceSet {
|
||||||
|
/// Creates an empty set.
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
resources: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a set from a list of resources.
|
||||||
|
///
|
||||||
|
/// Returns an error when two resources share an identifier, because a
|
||||||
|
/// lookup would then be ambiguous.
|
||||||
|
pub fn from_resources(resources: Vec<Resource>) -> Result<Self, ResourceError> {
|
||||||
|
let mut set = Self::new();
|
||||||
|
for resource in resources {
|
||||||
|
set.insert(resource)?;
|
||||||
|
}
|
||||||
|
Ok(set)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inserts a resource.
|
||||||
|
///
|
||||||
|
/// Returns an error when the identifier is already present.
|
||||||
|
pub fn insert(&mut self, resource: Resource) -> Result<(), ResourceError> {
|
||||||
|
if self.get(resource.id).is_some() {
|
||||||
|
return Err(ResourceError::new(ResourceErrorKind::DuplicateResource)
|
||||||
|
.with_detail(format!("{}", resource.id)));
|
||||||
|
}
|
||||||
|
self.resources.push(resource);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the resource with the given identifier.
|
||||||
|
pub fn get(&self, id: ResourceId) -> Option<&Resource> {
|
||||||
|
self.resources.iter().find(|resource| resource.id == id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the resource with the given identifier for modification.
|
||||||
|
pub fn get_mut(&mut self, id: ResourceId) -> Option<&mut Resource> {
|
||||||
|
self.resources.iter_mut().find(|resource| resource.id == id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes a resource.
|
||||||
|
///
|
||||||
|
/// Returns an error when no resource has the identifier.
|
||||||
|
pub fn remove(&mut self, id: ResourceId) -> Result<Resource, ResourceError> {
|
||||||
|
let index = self
|
||||||
|
.resources
|
||||||
|
.iter()
|
||||||
|
.position(|resource| resource.id == id)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ResourceError::new(ResourceErrorKind::NoSuchResource).with_detail(format!("{id}"))
|
||||||
|
})?;
|
||||||
|
Ok(self.resources.remove(index))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the resources in the order they were added.
|
||||||
|
pub fn resources(&self) -> &[Resource] {
|
||||||
|
&self.resources
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of resources.
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.resources.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` when there are no resources.
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.resources.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the identifiers of the resources available at an instant.
|
||||||
|
///
|
||||||
|
/// The result is in the order the resources were added.
|
||||||
|
pub fn available_at(&self, time: f64) -> Vec<ResourceId> {
|
||||||
|
self.resources
|
||||||
|
.iter()
|
||||||
|
.filter(|resource| resource.is_available_at(time))
|
||||||
|
.map(|resource| resource.id)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the identifiers of the resources available for a whole
|
||||||
|
/// interval.
|
||||||
|
pub fn available_over(&self, start: f64, duration: f64) -> Vec<ResourceId> {
|
||||||
|
self.resources
|
||||||
|
.iter()
|
||||||
|
.filter(|resource| resource.is_available_over(start, duration))
|
||||||
|
.map(|resource| resource.id)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the total availability of every resource, in seconds.
|
||||||
|
pub fn total_availability(&self) -> f64 {
|
||||||
|
self.resources
|
||||||
|
.iter()
|
||||||
|
.map(Resource::total_availability)
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,240 @@
|
||||||
|
//! Availability windows and the interval algebra over them.
|
||||||
|
|
||||||
|
use crate::error::{ResourceError, ResourceErrorKind};
|
||||||
|
use core::fmt;
|
||||||
|
|
||||||
|
/// A half-open interval of time, `[start, end)`.
|
||||||
|
///
|
||||||
|
/// The half-open convention is the one that makes intervals tile without
|
||||||
|
/// overlap or gap: an interval that ends where another begins shares only the
|
||||||
|
/// instant at the boundary, which belongs to the second. Two intervals are
|
||||||
|
/// therefore disjoint when the end of the first is at or before the start of
|
||||||
|
/// the second.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct Interval {
|
||||||
|
/// The start of the interval, in seconds.
|
||||||
|
pub start: f64,
|
||||||
|
/// The end of the interval, in seconds.
|
||||||
|
pub end: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Interval {
|
||||||
|
/// Creates an interval.
|
||||||
|
pub const fn new(start: f64, end: f64) -> Self {
|
||||||
|
Self { start, end }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the duration of the interval, in seconds.
|
||||||
|
///
|
||||||
|
/// The value is zero for an empty interval and positive otherwise.
|
||||||
|
pub fn duration(&self) -> f64 {
|
||||||
|
(self.end - self.start).max(0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` when the interval contains no time.
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.end <= self.start
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` when the interval is finite and ordered.
|
||||||
|
pub fn is_valid(&self) -> bool {
|
||||||
|
self.start.is_finite() && self.end.is_finite() && self.end >= self.start
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` when an instant lies inside the interval.
|
||||||
|
///
|
||||||
|
/// The start is included and the end is excluded, so a boundary instant
|
||||||
|
/// belongs to the interval that begins there.
|
||||||
|
pub fn contains(&self, time: f64) -> bool {
|
||||||
|
time >= self.start && time < self.end
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` when the interval fully contains another.
|
||||||
|
///
|
||||||
|
/// An empty interval is contained by any interval that contains its
|
||||||
|
/// instant, so a zero length query inside the range is satisfied.
|
||||||
|
pub fn contains_interval(&self, other: &Self) -> bool {
|
||||||
|
if other.is_empty() {
|
||||||
|
return other.start >= self.start && other.start <= self.end;
|
||||||
|
}
|
||||||
|
other.start >= self.start && other.end <= self.end
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` when two intervals share any time.
|
||||||
|
///
|
||||||
|
/// Two intervals that meet at a point do not overlap, because the point
|
||||||
|
/// belongs to only one of them under the half-open convention.
|
||||||
|
pub fn overlaps(&self, other: &Self) -> bool {
|
||||||
|
self.start < other.end && other.start < self.end
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the intersection of two intervals, or `None` when they do not
|
||||||
|
/// overlap.
|
||||||
|
pub fn intersection(&self, other: &Self) -> Option<Self> {
|
||||||
|
if !self.overlaps(other) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(Self::new(
|
||||||
|
self.start.max(other.start),
|
||||||
|
self.end.min(other.end),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the shortest interval containing both.
|
||||||
|
pub fn hull(&self, other: &Self) -> Self {
|
||||||
|
Self::new(self.start.min(other.start), self.end.max(other.end))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` when the two intervals touch or overlap, so that they
|
||||||
|
/// can be merged without leaving a gap.
|
||||||
|
pub fn is_adjacent_to(&self, other: &Self) -> bool {
|
||||||
|
self.start <= other.end && other.start <= self.end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Interval {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "[{}, {})", self.start, self.end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An availability window of a resource.
|
||||||
|
///
|
||||||
|
/// This is an [`Interval`] under a name that reads better where the caller is
|
||||||
|
/// describing when a resource can be used rather than doing interval algebra.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct Window {
|
||||||
|
/// The interval during which the resource is available.
|
||||||
|
pub interval: Interval,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Window {
|
||||||
|
/// Creates a window from a start and an end time.
|
||||||
|
pub const fn new(start: f64, end: f64) -> Self {
|
||||||
|
Self {
|
||||||
|
interval: Interval::new(start, end),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a window from an interval.
|
||||||
|
pub const fn from_interval(interval: Interval) -> Self {
|
||||||
|
Self { interval }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the start of the window, in seconds.
|
||||||
|
pub const fn start(&self) -> f64 {
|
||||||
|
self.interval.start
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the end of the window, in seconds.
|
||||||
|
pub const fn end(&self) -> f64 {
|
||||||
|
self.interval.end
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the duration of the window, in seconds.
|
||||||
|
pub fn duration(&self) -> f64 {
|
||||||
|
self.interval.duration()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` when the window is usable.
|
||||||
|
pub fn is_valid(&self) -> bool {
|
||||||
|
self.interval.is_valid()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates the window, returning the reason when it is not usable.
|
||||||
|
pub fn validate(&self) -> Result<(), ResourceError> {
|
||||||
|
if !self.interval.start.is_finite() || !self.interval.end.is_finite() {
|
||||||
|
return Err(ResourceError::new(ResourceErrorKind::NotFinite)
|
||||||
|
.with_detail(format!("window {}", self.interval)));
|
||||||
|
}
|
||||||
|
if self.interval.end < self.interval.start {
|
||||||
|
return Err(ResourceError::new(ResourceErrorKind::ReversedInterval)
|
||||||
|
.with_detail(format!("window {}", self.interval)));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Window {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merges a list of windows into a list of disjoint windows in time order.
|
||||||
|
///
|
||||||
|
/// Windows that overlap or touch are combined, and empty windows are dropped.
|
||||||
|
/// The result is the canonical form of a set of availability windows: sorted,
|
||||||
|
/// disjoint, and non-empty, so that two sets can be compared directly.
|
||||||
|
///
|
||||||
|
/// Returns an error when a window is not usable.
|
||||||
|
pub fn merge_windows(windows: &[Window]) -> Result<Vec<Window>, ResourceError> {
|
||||||
|
for window in windows {
|
||||||
|
window.validate()?;
|
||||||
|
}
|
||||||
|
let mut intervals: Vec<Interval> = windows
|
||||||
|
.iter()
|
||||||
|
.map(|window| window.interval)
|
||||||
|
.filter(|interval| !interval.is_empty())
|
||||||
|
.collect();
|
||||||
|
if intervals.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
// Sorting on the start and then the end gives a deterministic order for
|
||||||
|
// intervals with equal starts.
|
||||||
|
intervals.sort_by(|first, second| {
|
||||||
|
first
|
||||||
|
.start
|
||||||
|
.partial_cmp(&second.start)
|
||||||
|
.unwrap_or(core::cmp::Ordering::Equal)
|
||||||
|
.then_with(|| {
|
||||||
|
first
|
||||||
|
.end
|
||||||
|
.partial_cmp(&second.end)
|
||||||
|
.unwrap_or(core::cmp::Ordering::Equal)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut merged: Vec<Interval> = Vec::with_capacity(intervals.len());
|
||||||
|
for interval in intervals {
|
||||||
|
match merged.last_mut() {
|
||||||
|
Some(current) if current.is_adjacent_to(&interval) => {
|
||||||
|
current.end = current.end.max(interval.end);
|
||||||
|
}
|
||||||
|
_ => merged.push(interval),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(merged.into_iter().map(Window::from_interval).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes a window from each of a list of windows.
|
||||||
|
///
|
||||||
|
/// The result is the same set of availability with the removed window taken
|
||||||
|
/// out. Windows that are entirely inside the removed one disappear, and one
|
||||||
|
/// that contains it is split in two.
|
||||||
|
///
|
||||||
|
/// Returns an error when any input window is not usable.
|
||||||
|
pub fn subtract_window(windows: &[Window], removed: Window) -> Result<Vec<Window>, ResourceError> {
|
||||||
|
removed.validate()?;
|
||||||
|
let merged = merge_windows(windows)?;
|
||||||
|
if removed.interval.is_empty() {
|
||||||
|
return Ok(merged);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut result = Vec::with_capacity(merged.len() + 1);
|
||||||
|
for window in merged {
|
||||||
|
match window.interval.intersection(&removed.interval) {
|
||||||
|
None => result.push(window),
|
||||||
|
Some(overlap) => {
|
||||||
|
// The part before the removed window, and the part after it.
|
||||||
|
if overlap.start > window.interval.start {
|
||||||
|
result.push(Window::new(window.interval.start, overlap.start));
|
||||||
|
}
|
||||||
|
if overlap.end < window.interval.end {
|
||||||
|
result.push(Window::new(overlap.end, window.interval.end));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,312 @@
|
||||||
|
//! Tests for resources and the set that holds them.
|
||||||
|
|
||||||
|
use apsis_resource::{Interval, Resource, ResourceErrorKind, ResourceId, ResourceSet, Window};
|
||||||
|
|
||||||
|
/// A resource with one window from 100 to 200 seconds.
|
||||||
|
fn station() -> Resource {
|
||||||
|
let mut resource = Resource::new(ResourceId::new(0), 1.0);
|
||||||
|
resource
|
||||||
|
.add_window(Window::new(100.0, 200.0))
|
||||||
|
.expect("the window is usable");
|
||||||
|
resource
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_new_resource_has_no_availability() {
|
||||||
|
let resource = Resource::new(ResourceId::new(3), 1.0);
|
||||||
|
assert!(resource.is_empty());
|
||||||
|
assert_eq!(resource.window_count(), 0);
|
||||||
|
assert_eq!(resource.total_availability(), 0.0);
|
||||||
|
assert!(!resource.is_available_at(0.0));
|
||||||
|
assert_eq!(resource.id, ResourceId::new(3));
|
||||||
|
assert!(!resource.is_available_at(f64::NAN));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn adding_a_window_makes_the_resource_available() {
|
||||||
|
let resource = station();
|
||||||
|
assert_eq!(resource.window_count(), 1);
|
||||||
|
assert_eq!(resource.total_availability(), 100.0);
|
||||||
|
assert!(resource.is_available_at(150.0));
|
||||||
|
assert!(!resource.is_available_at(50.0));
|
||||||
|
assert!(!resource.is_available_at(250.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn availability_is_half_open_at_the_edges() {
|
||||||
|
let resource = station();
|
||||||
|
assert!(resource.is_available_at(100.0));
|
||||||
|
assert!(resource.is_available_at(199.999));
|
||||||
|
// The end instant belongs to the next interval, so the resource stops
|
||||||
|
// being available exactly there.
|
||||||
|
assert!(!resource.is_available_at(200.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn availability_over_a_duration_requires_the_whole_span() {
|
||||||
|
let resource = station();
|
||||||
|
assert!(resource.is_available_over(100.0, 100.0));
|
||||||
|
assert!(resource.is_available_over(120.0, 80.0));
|
||||||
|
assert!(resource.is_available_over(150.0, 50.0));
|
||||||
|
// A request that reaches past the window fails, even by a little.
|
||||||
|
assert!(!resource.is_available_over(150.0, 50.001));
|
||||||
|
assert!(!resource.is_available_over(99.0, 2.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_zero_duration_request_is_an_instantaneous_query() {
|
||||||
|
let resource = station();
|
||||||
|
assert!(resource.is_available_over(150.0, 0.0));
|
||||||
|
assert!(!resource.is_available_over(250.0, 0.0));
|
||||||
|
// The half-open boundary applies here too.
|
||||||
|
assert!(!resource.is_available_over(200.0, 0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_request_with_an_invalid_duration_is_refused() {
|
||||||
|
let resource = station();
|
||||||
|
assert!(!resource.is_available_over(100.0, -1.0));
|
||||||
|
assert!(!resource.is_available_over(100.0, f64::NAN));
|
||||||
|
assert!(!resource.is_available_over(f64::NAN, 10.0));
|
||||||
|
assert!(!resource.is_available_over(100.0, f64::INFINITY));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn adding_overlapping_windows_merges_them() {
|
||||||
|
let mut resource = Resource::new(ResourceId::new(0), 1.0);
|
||||||
|
resource.add_window(Window::new(0.0, 10.0)).expect("usable");
|
||||||
|
resource.add_window(Window::new(5.0, 15.0)).expect("usable");
|
||||||
|
// The two overlap, so the stored form is one window.
|
||||||
|
assert_eq!(resource.window_count(), 1);
|
||||||
|
assert_eq!(resource.windows()[0], Window::new(0.0, 15.0));
|
||||||
|
assert_eq!(resource.total_availability(), 15.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn adding_disjoint_windows_keeps_them_separate() {
|
||||||
|
let mut resource = Resource::new(ResourceId::new(0), 1.0);
|
||||||
|
resource
|
||||||
|
.add_window(Window::new(30.0, 40.0))
|
||||||
|
.expect("usable");
|
||||||
|
resource.add_window(Window::new(0.0, 10.0)).expect("usable");
|
||||||
|
assert_eq!(resource.window_count(), 2);
|
||||||
|
// The stored order is time order, not insertion order.
|
||||||
|
assert_eq!(resource.windows()[0].start(), 0.0);
|
||||||
|
assert_eq!(resource.windows()[1].start(), 30.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn removing_a_window_splits_availability() {
|
||||||
|
let mut resource = station();
|
||||||
|
resource
|
||||||
|
.remove_window(Window::new(140.0, 160.0))
|
||||||
|
.expect("usable");
|
||||||
|
assert_eq!(resource.window_count(), 2);
|
||||||
|
assert_eq!(resource.windows()[0], Window::new(100.0, 140.0));
|
||||||
|
assert_eq!(resource.windows()[1], Window::new(160.0, 200.0));
|
||||||
|
assert!(!resource.is_available_at(150.0));
|
||||||
|
assert!(resource.is_available_at(130.0));
|
||||||
|
assert!(resource.is_available_at(180.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn setting_the_windows_replaces_them() {
|
||||||
|
let mut resource = station();
|
||||||
|
resource
|
||||||
|
.set_windows(&[Window::new(0.0, 10.0), Window::new(20.0, 30.0)])
|
||||||
|
.expect("usable");
|
||||||
|
assert_eq!(resource.window_count(), 2);
|
||||||
|
assert!(!resource.is_available_at(150.0));
|
||||||
|
assert!(resource.is_available_at(5.0));
|
||||||
|
|
||||||
|
// Setting nothing removes all availability.
|
||||||
|
resource.set_windows(&[]).expect("usable");
|
||||||
|
assert!(resource.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn availability_within_a_query_is_the_intersection() {
|
||||||
|
let resource = station();
|
||||||
|
let within = resource.availability_within(Interval::new(150.0, 250.0));
|
||||||
|
assert_eq!(within, vec![Window::new(150.0, 200.0)]);
|
||||||
|
|
||||||
|
let outside = resource.availability_within(Interval::new(300.0, 400.0));
|
||||||
|
assert!(outside.is_empty());
|
||||||
|
|
||||||
|
let covering = resource.availability_within(Interval::new(0.0, 1000.0));
|
||||||
|
assert_eq!(covering, resource.windows().to_vec());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_earliest_fit_is_the_first_window_long_enough() {
|
||||||
|
let mut resource = Resource::new(ResourceId::new(0), 1.0);
|
||||||
|
resource
|
||||||
|
.set_windows(&[Window::new(0.0, 10.0), Window::new(30.0, 60.0)])
|
||||||
|
.expect("usable");
|
||||||
|
|
||||||
|
// A request of five seconds fits in the first window.
|
||||||
|
assert_eq!(resource.earliest_fit(5.0), Some(Window::new(0.0, 5.0)));
|
||||||
|
// A request of twenty does not, so it takes the second.
|
||||||
|
assert_eq!(resource.earliest_fit(20.0), Some(Window::new(30.0, 50.0)));
|
||||||
|
// A request longer than any window has no answer.
|
||||||
|
assert_eq!(resource.earliest_fit(40.0), None);
|
||||||
|
// An instantaneous request fits at the start of the first window.
|
||||||
|
assert_eq!(resource.earliest_fit(0.0), Some(Window::new(0.0, 0.0)));
|
||||||
|
assert_eq!(resource.earliest_fit(-1.0), None);
|
||||||
|
assert_eq!(resource.earliest_fit(f64::NAN), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_earliest_fit_after_a_time_respects_the_lower_bound() {
|
||||||
|
let mut resource = Resource::new(ResourceId::new(0), 1.0);
|
||||||
|
resource
|
||||||
|
.set_windows(&[Window::new(0.0, 10.0), Window::new(30.0, 60.0)])
|
||||||
|
.expect("usable");
|
||||||
|
|
||||||
|
// Starting no earlier than five, the first window can still hold it.
|
||||||
|
assert_eq!(
|
||||||
|
resource.earliest_fit_after(5.0, 4.0),
|
||||||
|
Some(Window::new(5.0, 9.0))
|
||||||
|
);
|
||||||
|
// A request of eight seconds starting at five does not fit, because the
|
||||||
|
// window ends at ten, so the second window is used.
|
||||||
|
assert_eq!(
|
||||||
|
resource.earliest_fit_after(5.0, 8.0),
|
||||||
|
Some(Window::new(30.0, 38.0))
|
||||||
|
);
|
||||||
|
// Starting after the second window begins, the request starts there.
|
||||||
|
assert_eq!(
|
||||||
|
resource.earliest_fit_after(40.0, 10.0),
|
||||||
|
Some(Window::new(40.0, 50.0))
|
||||||
|
);
|
||||||
|
assert_eq!(resource.earliest_fit_after(100.0, 1.0), None);
|
||||||
|
assert_eq!(resource.earliest_fit_after(f64::NAN, 1.0), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_busy_until_time_is_the_end_of_the_containing_window() {
|
||||||
|
let resource = station();
|
||||||
|
assert_eq!(resource.busy_until(150.0), Some(200.0));
|
||||||
|
assert_eq!(resource.busy_until(50.0), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_capacity_is_validated() {
|
||||||
|
let mut resource = Resource::new(ResourceId::new(0), 1.0);
|
||||||
|
assert!(resource.has_valid_capacity());
|
||||||
|
assert!(resource.set_capacity(2.5).is_ok());
|
||||||
|
assert_eq!(resource.capacity(), 2.5);
|
||||||
|
|
||||||
|
for capacity in [0.0, -1.0, f64::NAN, f64::INFINITY] {
|
||||||
|
let error = resource
|
||||||
|
.set_capacity(capacity)
|
||||||
|
.expect_err("the capacity is not usable");
|
||||||
|
assert_eq!(error.kind(), ResourceErrorKind::InvalidCapacity);
|
||||||
|
}
|
||||||
|
// The capacity is unchanged after a failed set.
|
||||||
|
assert_eq!(resource.capacity(), 2.5);
|
||||||
|
|
||||||
|
let invalid = Resource::new(ResourceId::new(0), -1.0);
|
||||||
|
assert!(!invalid.has_valid_capacity());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_set_holds_resources_by_identifier() {
|
||||||
|
let mut set = ResourceSet::new();
|
||||||
|
assert!(set.is_empty());
|
||||||
|
set.insert(station()).expect("the identifier is new");
|
||||||
|
set.insert(Resource::new(ResourceId::new(1), 2.0))
|
||||||
|
.expect("the identifier is new");
|
||||||
|
assert_eq!(set.len(), 2);
|
||||||
|
|
||||||
|
let found = set.get(ResourceId::new(0)).expect("the resource exists");
|
||||||
|
assert_eq!(found.capacity(), 1.0);
|
||||||
|
assert!(set.get(ResourceId::new(9)).is_none());
|
||||||
|
assert_eq!(set.resources().len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_duplicate_identifier_is_rejected() {
|
||||||
|
let mut set = ResourceSet::new();
|
||||||
|
set.insert(station()).expect("the identifier is new");
|
||||||
|
let error = set
|
||||||
|
.insert(Resource::new(ResourceId::new(0), 5.0))
|
||||||
|
.expect_err("the identifier is taken");
|
||||||
|
assert_eq!(error.kind(), ResourceErrorKind::DuplicateResource);
|
||||||
|
assert_eq!(set.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_set_can_be_built_from_a_list() {
|
||||||
|
let set = ResourceSet::from_resources(vec![station(), Resource::new(ResourceId::new(1), 1.0)])
|
||||||
|
.expect("the identifiers are distinct");
|
||||||
|
assert_eq!(set.len(), 2);
|
||||||
|
|
||||||
|
let error =
|
||||||
|
ResourceSet::from_resources(vec![station(), Resource::new(ResourceId::new(0), 1.0)])
|
||||||
|
.expect_err("the identifiers collide");
|
||||||
|
assert_eq!(error.kind(), ResourceErrorKind::DuplicateResource);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_resource_can_be_removed() {
|
||||||
|
let mut set = ResourceSet::new();
|
||||||
|
set.insert(station()).expect("new");
|
||||||
|
let removed = set.remove(ResourceId::new(0)).expect("the resource exists");
|
||||||
|
assert_eq!(removed.id, ResourceId::new(0));
|
||||||
|
assert!(set.is_empty());
|
||||||
|
assert_eq!(
|
||||||
|
set.remove(ResourceId::new(0)).expect_err("gone").kind(),
|
||||||
|
ResourceErrorKind::NoSuchResource
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_resource_can_be_modified_through_the_set() {
|
||||||
|
let mut set = ResourceSet::new();
|
||||||
|
set.insert(station()).expect("new");
|
||||||
|
let resource = set
|
||||||
|
.get_mut(ResourceId::new(0))
|
||||||
|
.expect("the resource exists");
|
||||||
|
resource
|
||||||
|
.add_window(Window::new(300.0, 400.0))
|
||||||
|
.expect("usable");
|
||||||
|
assert_eq!(
|
||||||
|
set.get(ResourceId::new(0)).expect("exists").window_count(),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
assert!(set.get_mut(ResourceId::new(9)).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_set_reports_availability() {
|
||||||
|
let mut set = ResourceSet::new();
|
||||||
|
set.insert(station()).expect("new");
|
||||||
|
set.insert(Resource::new(ResourceId::new(1), 1.0))
|
||||||
|
.expect("new");
|
||||||
|
|
||||||
|
// Only the first resource has any availability.
|
||||||
|
assert_eq!(set.available_at(150.0), vec![ResourceId::new(0)]);
|
||||||
|
assert!(set.available_at(50.0).is_empty());
|
||||||
|
assert_eq!(set.available_over(120.0, 50.0), vec![ResourceId::new(0)]);
|
||||||
|
assert!(set.available_over(120.0, 200.0).is_empty());
|
||||||
|
assert_eq!(set.total_availability(), 100.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_identifier_displays_its_index() {
|
||||||
|
assert_eq!(ResourceId::new(4).to_string(), "resource 4");
|
||||||
|
assert_eq!(ResourceId::new(4).index(), 4);
|
||||||
|
assert_eq!(ResourceId::new(4), ResourceId(4));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn identifiers_are_ordered() {
|
||||||
|
// Ordering lets a caller sort a list of resources deterministically.
|
||||||
|
let mut ids = [ResourceId::new(3), ResourceId::new(1), ResourceId::new(2)];
|
||||||
|
ids.sort();
|
||||||
|
assert_eq!(
|
||||||
|
ids,
|
||||||
|
[ResourceId::new(1), ResourceId::new(2), ResourceId::new(3)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,260 @@
|
||||||
|
//! Tests for availability windows and the interval algebra.
|
||||||
|
|
||||||
|
use apsis_resource::{Interval, ResourceErrorKind, Window, merge_windows, subtract_window};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_interval_reports_its_duration() {
|
||||||
|
assert_eq!(Interval::new(10.0, 30.0).duration(), 20.0);
|
||||||
|
// An empty or reversed interval has no duration.
|
||||||
|
assert_eq!(Interval::new(30.0, 30.0).duration(), 0.0);
|
||||||
|
assert_eq!(Interval::new(30.0, 10.0).duration(), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn emptiness_and_validity_are_reported() {
|
||||||
|
assert!(Interval::new(5.0, 5.0).is_empty());
|
||||||
|
assert!(Interval::new(5.0, 4.0).is_empty());
|
||||||
|
assert!(!Interval::new(4.0, 5.0).is_empty());
|
||||||
|
assert!(Interval::new(4.0, 5.0).is_valid());
|
||||||
|
// A reversed interval is not valid, although it is empty.
|
||||||
|
assert!(!Interval::new(5.0, 4.0).is_valid());
|
||||||
|
assert!(!Interval::new(f64::NAN, 5.0).is_valid());
|
||||||
|
assert!(!Interval::new(4.0, f64::INFINITY).is_valid());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn containment_is_half_open() {
|
||||||
|
let interval = Interval::new(10.0, 20.0);
|
||||||
|
// The start is inside and the end is not.
|
||||||
|
assert!(!interval.contains(9.999));
|
||||||
|
assert!(interval.contains(10.0));
|
||||||
|
assert!(interval.contains(15.0));
|
||||||
|
assert!(interval.contains(19.999));
|
||||||
|
assert!(!interval.contains(20.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn containing_an_interval_includes_the_boundaries() {
|
||||||
|
let outer = Interval::new(0.0, 100.0);
|
||||||
|
assert!(outer.contains_interval(&Interval::new(10.0, 20.0)));
|
||||||
|
// The boundaries are included, so an interval that shares an end is
|
||||||
|
// contained.
|
||||||
|
assert!(outer.contains_interval(&Interval::new(0.0, 100.0)));
|
||||||
|
assert!(outer.contains_interval(&Interval::new(0.0, 50.0)));
|
||||||
|
assert!(outer.contains_interval(&Interval::new(50.0, 100.0)));
|
||||||
|
// An interval that reaches past the end is not.
|
||||||
|
assert!(!outer.contains_interval(&Interval::new(50.0, 100.1)));
|
||||||
|
assert!(!outer.contains_interval(&Interval::new(-0.1, 50.0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_empty_interval_is_contained_when_its_instant_is() {
|
||||||
|
let outer = Interval::new(0.0, 100.0);
|
||||||
|
assert!(outer.contains_interval(&Interval::new(50.0, 50.0)));
|
||||||
|
assert!(!outer.contains_interval(&Interval::new(200.0, 200.0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn intervals_that_meet_do_not_overlap() {
|
||||||
|
// The half-open convention means the boundary instant belongs to the
|
||||||
|
// second interval only.
|
||||||
|
let first = Interval::new(0.0, 10.0);
|
||||||
|
let second = Interval::new(10.0, 20.0);
|
||||||
|
assert!(!first.overlaps(&second));
|
||||||
|
assert!(!second.overlaps(&first));
|
||||||
|
assert!(first.is_adjacent_to(&second));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn overlapping_intervals_report_their_intersection() {
|
||||||
|
let first = Interval::new(0.0, 10.0);
|
||||||
|
let second = Interval::new(5.0, 15.0);
|
||||||
|
assert!(first.overlaps(&second));
|
||||||
|
assert_eq!(first.intersection(&second), Some(Interval::new(5.0, 10.0)));
|
||||||
|
assert_eq!(second.intersection(&first), first.intersection(&second));
|
||||||
|
// Disjoint intervals have no intersection.
|
||||||
|
assert_eq!(
|
||||||
|
Interval::new(0.0, 1.0).intersection(&Interval::new(2.0, 3.0)),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_hull_covers_both_intervals() {
|
||||||
|
let first = Interval::new(0.0, 10.0);
|
||||||
|
let second = Interval::new(5.0, 15.0);
|
||||||
|
assert_eq!(first.hull(&second), Interval::new(0.0, 15.0));
|
||||||
|
assert_eq!(first.hull(&second), second.hull(&first));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merging_combines_overlapping_windows() {
|
||||||
|
let windows = [
|
||||||
|
Window::new(0.0, 10.0),
|
||||||
|
Window::new(5.0, 15.0),
|
||||||
|
Window::new(30.0, 40.0),
|
||||||
|
];
|
||||||
|
let merged = merge_windows(&windows).expect("the windows are usable");
|
||||||
|
assert_eq!(merged.len(), 2);
|
||||||
|
assert_eq!(merged[0], Window::new(0.0, 15.0));
|
||||||
|
assert_eq!(merged[1], Window::new(30.0, 40.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merging_combines_windows_that_touch() {
|
||||||
|
// Two windows that meet share a boundary instant, which belongs to the
|
||||||
|
// second, so the union is one continuous window.
|
||||||
|
let merged = merge_windows(&[Window::new(0.0, 10.0), Window::new(10.0, 20.0)])
|
||||||
|
.expect("the windows are usable");
|
||||||
|
assert_eq!(merged, vec![Window::new(0.0, 20.0)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merging_drops_empty_windows() {
|
||||||
|
// A zero length window is valid but contributes no availability, so it
|
||||||
|
// is dropped. A reversed window is a caller error and is rejected.
|
||||||
|
let merged = merge_windows(&[
|
||||||
|
Window::new(5.0, 5.0),
|
||||||
|
Window::new(0.0, 10.0),
|
||||||
|
Window::new(50.0, 50.0),
|
||||||
|
])
|
||||||
|
.expect("the windows are usable");
|
||||||
|
assert_eq!(merged, vec![Window::new(0.0, 10.0)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merging_sorts_windows_into_time_order() {
|
||||||
|
let merged = merge_windows(&[
|
||||||
|
Window::new(30.0, 40.0),
|
||||||
|
Window::new(0.0, 10.0),
|
||||||
|
Window::new(15.0, 20.0),
|
||||||
|
])
|
||||||
|
.expect("the windows are usable");
|
||||||
|
assert_eq!(merged[0].start(), 0.0);
|
||||||
|
assert_eq!(merged[1].start(), 15.0);
|
||||||
|
assert_eq!(merged[2].start(), 30.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merging_an_empty_list_gives_an_empty_list() {
|
||||||
|
assert!(merge_windows(&[]).expect("nothing to check").is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merging_is_idempotent() {
|
||||||
|
// The canonical form must be a fixed point, so that comparing two sets
|
||||||
|
// is a direct comparison.
|
||||||
|
let windows = [
|
||||||
|
Window::new(0.0, 10.0),
|
||||||
|
Window::new(5.0, 15.0),
|
||||||
|
Window::new(30.0, 40.0),
|
||||||
|
];
|
||||||
|
let once = merge_windows(&windows).expect("usable");
|
||||||
|
let twice = merge_windows(&once).expect("usable");
|
||||||
|
assert_eq!(once, twice);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_reversed_window_is_rejected() {
|
||||||
|
let error = merge_windows(&[Window::new(10.0, 0.0)]).expect_err("reversed");
|
||||||
|
assert_eq!(error.kind(), ResourceErrorKind::ReversedInterval);
|
||||||
|
|
||||||
|
let error = Window::new(10.0, 0.0).validate().expect_err("reversed");
|
||||||
|
assert_eq!(error.kind(), ResourceErrorKind::ReversedInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_non_finite_window_is_rejected() {
|
||||||
|
assert_eq!(
|
||||||
|
Window::new(f64::NAN, 1.0)
|
||||||
|
.validate()
|
||||||
|
.expect_err("not finite")
|
||||||
|
.kind(),
|
||||||
|
ResourceErrorKind::NotFinite
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
merge_windows(&[Window::new(0.0, f64::INFINITY)])
|
||||||
|
.expect_err("not finite")
|
||||||
|
.kind(),
|
||||||
|
ResourceErrorKind::NotFinite
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn subtracting_removes_the_middle_of_a_window() {
|
||||||
|
let windows = [Window::new(0.0, 100.0)];
|
||||||
|
let result = subtract_window(&windows, Window::new(30.0, 40.0)).expect("usable");
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
vec![Window::new(0.0, 30.0), Window::new(40.0, 100.0)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn subtracting_the_whole_of_a_window_leaves_nothing() {
|
||||||
|
let result =
|
||||||
|
subtract_window(&[Window::new(0.0, 10.0)], Window::new(0.0, 10.0)).expect("usable");
|
||||||
|
assert!(result.is_empty());
|
||||||
|
|
||||||
|
// A window that covers more than the stored one also removes it.
|
||||||
|
let result =
|
||||||
|
subtract_window(&[Window::new(0.0, 10.0)], Window::new(-5.0, 15.0)).expect("usable");
|
||||||
|
assert!(result.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn subtracting_the_start_trims_the_window() {
|
||||||
|
let result = subtract_window(&[Window::new(0.0, 10.0)], Window::new(0.0, 4.0)).expect("usable");
|
||||||
|
assert_eq!(result, vec![Window::new(4.0, 10.0)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn subtracting_the_end_trims_the_window() {
|
||||||
|
let result =
|
||||||
|
subtract_window(&[Window::new(0.0, 10.0)], Window::new(6.0, 10.0)).expect("usable");
|
||||||
|
assert_eq!(result, vec![Window::new(0.0, 6.0)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn subtracting_a_disjoint_window_changes_nothing() {
|
||||||
|
let windows = [Window::new(0.0, 10.0), Window::new(20.0, 30.0)];
|
||||||
|
let result = subtract_window(&windows, Window::new(12.0, 18.0)).expect("usable");
|
||||||
|
assert_eq!(result, windows.to_vec());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn subtracting_an_empty_window_changes_nothing() {
|
||||||
|
let windows = [Window::new(0.0, 10.0)];
|
||||||
|
let result = subtract_window(&windows, Window::new(5.0, 5.0)).expect("usable");
|
||||||
|
assert_eq!(result, windows.to_vec());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn subtracting_from_several_windows_affects_each() {
|
||||||
|
let windows = [Window::new(0.0, 10.0), Window::new(20.0, 30.0)];
|
||||||
|
let result = subtract_window(&windows, Window::new(5.0, 25.0)).expect("usable");
|
||||||
|
assert_eq!(result, vec![Window::new(0.0, 5.0), Window::new(25.0, 30.0)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn subtracting_merges_the_windows_first() {
|
||||||
|
// The two overlapping windows form one, which the subtraction then
|
||||||
|
// splits. Working on the unmerged list would give a different answer.
|
||||||
|
let windows = [Window::new(0.0, 10.0), Window::new(5.0, 15.0)];
|
||||||
|
let result = subtract_window(&windows, Window::new(8.0, 12.0)).expect("usable");
|
||||||
|
assert_eq!(result, vec![Window::new(0.0, 8.0), Window::new(12.0, 15.0)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_window_displays_its_interval() {
|
||||||
|
assert_eq!(Window::new(1.5, 2.5).to_string(), "[1.5, 2.5)");
|
||||||
|
assert_eq!(Interval::new(0.0, 1.0).to_string(), "[0, 1)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_window_can_be_built_from_an_interval() {
|
||||||
|
let interval = Interval::new(3.0, 7.0);
|
||||||
|
let window = Window::from_interval(interval);
|
||||||
|
assert_eq!(window.interval, interval);
|
||||||
|
assert_eq!(window.duration(), 4.0);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue