textgridde_rs/
interval.rs

1use std::{
2    cmp::Ordering,
3    fmt::{self, Display, Formatter},
4};
5
6use derive_more::Constructor;
7use getset::{Getters, MutGetters, Setters};
8
9/// An "interval," used in Praat as a specific period of time with an associated label.
10#[derive(Clone, Constructor, Debug, Default, Getters, Setters)]
11pub struct Interval {
12    #[getset(get = "pub")]
13    xmin: f64,
14    #[getset(get = "pub")]
15    xmax: f64,
16    #[getset(get = "pub", set = "pub")]
17    text: String,
18}
19
20impl Interval {
21    /// Returns the duration of the interval.
22    #[must_use]
23    pub fn get_duration(&self) -> f64 {
24        self.xmax - self.xmin
25    }
26
27    /// Returns the midpoint of the interval.
28    #[must_use]
29    pub fn get_midpoint(&self) -> f64 {
30        (self.xmin + self.xmax) / 2.0
31    }
32
33    /// Sets the xmin value of the interval.
34    ///
35    /// # Arguments
36    ///
37    /// * `xmin` - The xmin value to set.
38    pub fn set_xmin(&mut self, xmin: f64) {
39        self.xmin = xmin;
40    }
41
42    /// Sets the xmax value of the interval.
43    ///
44    /// # Arguments
45    ///
46    /// * `xmax` - The xmax value to set.
47    pub fn set_xmax(&mut self, xmax: f64) {
48        self.xmax = xmax;
49    }
50}
51
52impl Display for Interval {
53    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
54        writeln!(f, "Interval")?;
55        writeln!(f, "    xmin = {}", self.xmin)?;
56        writeln!(f, "    xmax = {}", self.xmax)?;
57        writeln!(f, "    text = \"{}\"", self.text)?;
58        Ok(())
59    }
60}
61
62/// Represents an interval tier in a `TextGrid`.
63#[derive(Clone, Constructor, Debug, Default, Getters, MutGetters, Setters)]
64pub struct Tier {
65    #[getset(get = "pub", set = "pub")]
66    name: String,
67    #[getset(get = "pub")]
68    xmin: f64,
69    #[getset(get = "pub")]
70    xmax: f64,
71    #[getset(get = "pub", get_mut = "pub")]
72    intervals: Vec<Interval>,
73}
74
75impl Tier {
76    /// Sets the minimum x value for the interval tier.
77    ///
78    /// # Arguments
79    ///
80    /// * `xmin` - The minimum x value to set.
81    /// * `warn` - If `true`, displays a warning if the minimum point of any interval is greater than `xmin`.
82    pub fn set_xmin<T: Into<Option<bool>>>(&mut self, xmin: f64, warn: T) {
83        if warn.into().unwrap_or_default() {
84            let min_point = self
85                .intervals
86                .iter()
87                .filter_map(|intervals| {
88                    intervals
89                        .xmin
90                        .partial_cmp(&f64::INFINITY)
91                        .map(|_| intervals.xmin)
92                })
93                .min_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Greater)); // If invalid, return greater, since we're looking for the minimum
94
95            if min_point.is_some_and(|min| xmin > min) {
96                eprintln!("Warning: Tier `{}` has a minimum point of {} but the TextGrid has an xmin of {}", self.name, min_point.unwrap_or_default(), xmin);
97            }
98        }
99
100        self.xmin = xmin;
101    }
102
103    /// Sets the maximum x value for the interval tier.
104    ///
105    /// # Arguments
106    ///
107    /// * `xmax` - The maximum x value to set.
108    /// * `warn` - If `true`, displays a warning if the maximum point of any interval is less than `xmax`.
109    pub fn set_xmax<W: Into<Option<bool>>>(&mut self, xmax: f64, warn: W) {
110        if warn.into().unwrap_or_default() {
111            let max_point = self
112                .intervals
113                .iter()
114                .filter_map(|interval| {
115                    interval
116                        .xmax
117                        .partial_cmp(&f64::INFINITY)
118                        .map(|_| interval.xmax)
119                })
120                .max_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Less)); // If invalid, return less, since we're looking for the maximum
121
122            if max_point.is_some_and(|max| xmax < max) {
123                eprintln!("Warning: Tier `{}` has a minimum point of {} but the TextGrid has an xmax of {}", self.name, max_point.unwrap_or_default(), xmax);
124            }
125        }
126
127        self.xmax = xmax;
128    }
129
130    /// Returns the number of intervals in the interval tier.
131    #[must_use]
132    pub fn get_size(&self) -> usize {
133        self.intervals.len()
134    }
135
136    /// Pushes an interval to the interval tier.
137    /// Calls `reorder()` to ensure the intervals are sorted by their minimum x value after pushing the interval.
138    ///
139    /// # Arguments
140    ///
141    /// * `interval` - The interval to push.
142    /// * `warn` - If `Some(true)`, displays a warning if the minimum point of the interval is less than the minimum x value of the interval tier.
143    pub fn push_interval<W: Into<Option<bool>>>(&mut self, interval: Interval, warn: W) {
144        if warn.into().unwrap_or_default() && interval.xmin < self.xmin {
145            eprintln!(
146                "Warning: Tier `{}` has a minimum point of {} but the TextGrid has an xmin of {}",
147                self.name, interval.xmin, self.xmin
148            );
149        }
150        self.intervals.push(interval);
151
152        self.reorder();
153    }
154
155    /// Pushes multiple intervals to the interval tier vector.
156    /// Calls `reorder()` afterwards to ensure the intervals are sorted by their minimum x value after pushing the intervals.
157    ///
158    /// # Arguments
159    ///
160    /// * `intervals` - The intervals to push.
161    /// * `warn` - If `Some(true)`, displays a warning if the minimum point of any interval is less than the minimum x value of the interval tier.
162    pub fn push_intervals<W: Into<Option<bool>> + Copy>(
163        &mut self,
164        intervals: Vec<Interval>,
165        warn: W,
166    ) {
167        for interval in &intervals {
168            if warn.into().unwrap_or_default() {
169                if interval.xmin < self.xmin {
170                    eprintln!(
171                        "Warning: Tier `{}` has a minimum point of {} but the TextGrid has an xmin of {}",
172                        self.name, interval.xmin, self.xmin
173                    );
174                }
175                if interval.xmax > self.xmax {
176                    eprintln!(
177                        "Warning: Tier `{}` has a maximum point of {} but the TextGrid has an xmax of {}",
178                        self.name, interval.xmax, self.xmax
179                    );
180                }
181            }
182        }
183
184        self.intervals.extend(intervals);
185
186        self.reorder();
187    }
188
189    /// Sets the intervals of the interval tier.
190    ///
191    /// # Arguments
192    ///
193    /// * `intervals` - The intervals to set.
194    /// * `warn` - If `Some(true)`, displays a warning if any interval's minimum point is less than the minimum x value of the interval tier or if any interval's maximum point is greater than the maximum x value of the interval tier.
195    pub fn set_intervals<W: Into<Option<bool>>>(&mut self, intervals: Vec<Interval>, warn: W) {
196        if warn.into().unwrap_or_default() {
197            for interval in &intervals {
198                if interval.xmin < self.xmin {
199                    eprintln!(
200                        "Warning: Tier `{}` has a minimum point of {} but the TextGrid has an xmin of {}",
201                        self.name, interval.xmin, self.xmin
202                    );
203                }
204                if interval.xmax > self.xmax {
205                    eprintln!(
206                        "Warning: Tier `{}` has a maximum point of {} but the TextGrid has an xmax of {}",
207                        self.name, interval.xmax, self.xmax
208                    );
209                }
210            }
211        }
212
213        self.intervals = intervals;
214    }
215
216    /// Sorts the intervals in the interval tier by their minimum x value.
217    fn reorder(&mut self) {
218        self.intervals
219            .sort_by(|a, b| a.xmin.partial_cmp(&b.xmin).unwrap_or(Ordering::Equal));
220    }
221
222    /// Checks for overlaps in the interval tier.
223    /// Calls `reorder` to ensure the intervals are sorted by their minimum x value before checking for overlaps.
224    ///
225    /// # Returns
226    ///
227    /// A vector of pairs of the overlapping intervals' indices or `None` if there are no overlaps.
228    #[must_use]
229    pub fn check_overlaps(&self) -> Option<Vec<(u64, u64)>> {
230        let mut overlaps: Vec<(u64, u64)> = Vec::new();
231
232        // iterate over each pair of intervals, checking to make sure the xmax of the first interval is perfectly equal to the xmin of the second interval
233        for (i, window) in self.intervals.windows(2).enumerate() {
234            let interval = &window[0];
235            let next_interval = &window[1];
236
237            #[allow(clippy::float_cmp)]
238            if interval.xmax != next_interval.xmin {
239                overlaps.push((i as u64, (i + 1) as u64));
240            }
241        }
242
243        if overlaps.is_empty() {
244            None
245        } else {
246            Some(overlaps)
247        }
248    }
249
250    /// Fixes gaps/overlaps in the interval tier.
251    /// Calls `reorder` to ensure the intervals are sorted by their minimum x value before fixing gaps/overlaps.
252    ///
253    /// # Arguments
254    ///
255    /// * `prefer_first` - `true` by default. If `true`, prefers the first interval in the case of a gap. If `false`, prefers the second interval in the case of a gap.
256    ///
257    /// # Panics
258    ///
259    /// If the amount of intervals exceeds `isize::MAX`.
260    pub fn fix_boundaries<P: Into<Option<bool>> + Copy>(&mut self, prefer_first: P) {
261        if self.intervals.len() < 2 {
262            return;
263        }
264
265        self.reorder();
266
267        // Iterate over each pair of intervals, checking to make sure the xmax of the first
268        // interval is perfectly equal to the xmin of the second interval. If not, handle
269        // the gap by either modifying the xmax of the first interval or the xmin of the
270        // second interval, depending on the value of `prefer_first`.
271        if prefer_first.into().unwrap_or(true) {
272            // Iterate in reverse, so we can modify the less-preferred interval without
273            // affecting the preferred interval
274            for i in (1..self.intervals.len()).rev() {
275                let prev_interval = self.intervals[i - 1].clone();
276                let interval = &mut self.intervals[i];
277
278                #[allow(clippy::float_cmp)]
279                if interval.xmin != prev_interval.xmax {
280                    interval.xmin = prev_interval.xmax;
281                }
282            }
283        } else {
284            for i in 0..self.intervals.len() - 1 {
285                let next_interval = self.intervals[i + 1].clone();
286                let interval = &mut self.intervals[i];
287
288                #[allow(clippy::float_cmp)]
289                if interval.xmax != next_interval.xmin {
290                    interval.xmax = next_interval.xmin;
291                }
292            }
293        }
294    }
295
296    /// Fills gaps within an `IntervalTier` with the specified text, ensuring no time period
297    /// is left empty.
298    ///
299    /// # Arguments
300    ///
301    /// * `text` - The text to fill the gaps with.
302    ///
303    /// # Panics
304    ///
305    /// If the amount of intervals exceeds `isize::MAX`.
306    #[allow(clippy::float_cmp)]
307    pub fn fill_gaps(&mut self, text: &str) {
308        if self.intervals.len() < 2 {
309            return;
310        }
311
312        self.reorder();
313
314        let first_xmin = self.intervals.first().unwrap().xmin;
315        if first_xmin != self.xmin {
316            let new_interval = Interval::new(self.xmin, first_xmin, text.to_string());
317            self.intervals.insert(0, new_interval);
318        }
319
320        let last_xmax = self.intervals.last().unwrap().xmax;
321        if last_xmax != self.xmax {
322            let new_interval = Interval::new(last_xmax, self.xmax, text.to_string());
323            self.intervals.push(new_interval);
324        }
325
326        for (index, window) in self.intervals.clone().windows(2).enumerate() {
327            let interval = &window[0];
328            let next_interval = &window[1];
329
330            #[allow(clippy::float_cmp)]
331            if interval.xmax != next_interval.xmin {
332                let new_interval =
333                    Interval::new(interval.xmax, next_interval.xmin, text.to_string());
334                self.intervals.insert(index + 1, new_interval);
335            }
336        }
337    }
338}
339
340impl Display for Tier {
341    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
342        write!(
343            f,
344            "IntervalTier {}:
345                xmin:  {}
346                xmax:  {}
347                interval count: {}",
348            self.name,
349            self.xmin,
350            self.xmax,
351            self.intervals.len()
352        )
353    }
354}
355
356#[cfg(test)]
357#[allow(clippy::float_cmp)]
358mod test_interval_tier {
359    use crate::interval::Interval;
360
361    #[test]
362    fn get_duration() {
363        let interval = Interval::new(0.0, 2.3, "test".to_string());
364
365        assert_eq!(interval.get_duration(), 2.3);
366    }
367
368    #[test]
369    fn get_midpoint() {
370        let interval = Interval::new(0.0, 2.3, "test".to_string());
371
372        assert_eq!(interval.get_midpoint(), 1.15);
373    }
374
375    #[test]
376    fn set_xmin() {
377        let mut interval = Interval::new(0.0, 2.3, "test".to_string());
378
379        interval.set_xmin(1.0);
380
381        assert_eq!(interval.xmin, 1.0);
382    }
383
384    #[test]
385    fn set_xmax() {
386        let mut interval = Interval::new(0.0, 2.3, "test".to_string());
387
388        interval.set_xmax(1.0);
389
390        assert_eq!(interval.xmax, 1.0);
391    }
392
393    #[test]
394    fn to_string() {
395        let interval = Interval::new(0.0, 2.3, "test".to_string());
396
397        assert_eq!(
398            interval.to_string(),
399            "Interval\n    xmin = 0\n    xmax = 2.3\n    text = \"test\"\n"
400        );
401    }
402}
403
404#[cfg(test)]
405#[allow(clippy::float_cmp)]
406mod test_tier {
407    use crate::interval::{Interval, Tier};
408
409    #[test]
410    fn set_xmin() {
411        let mut tier = Tier::new("test".to_string(), 0.0, 2.3, Vec::new());
412
413        tier.set_xmin(1.0, Some(true));
414
415        assert_eq!(tier.xmin, 1.0);
416    }
417
418    #[test]
419    fn set_xmax() {
420        let mut tier = Tier::new("test".to_string(), 0.0, 2.3, Vec::new());
421
422        tier.set_xmax(1.0, Some(true));
423
424        assert_eq!(tier.xmax, 1.0);
425    }
426
427    #[test]
428    fn get_size() {
429        let tier = Tier::new("test".to_string(), 0.0, 2.3, Vec::new());
430
431        assert_eq!(tier.get_size(), 0);
432    }
433
434    #[test]
435    fn push_interval() {
436        let mut tier = Tier::new("test".to_string(), 0.0, 2.3, Vec::new());
437
438        tier.push_interval(Interval::new(0.0, 1.0, "test".to_string()), Some(true));
439
440        assert_eq!(tier.intervals.len(), 1);
441    }
442
443    #[test]
444    fn push_intervals() {
445        let mut tier = Tier::new("test".to_string(), 0.0, 2.3, Vec::new());
446
447        tier.push_intervals(
448            vec![
449                Interval::new(0.0, 1.0, "test".to_string()),
450                Interval::new(1.0, 2.0, "test".to_string()),
451            ],
452            Some(true),
453        );
454
455        assert_eq!(tier.intervals.len(), 2);
456    }
457
458    #[test]
459    fn set_intervals() {
460        let mut tier = Tier::new("test".to_string(), 0.0, 2.3, Vec::new());
461
462        tier.set_intervals(
463            vec![
464                Interval::new(0.0, 1.0, "test".to_string()),
465                Interval::new(1.0, 2.0, "test".to_string()),
466            ],
467            Some(true),
468        );
469
470        assert_eq!(tier.intervals.len(), 2);
471    }
472
473    #[test]
474    #[allow(clippy::float_cmp)]
475    fn reorder() {
476        let mut tier = Tier::new("test".to_string(), 0.0, 2.3, Vec::new());
477
478        tier.push_intervals(
479            vec![
480                Interval::new(1.0, 2.0, "test".to_string()),
481                Interval::new(0.0, 1.0, "test".to_string()),
482            ],
483            Some(true),
484        );
485
486        tier.reorder();
487
488        assert_eq!(tier.intervals[0].xmin, 0.0);
489        assert_eq!(tier.intervals[1].xmin, 1.0);
490    }
491
492    mod check_overlaps {
493        use crate::{
494            interval::{Interval, Tier as IntervalTier},
495            textgrid::{TextGrid, Tier},
496        };
497
498        #[test]
499        fn no_overlap() {
500            let mut textgrid = TextGrid::new(0.0, 2.3, Vec::new(), "test".to_string());
501
502            textgrid.push_tier(
503                Tier::IntervalTier(IntervalTier::new(
504                    "John".to_string(),
505                    0.0,
506                    2.3,
507                    vec![
508                        Interval::new(0.0, 1.5, "daisy bell".to_string()),
509                        Interval::new(1.5, 2.3, "daisy bell".to_string()),
510                    ],
511                )),
512                false,
513            );
514
515            let overlaps = textgrid.check_overlaps();
516
517            assert!(overlaps.is_none());
518        }
519
520        #[test]
521        fn overlap() {
522            let mut textgrid = TextGrid::new(0.0, 2.3, Vec::new(), "test".to_string());
523
524            textgrid.push_tier(
525                Tier::IntervalTier(IntervalTier::new(
526                    "John".to_string(),
527                    0.0,
528                    2.3,
529                    vec![
530                        Interval::new(0.0, 1.5, "daisy bell".to_string()),
531                        Interval::new(1.0, 2.3, "daisy bell".to_string()),
532                    ],
533                )),
534                false,
535            );
536
537            let overlaps = textgrid.check_overlaps().unwrap();
538
539            assert_eq!(overlaps.len(), 1);
540            assert_eq!(overlaps[0].0, "John");
541            assert_eq!(overlaps[0].1, (0, 1));
542        }
543    }
544
545    #[allow(clippy::float_cmp)]
546    mod fix_boundaries {
547        use crate::interval::{Interval, Tier};
548
549        #[test]
550        fn prefer_first() {
551            let mut tier = Tier::new("test".to_string(), 0.0, 2.3, Vec::new());
552
553            tier.push_intervals(
554                vec![
555                    Interval::new(0.0, 1.2, "daisy".to_string()),
556                    Interval::new(1.0, 1.75, "bell".to_string()),
557                    Interval::new(1.5, 2.5, "answer".to_string()),
558                    Interval::new(2.0, 5.0, "do".to_string()),
559                ],
560                false,
561            );
562
563            tier.fix_boundaries(true);
564
565            assert_eq!(tier.intervals()[0].xmin(), &0.0);
566            assert_eq!(tier.intervals()[0].xmax(), &1.2);
567            assert_eq!(tier.intervals()[1].xmin(), &1.2);
568            assert_eq!(tier.intervals()[1].xmax(), &1.75);
569            assert_eq!(tier.intervals()[2].xmin(), &1.75);
570            assert_eq!(tier.intervals()[2].xmax(), &2.5);
571            assert_eq!(tier.intervals()[3].xmin(), &2.5);
572            assert_eq!(tier.intervals()[3].xmax(), &5.0);
573        }
574
575        #[test]
576        fn prefer_last() {
577            let mut tier = Tier::new("test".to_string(), 0.0, 2.3, Vec::new());
578
579            tier.push_intervals(
580                vec![
581                    Interval::new(0.0, 1.2, "daisy".to_string()),
582                    Interval::new(1.0, 1.75, "bell".to_string()),
583                    Interval::new(1.5, 2.5, "answer".to_string()),
584                    Interval::new(2.0, 5.0, "do".to_string()),
585                ],
586                false,
587            );
588
589            tier.fix_boundaries(false);
590
591            assert_eq!(tier.intervals()[0].xmin(), &0.0);
592            assert_eq!(tier.intervals()[0].xmax(), &1.0);
593            assert_eq!(tier.intervals()[1].xmin(), &1.0);
594            assert_eq!(tier.intervals()[1].xmax(), &1.5);
595            assert_eq!(tier.intervals()[2].xmin(), &1.5);
596            assert_eq!(tier.intervals()[2].xmax(), &2.0);
597            assert_eq!(tier.intervals()[3].xmin(), &2.0);
598            assert_eq!(tier.intervals()[3].xmax(), &5.0);
599        }
600    }
601
602    #[test]
603    #[allow(clippy::float_cmp)]
604    fn fill_gaps() {
605        let mut tier = Tier::new("test".to_string(), 0.0, 2.3, Vec::new());
606
607        tier.push_intervals(
608            vec![
609                Interval::new(0.0, 1.2, "daisy".to_string()),
610                Interval::new(1.5, 2.3, "bell".to_string()),
611            ],
612            false,
613        );
614
615        tier.fill_gaps("gap");
616
617        assert_eq!(tier.intervals()[1].text(), "gap");
618        assert_eq!(tier.intervals()[1].xmin(), &1.2);
619        assert_eq!(tier.intervals()[1].xmax(), &1.5);
620    }
621
622    #[test]
623    fn to_string() {
624        let tier = Tier::new("test".to_string(), 0.0, 2.3, Vec::new());
625
626        assert_eq!(
627            tier.to_string(),
628            "IntervalTier test:
629                xmin:  0
630                xmax:  2.3
631                interval count: 0"
632        );
633    }
634}