Skip to content

Commit

Permalink
refactor: re-implemented SortedVec (#22)
Browse files Browse the repository at this point in the history
* refactor: re-implemented SortedVec

* feat: add methods to remove items from SortedVec
  • Loading branch information
MaxOhn authored Nov 10, 2023
1 parent 5456be3 commit 3492a94
Show file tree
Hide file tree
Showing 8 changed files with 310 additions and 166 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ Cargo.lock
output*
/.idea
/tests/custom.rs
/.idea

/pp-gen/.env
/pp-gen/target
Expand Down
9 changes: 8 additions & 1 deletion src/beatmap/control_points.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ impl DifficultyPoint {
}
}

pub(crate) fn is_redundant(&self, existing: &DifficultyPoint) -> bool {
pub(crate) fn is_redundant(&self, existing: &Self) -> bool {
(self.slider_vel - existing.slider_vel).abs() <= f64::EPSILON
&& self.generate_ticks == existing.generate_ticks
}
Expand Down Expand Up @@ -118,6 +118,13 @@ impl EffectPoint {
}
}

impl PartialOrd for EffectPoint {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.time.partial_cmp(&other.time)
}
}

impl Default for EffectPoint {
#[inline]
fn default() -> Self {
Expand Down
4 changes: 3 additions & 1 deletion src/beatmap/mod.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use std::{borrow::Cow, cmp::Ordering, collections::HashMap};

use crate::{parse::HitObject, util::SortedVec};
use crate::parse::HitObject;

pub use self::{
attributes::{BeatmapAttributes, BeatmapAttributesBuilder, BeatmapHitWindows},
breaks::Break,
control_points::{DifficultyPoint, EffectPoint, TimingPoint},
ext::*,
mode::GameMode,
sorted_vec::{Sortable, SortedVec},
};

mod attributes;
Expand All @@ -16,6 +17,7 @@ mod control_points;
mod converts;
mod ext;
mod mode;
mod sorted_vec;

/// The main beatmap struct containing all data relevant
/// for difficulty and performance calculation
Expand Down
293 changes: 293 additions & 0 deletions src/beatmap/sorted_vec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
use std::{
cmp::Ordering,
fmt::{Debug, Formatter, Result as FmtResult},
ops::{Deref, Index},
slice::SliceIndex,
};

use crate::beatmap::{DifficultyPoint, EffectPoint, TimingPoint};

/// A [`Vec`] whose elements are guaranteed to be unique and in order.
#[derive(Clone)]
pub struct SortedVec<T> {
inner: Vec<T>,
}

impl<T> SortedVec<T> {
/// Constructs a new, empty `SortedVec<T>`.
#[inline]
pub fn new() -> Self {
Self { inner: Vec::new() }
}

/// Constructs a new, empty `SortedVec<T>` with at least the specified capacity.
#[inline]
pub fn with_capacity(capacity: usize) -> Self {
Self {
inner: Vec::with_capacity(capacity),
}
}

/// Extracts the inner [`Vec`].
#[inline]
pub fn into_inner(self) -> Vec<T> {
self.inner
}

/// Extracts a slice containing the entire sorted vector.
pub fn as_slice(&self) -> &[T] {
self.inner.as_slice()
}

/// Returns a mutable reference to the underlying `Vec`.
///
/// # Safety
///
/// The caller must ensure that the items stay in order.
pub unsafe fn as_inner_mut(&mut self) -> &mut Vec<T> {
&mut self.inner
}

/// Removes the last element and returns it, or `None` if the vec is empty.
pub fn pop(&mut self) -> Option<T> {
self.inner.pop()
}

/// Retains only the elements specified by the predicate.
///
/// In other words, remove all items `i` for which `f(&i)` returns `false`.
pub fn retain<F>(&mut self, f: F)
where
F: FnMut(&T) -> bool,
{
self.inner.retain(f);
}
}

impl<T: Sortable> SortedVec<T> {
/// Same as [`slice::binary_search_by`] with the function
/// [`<T as Sortable>::cmp`](Sortable::cmp).
#[inline]
pub fn find(&self, value: &T) -> Result<usize, usize> {
self.inner
.binary_search_by(|probe| <T as Sortable>::cmp(probe, value))
}

/// Push a new value into the sorted list based on [`<T as Sortable>::push`](Sortable::push).
pub fn push(&mut self, value: T) {
<T as Sortable>::push(value, self)
}
}

impl<T> Deref for SortedVec<T> {
type Target = [T];

#[inline]
fn deref(&self) -> &Self::Target {
<Vec<T> as Deref>::deref(&self.inner)
}
}

impl<T, I> Index<I> for SortedVec<T>
where
I: SliceIndex<[T]>,
{
type Output = <I as SliceIndex<[T]>>::Output;

#[inline]
fn index(&self, index: I) -> &Self::Output {
<Vec<T> as Index<I>>::index(&self.inner, index)
}
}

impl<T: Debug> Debug for SortedVec<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
<Vec<T> as Debug>::fmt(&self.inner, f)
}
}

