rosu_pp/util/
interval_grouping.rs

1use crate::util::float_ext::FloatExt;
2
3use super::sync::{RefCount, Weak};
4
5pub trait HasInterval {
6    fn interval(&self) -> f64;
7}
8
9pub const fn group_by_interval<T: HasInterval>(
10    objects: &[RefCount<T>],
11) -> GroupedByIntervalIter<'_, T> {
12    GroupedByIntervalIter::new(objects)
13}
14
15pub struct GroupedByIntervalIter<'a, T> {
16    objects: &'a [RefCount<T>],
17    i: usize,
18}
19
20impl<'a, T> GroupedByIntervalIter<'a, T> {
21    const fn new(objects: &'a [RefCount<T>]) -> Self {
22        Self { objects, i: 0 }
23    }
24}
25
26impl<T: HasInterval> Iterator for GroupedByIntervalIter<'_, T> {
27    type Item = Vec<Weak<T>>;
28
29    fn next(&mut self) -> Option<Self::Item> {
30        if self.i < self.objects.len() {
31            Some(self.create_next_group())
32        } else {
33            None
34        }
35    }
36
37    fn size_hint(&self) -> (usize, Option<usize>) {
38        let min = usize::from(!self.objects.is_empty());
39        let max = self.objects.len() - self.i;
40
41        (min, Some(max))
42    }
43}
44
45impl<T: HasInterval> GroupedByIntervalIter<'_, T> {
46    fn create_next_group(&mut self) -> Vec<Weak<T>> {
47        const MARGIN_OF_ERROR: f64 = 5.0;
48
49        let &mut Self { objects, ref mut i } = self;
50
51        // * This never compares the first two elements in the group.
52        // * This sounds wrong but is apparently "as intended" (https://github.com/ppy/osu/pull/31636#discussion_r1942673329)
53        let mut grouped_objects = vec![objects[*i].downgrade()];
54
55        *i += 1;
56
57        while *i < objects.len() - 1 {
58            if !(objects[*i]
59                .get()
60                .interval()
61                .almost_eq(objects[*i + 1].get().interval(), MARGIN_OF_ERROR))
62            {
63                // * When an interval change occurs, include the object with the differing interval in the case it increased
64                // * See https://github.com/ppy/osu/pull/31636#discussion_r1942368372 for rationale.
65                if objects[*i + 1].get().interval() > objects[*i].get().interval() + MARGIN_OF_ERROR
66                {
67                    grouped_objects.push(objects[*i].downgrade());
68                    *i += 1;
69                }
70
71                return grouped_objects;
72            }
73
74            // * No interval change occurred
75            grouped_objects.push(objects[*i].downgrade());
76
77            *i += 1;
78        }
79
80        // * Check if the last two objects in the object form a "flat" rhythm pattern within the specified margin of error.
81        // * If true, add the current object to the group and increment the index to process the next object.
82        if objects.len() > 2
83            && *i < objects.len()
84            && objects[objects.len() - 1]
85                .get()
86                .interval()
87                .almost_eq(objects[objects.len() - 2].get().interval(), MARGIN_OF_ERROR)
88        {
89            grouped_objects.push(objects[*i].downgrade());
90            *i += 1;
91        }
92
93        grouped_objects
94    }
95}