textgridde_rs/
point.rs

1use std::{
2    cmp::Ordering,
3    fmt::{self, Display, Formatter},
4};
5
6use derive_more::Constructor;
7use getset::{Getters, MutGetters, Setters};
8
9/// A "point," used in Praat as a specific time marker with an associated label.
10#[derive(Constructor, Debug, Default, Clone, Getters, Setters)]
11pub struct Point {
12    #[getset(get = "pub", set = "pub")]
13    number: f64,
14    #[getset(get = "pub", set = "pub")]
15    mark: String,
16}
17
18impl Display for Point {
19    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
20        writeln!(f, "Point:\t{}\t{}", self.number, self.mark)
21    }
22}
23
24/// Represents a point tier in a `TextGrid`.
25#[derive(Clone, Constructor, Debug, Default, Getters, MutGetters, Setters)]
26pub struct Tier {
27    #[getset(get = "pub", set = "pub")]
28    name: String,
29    #[getset(get = "pub")]
30    xmin: f64,
31    #[getset(get = "pub")]
32    xmax: f64,
33    #[getset(get = "pub", get_mut = "pub", set = "pub")]
34    points: Vec<Point>,
35}
36
37impl Tier {
38    pub fn set_xmin(&mut self, xmin: f64, warn: Option<bool>) {
39        if warn.is_some_and(|b| b) {
40            let min_point = self
41                .points
42                .iter()
43                .filter_map(|point| {
44                    point
45                        .number
46                        .partial_cmp(&f64::INFINITY)
47                        .map(|_| point.number)
48                })
49                .min_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Greater)); // If invalid, return greater, since we're looking for the minimum
50
51            if min_point.is_some_and(|min| xmin > min) {
52                eprintln!(
53                    "Warning: Tier `{}` has a minimum point of {} but the set xmin is {}",
54                    self.name,
55                    min_point.unwrap_or_default(),
56                    xmin
57                );
58            }
59        }
60
61        self.xmin = xmin;
62    }
63
64    pub fn set_xmax(&mut self, xmax: f64, warn: Option<bool>) {
65        if warn.is_some_and(|b| b) {
66            let max_point = self
67                .points
68                .iter()
69                .filter_map(|point| {
70                    point
71                        .number
72                        .partial_cmp(&f64::INFINITY)
73                        .map(|_| point.number)
74                })
75                .max_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Less)); // If invalid, return less, since we're looking for the maximum
76
77            if max_point.is_some_and(|max| xmax < max) {
78                eprintln!(
79                    "Warning: Tier `{}` has a maximum point of {} but the set xmax is {}",
80                    self.name,
81                    max_point.unwrap_or_default(),
82                    xmax
83                );
84            }
85        }
86
87        self.xmax = xmax;
88    }
89
90    #[must_use]
91    pub fn get_size(&self) -> usize {
92        self.points.len()
93    }
94
95    /// Pushes a point to the tier.
96    /// Calls `reorder()` to ensure the points are sorted by their number after pushing.
97    ///
98    /// # Arguments
99    ///
100    /// * `point` - The point to push.
101    /// * `warn` - Whether to warn if the point is outside the tier's bounds.
102    pub fn push_point<W: Into<Option<bool>> + Copy>(&mut self, point: Point, warn: W) {
103        if warn.into().unwrap_or_default() {
104            if point.number < self.xmin {
105                eprintln!(
106                    "Warning: Tier `{}` has a number of {} but the tier has an xmin of  {}",
107                    self.name, point.number, self.xmin
108                );
109            }
110            if point.number > self.xmax {
111                eprintln!(
112                    "Warning: Tier `{}` has a number of {} but the tier has an xmax of {}",
113                    self.name, point.number, self.xmax
114                );
115            }
116        }
117
118        self.points.push(point);
119
120        self.reorder();
121    }
122
123    /// Pushes multiple points to the tier by calling `push_point()` for each point.
124    /// Calls `reorder()` to ensure the points are sorted by their number after pushing.
125    ///
126    /// # Arguments
127    ///
128    /// * `points` - The points to push.
129    /// * `warn` - Whether to warn if the point is outside the tier's bounds.
130    pub fn push_points<W: Into<Option<bool>> + Copy>(&mut self, points: Vec<Point>, warn: W) {
131        if warn.into().unwrap_or_default() {
132            for point in &points {
133                if point.number < self.xmin {
134                    eprintln!(
135                        "Warning: Tier `{}` has a number of {} but the tier has an xmin of  {}",
136                        self.name, point.number, self.xmin
137                    );
138                }
139                if point.number > self.xmax {
140                    eprintln!(
141                        "Warning: Tier `{}` has a number of {} but the tier has an xmax of {}",
142                        self.name, point.number, self.xmax
143                    );
144                }
145            }
146        }
147
148        self.points.extend(points);
149
150        self.reorder();
151    }
152
153    /// Checks for overlaps in the tier.
154    ///
155    /// # Returns
156    ///
157    /// A vector of the indices of the overlapping points or `None` if there are no overlaps.
158    #[must_use]
159    pub fn check_overlaps(&self) -> Option<Vec<(u64, u64)>> {
160        let mut overlaps: Vec<(u64, u64)> = Vec::new();
161        for (i, point) in self.points.iter().enumerate() {
162            for (j, other_point) in self.points.iter().enumerate() {
163                #[allow(clippy::float_cmp)]
164                if i != j && point.number == other_point.number {
165                    overlaps.push((i as u64, j as u64));
166                }
167            }
168        }
169        if overlaps.is_empty() {
170            None
171        } else {
172            Some(overlaps)
173        }
174    }
175
176    /// Reorders the points in the tier by their number.
177    pub fn reorder(&mut self) {
178        self.points
179            .sort_by(|a, b| a.number.partial_cmp(&b.number).unwrap_or(Ordering::Equal));
180    }
181}
182
183impl Display for Tier {
184    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
185        write!(
186            f,
187            "PointTier {}:
188                xmin:  {}
189                xmax:  {}
190                point count: {}",
191            self.name,
192            self.xmin,
193            self.xmax,
194            self.points.len()
195        )
196    }
197}
198
199#[cfg(test)]
200#[allow(clippy::float_cmp)]
201mod test_point {
202    #[test]
203    fn test_point() {
204        use crate::point::Point;
205
206        let point = Point::new(1.0, "test".to_string());
207        assert_eq!(point.number(), &1.0);
208        assert_eq!(point.mark(), "test");
209        assert_eq!(point.to_string(), "Point:\t1\ttest\n");
210    }
211}
212
213#[cfg(test)]
214#[allow(clippy::float_cmp)]
215mod test_point_tier {
216    #[test]
217    fn set_xmin() {
218        use crate::point::Tier;
219
220        let mut tier = Tier::new("test".to_string(), 0.0, 10.0, vec![]);
221        tier.set_xmin(5.0, Some(true));
222        assert_eq!(tier.xmin(), &5.0);
223    }
224
225    #[test]
226    fn set_xmax() {
227        use crate::point::Tier;
228
229        let mut tier = Tier::new("test".to_string(), 0.0, 10.0, vec![]);
230        tier.set_xmax(5.0, Some(true));
231        assert_eq!(tier.xmax(), &5.0);
232    }
233
234    #[test]
235    fn get_size() {
236        use crate::point::Tier;
237
238        let tier = Tier::new("test".to_string(), 0.0, 10.0, vec![]);
239        assert_eq!(tier.get_size(), 0);
240    }
241
242    #[test]
243    fn push_point() {
244        use crate::point::{Point, Tier};
245
246        let mut tier = Tier::new("test".to_string(), 0.0, 10.0, vec![]);
247        tier.push_point(Point::new(5.0, "test".to_string()), true);
248        assert_eq!(tier.get_size(), 1);
249    }
250
251    #[test]
252    fn push_points() {
253        use crate::point::{Point, Tier};
254
255        let mut tier = Tier::new("test".to_string(), 0.0, 10.0, vec![]);
256        tier.push_points(vec![Point::new(5.0, "test".to_string())], true);
257        assert_eq!(tier.get_size(), 1);
258    }
259
260    #[test]
261    fn check_overlaps() {
262        use crate::point::{Point, Tier};
263
264        let mut tier = Tier::new("test".to_string(), 0.0, 10.0, vec![]);
265        tier.push_points(
266            vec![
267                Point::new(5.0, "test".to_string()),
268                Point::new(5.0, "test".to_string()),
269            ],
270            true,
271        );
272        assert_eq!(tier.check_overlaps(), Some(vec![(0, 1), (1, 0)]));
273    }
274
275    #[test]
276    fn reorder() {
277        use crate::point::{Point, Tier};
278
279        let mut tier = Tier::new(
280            "test".to_string(),
281            0.0,
282            10.0,
283            vec![
284                Point::new(5.0, "test".to_string()),
285                Point::new(3.0, "test".to_string()),
286            ],
287        );
288        tier.reorder();
289        assert_eq!(tier.points()[0].number(), &3.0);
290        assert_eq!(tier.points()[1].number(), &5.0);
291    }
292}