Skip to content

Commit

Permalink
Merge pull request #20 from earthstar-project/3d-range-and-areas
Browse files Browse the repository at this point in the history
Range3d, Area, and AreaOfInterest
  • Loading branch information
sgwilym authored Jun 24, 2024
2 parents d547db7 + 67b7f91 commit 6fedd72
Show file tree
Hide file tree
Showing 8 changed files with 575 additions and 19 deletions.
6 changes: 3 additions & 3 deletions data-model/src/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,15 @@ mod tests {

use super::*;

#[derive(Default, PartialEq, Eq)]
#[derive(Default, PartialEq, Eq, Clone)]
struct FakeNamespaceId(usize);
impl NamespaceId for FakeNamespaceId {}

#[derive(Default, PartialEq, Eq, PartialOrd, Ord)]
#[derive(Default, PartialEq, Eq, PartialOrd, Ord, Clone)]
struct FakeSubspaceId(usize);
impl SubspaceId for FakeSubspaceId {}

#[derive(Default, PartialEq, Eq, PartialOrd, Ord)]
#[derive(Default, PartialEq, Eq, PartialOrd, Ord, Clone)]
struct FakePayloadDigest(usize);
impl PayloadDigest for FakePayloadDigest {}

Expand Down
268 changes: 268 additions & 0 deletions data-model/src/grouping/area.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
use crate::{
entry::{Entry, Timestamp},
parameters::{NamespaceId, PayloadDigest, SubspaceId},
path::Path,
};

use super::{range::Range, range_3d::Range3d};

/// The possible values of an [`Area`]'s `subspace`.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum AreaSubspace<S: SubspaceId> {
/// A value that signals that an [`Area`] includes Entries with arbitrary subspace_ids.
Any,
/// A concrete [`SubspaceId`]
Id(S),
}

impl<S: SubspaceId> AreaSubspace<S> {
/// Whether this [`AreaSubspace`] includes a given [`SubspaceId`].
pub fn includes(&self, sub: &S) -> bool {
match self {
AreaSubspace::Any => true,
AreaSubspace::Id(id) => sub == id,
}
}

/// Return the intersection between two [`AreaSubspace`].
fn intersection(&self, other: &Self) -> Option<Self> {
match (self, other) {
(Self::Any, Self::Any) => Some(Self::Any),
(Self::Id(a), Self::Any) => Some(Self::Id(a.clone())),
(Self::Any, Self::Id(b)) => Some(Self::Id(b.clone())),
(Self::Id(a), Self::Id(b)) if a == b => Some(Self::Id(a.clone())),
(Self::Id(_a), Self::Id(_b)) => None,
}
}
}

/// A grouping of entries.
/// [Definition](https://willowprotocol.org/specs/grouping-entries/index.html#areas).
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Area<S: SubspaceId, P: Path> {
/// To be included in this [`Area`], an [`Entry`]’s `subspace_id` must be equal to the subspace_id, unless it is any.
pub subspace: AreaSubspace<S>,
/// To be included in this [`Area`], an [`Entry`]’s `path` must be prefixed by the path.
pub path: P,
/// To be included in this [`Area`], an [`Entry`]’s `timestamp` must be included in the times.
pub times: Range<Timestamp>,
}

impl<S: SubspaceId, P: Path> Area<S, P> {
/// Return an [`Area`] which includes all entries.
/// [Definition](https://willowprotocol.org/specs/grouping-entries/index.html#full_area).
pub fn full() -> Self {
Self {
subspace: AreaSubspace::Any,
path: P::empty(),
times: Range::new_open(0),
}
}

/// Return an [`Area`] which includes all entries within a [subspace](https://willowprotocol.org/specs/data-model/index.html#subspace).
/// [Definition](https://willowprotocol.org/specs/grouping-entries/index.html#subspace_area).
pub fn subspace(sub: S) -> Self {
Self {
subspace: AreaSubspace::Id(sub),
path: P::empty(),
times: Range::new_open(0),
}
}

/// Return whether an [`Area`] [includes](https://willowprotocol.org/specs/grouping-entries/index.html#area_include) an [`Entry`].
pub fn includes_entry<N: NamespaceId, PD: PayloadDigest>(
&self,
entry: &Entry<N, S, P, PD>,
) -> bool {
self.subspace.includes(&entry.subspace_id)
&& self.path.is_prefix_of(&entry.path)
&& self.times.includes(&entry.timestamp)
}

/// Return the intersection of this [`Area`] with another.
/// [Definition](https://willowprotocol.org/specs/grouping-entries/index.html#area_intersection).
pub fn intersection(&self, other: &Area<S, P>) -> Option<Self> {
let subspace_id = self.subspace.intersection(&other.subspace)?;
let path = if self.path.is_prefix_of(&other.path) {
Some(other.path.clone())
} else if self.path.is_prefixed_by(&other.path) {
Some(self.path.clone())
} else {
None
}?;
let times = self.times.intersection(&other.times)?;
Some(Self {
subspace: subspace_id,
times,
path,
})
}
}

