From 3492a9472e1daa039105ff978e15226001995e3b Mon Sep 17 00:00:00 2001 From: Badewanne3 <41148446+MaxOhn@users.noreply.github.com> Date: Fri, 10 Nov 2023 15:08:52 +0100 Subject: [PATCH] refactor: re-implemented SortedVec (#22) * refactor: re-implemented SortedVec * feat: add methods to remove items from SortedVec --- .gitignore | 1 + src/beatmap/control_points.rs | 9 +- src/beatmap/mod.rs | 4 +- src/beatmap/sorted_vec.rs | 293 ++++++++++++++++++++++++++++++++++ src/lib.rs | 3 +- src/parse/mod.rs | 8 +- src/util/mod.rs | 3 - src/util/sorted_vec.rs | 155 ------------------ 8 files changed, 310 insertions(+), 166 deletions(-) create mode 100644 src/beatmap/sorted_vec.rs delete mode 100644 src/util/sorted_vec.rs diff --git a/.gitignore b/.gitignore index 510a7229..f0b40982 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ Cargo.lock output* /.idea /tests/custom.rs +/.idea /pp-gen/.env /pp-gen/target diff --git a/src/beatmap/control_points.rs b/src/beatmap/control_points.rs index edc33739..88f1cb4b 100644 --- a/src/beatmap/control_points.rs +++ b/src/beatmap/control_points.rs @@ -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 } @@ -118,6 +118,13 @@ impl EffectPoint { } } +impl PartialOrd for EffectPoint { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + self.time.partial_cmp(&other.time) + } +} + impl Default for EffectPoint { #[inline] fn default() -> Self { diff --git a/src/beatmap/mod.rs b/src/beatmap/mod.rs index c17220de..d0fbc1ca 100644 --- a/src/beatmap/mod.rs +++ b/src/beatmap/mod.rs @@ -1,6 +1,6 @@ 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}, @@ -8,6 +8,7 @@ pub use self::{ control_points::{DifficultyPoint, EffectPoint, TimingPoint}, ext::*, mode::GameMode, + sorted_vec::{Sortable, SortedVec}, }; mod attributes; @@ -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 diff --git a/src/beatmap/sorted_vec.rs b/src/beatmap/sorted_vec.rs new file mode 100644 index 00000000..75526931 --- /dev/null +++ b/src/beatmap/sorted_vec.rs @@ -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 { + inner: Vec, +} + +impl SortedVec { + /// Constructs a new, empty `SortedVec`. + #[inline] + pub fn new() -> Self { + Self { inner: Vec::new() } + } + + /// Constructs a new, empty `SortedVec` 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 { + 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 { + &mut self.inner + } + + /// Removes the last element and returns it, or `None` if the vec is empty. + pub fn pop(&mut self) -> Option { + 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(&mut self, f: F) + where + F: FnMut(&T) -> bool, + { + self.inner.retain(f); + } +} + +impl SortedVec { + /// Same as [`slice::binary_search_by`] with the function + /// [`::cmp`](Sortable::cmp). + #[inline] + pub fn find(&self, value: &T) -> Result { + self.inner + .binary_search_by(|probe| ::cmp(probe, value)) + } + + /// Push a new value into the sorted list based on [`::push`](Sortable::push). + pub fn push(&mut self, value: T) { + ::push(value, self) + } +} + +impl Deref for SortedVec { + type Target = [T]; + + #[inline] + fn deref(&self) -> &Self::Target { + as Deref>::deref(&self.inner) + } +} + +impl Index for SortedVec +where + I: SliceIndex<[T]>, +{ + type Output = >::Output; + + #[inline] + fn index(&self, index: I) -> &Self::Output { + as Index>::index(&self.inner, index) + } +} + +impl Debug for SortedVec { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + as Debug>::fmt(&self.inner, f) + } +} + +impl Default for SortedVec { + #[inline] + fn default() -> Self { + Self::new() + } +} + +impl From> for SortedVec { + fn from(mut v: Vec) -> Self { + v.sort_by(::cmp); + v.dedup_by(|a, b| { + ::cmp(a, b) == Ordering::Equal || ::is_redundant(b, a) + }); + + Self { inner: v } + } +} + +impl FromIterator for SortedVec { + fn from_iter>(iter: I) -> Self { + Self::from(Vec::from_iter(iter)) + } +} + +impl Extend for SortedVec { + fn extend>(&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) { + 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) { + 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 Sortable for T { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + ::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]); + } +} diff --git a/src/lib.rs b/src/lib.rs index 283b72ba..54ade787 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; @@ -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. diff --git a/src/parse/mod.rs b/src/parse/mod.rs index f89b4d39..efad1460 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -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 + PartialOrd + FromStr { @@ -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); } } @@ -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) diff --git a/src/util/mod.rs b/src/util/mod.rs index c4e3213c..02e4c5cd 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -2,11 +2,8 @@ mod byte_hasher; mod compact_vec; mod float_ext; mod limited_queue; -mod sorted_vec; mod tandem_sort; -pub use self::sorted_vec::SortedVec; - pub(crate) use self::{ byte_hasher::ByteHasher, compact_vec::CompactVec, float_ext::FloatExt, limited_queue::LimitedQueue, tandem_sort::TandemSorter, diff --git a/src/util/sorted_vec.rs b/src/util/sorted_vec.rs deleted file mode 100644 index 87287dab..00000000 --- a/src/util/sorted_vec.rs +++ /dev/null @@ -1,155 +0,0 @@ -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 in order based on the given comparator. -#[derive(Clone)] -pub struct SortedVec { - inner: Vec, - cmp: fn(&T, &T) -> Ordering, -} - -impl SortedVec { - /// Same as [`slice::binary_search_by`] with the internal compare function - #[inline] - pub fn find(&self, value: &T) -> Result { - self.inner - .binary_search_by(|probe| (self.cmp)(probe, value)) - } - - /// Extracts the inner [`Vec`]. - #[inline] - pub fn into_inner(self) -> Vec { - self.inner - } - - /// Push a new value into the sorted list. - /// If there is already an element that matches the new value, - /// the old element will be replaced. - pub(crate) fn push(&mut self, value: T) { - match self.find(&value) { - Ok(i) => self.inner[i] = value, - Err(i) if i == self.inner.len() => self.inner.push(value), - Err(i) => self.inner.insert(i, value), - } - } -} - -impl Deref for SortedVec { - type Target = [T]; - - #[inline] - fn deref(&self) -> &Self::Target { - as Deref>::deref(&self.inner) - } -} - -impl Index for SortedVec -where - I: SliceIndex<[T]>, -{ - type Output = >::Output; - - #[inline] - fn index(&self, index: I) -> &Self::Output { - as Index>::index(&self.inner, index) - } -} - -impl Debug for SortedVec { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - as Debug>::fmt(&self.inner, f) - } -} - -impl Default for SortedVec -where - T: Ord, -{ - #[inline] - fn default() -> Self { - Self { - inner: Vec::default(), - cmp: ::cmp, - } - } -} - -macro_rules! impl_default_control_point { - ( $( $ty:ident ),* ) => { - $( - impl Default for SortedVec<$ty> { - #[inline] - fn default() -> Self { - Self { - inner: Vec::new(), - cmp: |a, b| a.time.partial_cmp(&b.time).unwrap_or(Ordering::Equal), - } - } - } - - impl SortedVec<$ty> { - #[allow(unused)] - pub(crate) fn with_capacity(capacity: usize) -> Self { - Self { - inner: Vec::with_capacity(capacity), - cmp: |a, b| a.time.partial_cmp(&b.time).unwrap_or(Ordering::Equal), - } - } - } - )* - } -} - -impl_default_control_point!(TimingPoint, DifficultyPoint, EffectPoint); - -impl SortedVec { - pub(crate) fn push_if_not_redundant(&mut self, value: DifficultyPoint) { - let is_redundant = match self.find(&value).map_err(|idx| idx.checked_sub(1)) { - Ok(idx) | Err(Some(idx)) => value.is_redundant(&self[idx]), - Err(None) => value.is_redundant(&DifficultyPoint::default()), - }; - - if !is_redundant { - self.push(value); - } - } -} - -#[cfg(test)] -mod tests { - use crate::beatmap::DifficultyPoint; - - use super::SortedVec; - - #[test] - fn sorts_on_push() { - let mut v = SortedVec { - inner: Vec::new(), - cmp: ::cmp, - }; - - v.push(42); - v.push(13); - v.push(20); - v.push(0); - - assert_eq!(&v[..], &[0_i32, 13, 20, 42]); - } - - #[test] - fn no_push_if_redundant() { - let mut v = SortedVec::default(); - - v.push(DifficultyPoint::default()); - assert_eq!(v.len(), 1); - - v.push_if_not_redundant(DifficultyPoint::default()); - assert_eq!(v.len(), 1); - } -}