impl<T> Default for SortedVec<T> {
#[inline]
fn default() -> Self {
Self::new()
}
}

impl<T: Sortable> From<Vec<T>> for SortedVec<T> {
fn from(mut v: Vec<T>) -> Self {
v.sort_by(<T as Sortable>::cmp);
v.dedup_by(|a, b| {
<T as Sortable>::cmp(a, b) == Ordering::Equal || <T as Sortable>::is_redundant(b, a)
});

Self { inner: v }
}
}

impl<T: Sortable> FromIterator<T> for SortedVec<T> {
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
Self::from(Vec::from_iter(iter))
}
}

impl<T: Sortable> Extend<T> for SortedVec<T> {
fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
for value in iter {
self.push(value);
}
}
}

/// Trait for types that can be sorted in a [`SortedVec`].
pub trait Sortable: Sized {
/// An [`Ordering`] between `self` and `other`.
fn cmp(&self, other: &Self) -> Ordering;

/// Indicates whether `self` and `_existing` are identical.
#[inline]
fn is_redundant(&self, _existing: &Self) -> bool {
false
}

/// Pushes a value into the [`SortedVec`].
#[inline]
fn push(self, sorted_vec: &mut SortedVec<Self>) {
match sorted_vec.find(&self) {
Ok(i) => sorted_vec.inner[i] = self,
Err(i) if i == sorted_vec.len() => sorted_vec.inner.push(self),
Err(i) => sorted_vec.inner.insert(i, self),
}
}
}

impl Sortable for TimingPoint {
#[inline]
fn cmp(&self, other: &Self) -> Ordering {
self.partial_cmp(other).unwrap_or(Ordering::Equal)
}
}

impl Sortable for DifficultyPoint {
#[inline]
fn cmp(&self, other: &Self) -> Ordering {
self.partial_cmp(other).unwrap_or(Ordering::Equal)
}

#[inline]
fn is_redundant(&self, existing: &Self) -> bool {
self.is_redundant(existing)
}

fn push(self, sorted_vec: &mut SortedVec<Self>) {
enum Action {
Insert(usize),
Replace(usize),
Push,
Skip,
}

let action = match sorted_vec.find(&self).map_err(|idx| idx.checked_sub(1)) {
Ok(i) | Err(Some(i)) if self.is_redundant(&sorted_vec[i]) => Action::Skip,
Ok(i) => Action::Replace(i),
Err(Some(i)) if i == sorted_vec.len() - 1 => Action::Push,
Err(Some(i)) => Action::Insert(i),
Err(None) if self.is_redundant(&Self::default()) => Action::Skip,
Err(None) => Action::Insert(0),
};

match action {
Action::Insert(i) => sorted_vec.inner.insert(i, self),
Action::Replace(i) => sorted_vec.inner[i] = self,
Action::Push => sorted_vec.inner.push(self),
Action::Skip => {}
}
}
}