#[cfg(test)]
mod tests {

use crate::path::{PathComponent, PathComponentBox, PathRc};

use super::*;

#[derive(Default, PartialEq, Eq, Clone)]
struct FakeNamespaceId(usize);
impl NamespaceId for FakeNamespaceId {}

#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Clone)]
struct FakeSubspaceId(usize);
impl SubspaceId for FakeSubspaceId {}

#[derive(Default, PartialEq, Eq, PartialOrd, Ord, Clone)]
struct FakePayloadDigest(usize);
impl PayloadDigest for FakePayloadDigest {}

const MCL: usize = 8;
const MCC: usize = 4;
const MPL: usize = 16;

#[test]
fn subspace_area_includes() {
assert!(AreaSubspace::<FakeSubspaceId>::Any.includes(&FakeSubspaceId(5)));
assert!(AreaSubspace::<FakeSubspaceId>::Id(FakeSubspaceId(5)).includes(&FakeSubspaceId(5)));
assert!(!AreaSubspace::<FakeSubspaceId>::Id(FakeSubspaceId(8)).includes(&FakeSubspaceId(5)));
}

#[test]
fn subspace_area_intersects() {
// Non-empty intersections
let any_any_intersection =
AreaSubspace::<FakeSubspaceId>::Any.intersection(&AreaSubspace::<FakeSubspaceId>::Any);

assert!(any_any_intersection.is_some());

assert!(any_any_intersection.unwrap() == AreaSubspace::<FakeSubspaceId>::Any);

let any_id_intersection = AreaSubspace::<FakeSubspaceId>::Any
.intersection(&AreaSubspace::<FakeSubspaceId>::Id(FakeSubspaceId(6)));

assert!(any_id_intersection.is_some());

assert!(
any_id_intersection.unwrap() == AreaSubspace::<FakeSubspaceId>::Id(FakeSubspaceId(6))
);

let id_id_intersection = AreaSubspace::<FakeSubspaceId>::Id(FakeSubspaceId(6))
.intersection(&AreaSubspace::<FakeSubspaceId>::Id(FakeSubspaceId(6)));

assert!(id_id_intersection.is_some());

assert!(
id_id_intersection.unwrap() == AreaSubspace::<FakeSubspaceId>::Id(FakeSubspaceId(6))
);

// Empty intersections

let empty_id_id_intersection = AreaSubspace::<FakeSubspaceId>::Id(FakeSubspaceId(7))
.intersection(&AreaSubspace::<FakeSubspaceId>::Id(FakeSubspaceId(6)));

assert!(empty_id_id_intersection.is_none())
}

#[test]
fn area_full() {
let full_area = Area::<FakeSubspaceId, PathRc<MCL, MCC, MPL>>::full();

assert_eq!(
full_area,
Area {
subspace: AreaSubspace::Any,
path: PathRc::empty(),
times: Range::new_open(0)
}
)
}

#[test]
fn area_subspace() {
let subspace_area =
Area::<FakeSubspaceId, PathRc<MCL, MCC, MPL>>::subspace(FakeSubspaceId(7));

assert_eq!(
subspace_area,
Area {
subspace: AreaSubspace::Id(FakeSubspaceId(7)),
path: PathRc::empty(),
times: Range::new_open(0)
}
)
}

#[test]
fn area_intersects() {
let empty_intersection_subspace = Area::<FakeSubspaceId, PathRc<MCL, MCC, MPL>> {
subspace: AreaSubspace::Id(FakeSubspaceId(1)),
path: PathRc::empty(),
times: Range::new_open(0),
}
.intersection(&Area {
subspace: AreaSubspace::Id(FakeSubspaceId(2)),
path: PathRc::empty(),
times: Range::new_open(0),
});

assert!(empty_intersection_subspace.is_none());

let empty_intersection_path = Area::<FakeSubspaceId, PathRc<MCL, MCC, MPL>> {
subspace: AreaSubspace::Id(FakeSubspaceId(1)),
path: PathRc::new(&[PathComponentBox::new(&[b'0']).unwrap()]).unwrap(),
times: Range::new_open(0),
}
.intersection(&Area {
subspace: AreaSubspace::Id(FakeSubspaceId(1)),
path: PathRc::new(&[PathComponentBox::new(&[b'1']).unwrap()]).unwrap(),
times: Range::new_open(0),
});

assert!(empty_intersection_path.is_none());

let empty_intersection_time = Area::<FakeSubspaceId, PathRc<MCL, MCC, MPL>> {
subspace: AreaSubspace::Id(FakeSubspaceId(1)),
path: PathRc::empty(),
times: Range::new_closed(0, 1).unwrap(),
}
.intersection(&Area {
subspace: AreaSubspace::Id(FakeSubspaceId(1)),
path: PathRc::empty(),
times: Range::new_closed(2, 3).unwrap(),
});

assert!(empty_intersection_time.is_none());

let intersection = Area::<FakeSubspaceId, PathRc<MCL, MCC, MPL>> {
subspace: AreaSubspace::Any,
path: PathRc::new(&[PathComponentBox::new(&[b'1']).unwrap()]).unwrap(),
times: Range::new_closed(0, 10).unwrap(),
}
.intersection(&Area {
subspace: AreaSubspace::Id(FakeSubspaceId(1)),
path: PathRc::new(&[
PathComponentBox::new(&[b'1']).unwrap(),
PathComponentBox::new(&[b'2']).unwrap(),
])
.unwrap(),
times: Range::new_closed(5, 15).unwrap(),
});

assert!(intersection.is_some());

assert_eq!(
intersection.unwrap(),
Area {
subspace: AreaSubspace::Id(FakeSubspaceId(1)),
path: PathRc::new(&[
PathComponentBox::new(&[b'1']).unwrap(),
PathComponentBox::new(&[b'2']).unwrap(),
])
.unwrap(),
times: Range::new_closed(5, 10).unwrap(),
}
)
}
}
27 changes: 27 additions & 0 deletions data-model/src/grouping/area_of_interest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use crate::{grouping::area::Area, parameters::SubspaceId, path::Path};