impl Sortable for EffectPoint {
#[inline]
fn cmp(&self, other: &Self) -> Ordering {
self.partial_cmp(other).unwrap_or(Ordering::Equal)
}
}

impl<T: Ord> Sortable for T {
#[inline]
fn cmp(&self, other: &Self) -> Ordering {
<Self as Ord>::cmp(self, other)
}
}

#[cfg(test)]
mod tests {
use crate::beatmap::DifficultyPoint;

use super::SortedVec;

#[test]
fn sorts_on_push() {
let mut v = SortedVec::with_capacity(4);

v.push(42);
v.push(13);
v.push(20);
v.push(0);

assert_eq!(v.as_slice(), &[0_i32, 13, 20, 42]);
}

#[test]
fn no_push_if_redundant() {
let mut v = SortedVec::default();

v.push(DifficultyPoint::default());
assert_eq!(v.len(), 0);

v.push(DifficultyPoint::new(1.0, 2.0, 3.0));
assert_eq!(v.len(), 1);

v.push(DifficultyPoint::new(2.0, 2.0, 3.0));
v.push(DifficultyPoint::default());
assert_eq!(v.len(), 1);
}

#[test]
fn from_iter() {
let base = vec![
DifficultyPoint {
time: 5.0,
slider_vel: 10.0,
bpm_mult: 1.0,
generate_ticks: true,
},
DifficultyPoint {
time: 3.0,
slider_vel: 20.0,
bpm_mult: 2.0,
generate_ticks: false,
},
DifficultyPoint {
time: 6.0,
slider_vel: 10.0,
bpm_mult: 3.0,
generate_ticks: true,
},
DifficultyPoint {
time: 10.0,
slider_vel: 15.0,
bpm_mult: 4.0,
generate_ticks: true,
},
];

let sorted = SortedVec::from_iter(base);

let v: Vec<_> = sorted
.into_inner()
.into_iter()
.map(|tp| tp.bpm_mult)
.collect();

assert_eq!(v, vec![2.0, 1.0, 4.0]);
}
}
3 changes: 1 addition & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ pub mod parse;

/// Beatmap and contained types
pub mod beatmap;
pub use beatmap::{Beatmap, BeatmapExt, GameMode};
pub use beatmap::{Beatmap, BeatmapExt, GameMode, SortedVec}; // TODO: remove SortedVec on next breaking release

#[cfg(feature = "gradual")]
mod gradual;
Expand Down Expand Up @@ -231,7 +231,6 @@ pub use taiko::{TaikoPP, TaikoStars};

pub use mods::Mods;
pub use parse::{ParseError, ParseResult};
pub use util::SortedVec;

/// The result of calculating the strains on a map.
/// Suitable to plot the difficulty of a map over time.
Expand Down
8 changes: 4 additions & 4 deletions src/parse/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ use std::path::Path;
use async_std::{fs::File, io::Read as AsyncRead, path::Path};

use crate::{
beatmap::{Beatmap, Break, DifficultyPoint, EffectPoint, GameMode, TimingPoint},
util::{SortedVec, TandemSorter},
beatmap::{Beatmap, Break, DifficultyPoint, EffectPoint, GameMode, SortedVec, TimingPoint},
util::TandemSorter,
};

trait InRange: Sized + Copy + Neg<Output = Self> + PartialOrd + FromStr {
Expand Down Expand Up @@ -355,7 +355,7 @@ macro_rules! parse_timingpoints_body {

if time != pending_diff_points_time {
if let Some(point) = pending_diff_point.take() {
$self.difficulty_points.push_if_not_redundant(point);
$self.difficulty_points.push(point);
}
}

Expand All @@ -376,7 +376,7 @@ macro_rules! parse_timingpoints_body {
}

if let Some(point) = pending_diff_point {
$self.difficulty_points.push_if_not_redundant(point);
$self.difficulty_points.push(point);
}

Ok(empty)
Expand Down
Loading

0 comments on commit 3492a94

Please sign in to comment.