/// A grouping of [`Entry`]s that are among the newest in some [`Store`].
/// [Definition](https://willowprotocol.org/specs/grouping-entries/index.html#aois).
pub struct AreaOfInterest<S: SubspaceId, P: Path> {
/// To be included in this [`AreaOfInterest`], an [`Entry`] must be included in the [`Area`].
pub area: Area<S, P>,
/// To be included in this AreaOfInterest, an Entry’s timestamp must be among the max_count greatest Timestamps, unless max_count is zero.
pub max_count: u64,
/// The total payload_lengths of all included Entries is at most max_size, unless max_size is zero.
pub max_size: u64,
}

impl<S: SubspaceId, P: Path> AreaOfInterest<S, P> {
/// Return the intersection of this [`AreaOfInterest`] with another.
/// [Definition](https://willowprotocol.org/specs/grouping-entries/index.html#aoi_intersection).
pub fn intersection(&self, other: AreaOfInterest<S, P>) -> Option<AreaOfInterest<S, P>> {
match self.area.intersection(&other.area) {
None => None,
Some(area_intersection) => Some(Self {
area: area_intersection,
max_count: core::cmp::min(self.max_count, other.max_count),
max_size: self.max_size.min(other.max_size),
}),
}
}
}
3 changes: 3 additions & 0 deletions data-model/src/grouping/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
pub mod area;
pub mod area_of_interest;
pub mod range;
pub mod range_3d;
16 changes: 8 additions & 8 deletions data-model/src/grouping/range.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ where
}

/// Return whether a given value is [included](https://willowprotocol.org/specs/grouping-entries/index.html#range_include) by the [`Range`] or not.
pub fn includes(&self, value: T) -> bool {
self.start <= value && self.end.gt_val(&value)
pub fn includes(&self, value: &T) -> bool {
&self.start <= value && self.end.gt_val(value)
}

/// Returns `true` if `other` range is fully [included](https://willowprotocol.org/specs/grouping-entries/index.html#range_include) in this [`Range`].
Expand Down Expand Up @@ -167,15 +167,15 @@ mod tests {
fn range_includes() {
let open_range = Range::new_open(20);

assert!(open_range.includes(20));
assert!(open_range.includes(30));
assert!(!open_range.includes(10));
assert!(open_range.includes(&20));
assert!(open_range.includes(&30));
assert!(!open_range.includes(&10));

let closed_range = Range::new_closed(5, 10).unwrap();

assert!(closed_range.includes(5));
assert!(closed_range.includes(8));
assert!(!closed_range.includes(1));
assert!(closed_range.includes(&5));
assert!(closed_range.includes(&8));
assert!(!closed_range.includes(&1));
}

#[test]
Expand Down
Loading

0 comments on commit 6fedd72

Please sign in to comment.