[CdTime] type for more ergonomic handling of CD-ROM times and addresses

3use bincode::{Decode, Encode};
4use std::cmp::Ordering;
5use std::fmt::{Display, Formatter};
6use std::ops::{Add, AddAssign, Sub, SubAssign};
7use std::str::FromStr;
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)]
10pub struct CdTime {
11    pub minutes: u8,
12    pub seconds: u8,
13    pub frames: u8,
14}
15
16impl CdTime {
17    pub const ZERO: Self = Self { minutes: 0, seconds: 0, frames: 0 };
18    pub const SECTOR_0_START: Self = Self { minutes: 0, seconds: 2, frames: 0 };
19    pub const DISC_END: Self = Self { minutes: 60, seconds: 3, frames: 74 };
20
21    pub const MAX_MINUTES: u8 = 80;
22    pub const SECONDS_PER_MINUTE: u8 = 60;
23    pub const FRAMES_PER_SECOND: u8 = 75;
24
25    pub const MAX_SECTORS: u32 = 360000;

Create a new CdTime value.

This function will panic if any field is out of bounds. Call [CdTime::new_checked] if this behavior is not desired.

Panics

This function will panic if any field is out of bounds:

  • Minutes must be less than 80
  • Seconds must be less than 60
  • Frames must be less than 75
38    #[must_use]
39    pub fn new(minutes: u8, seconds: u8, frames: u8) -> Self {
40        assert!(minutes < Self::MAX_MINUTES, "Minutes must be less than {}", Self::MAX_MINUTES);
41        assert!(
42            seconds < Self::SECONDS_PER_MINUTE,
43            "Seconds must be less than {}",
44            Self::SECONDS_PER_MINUTE
45        );
46        assert!(
47            frames < Self::FRAMES_PER_SECOND,
48            "Frames must be less than {}",
49            Self::FRAMES_PER_SECOND
50        );
51
52        Self { minutes, seconds, frames }
53    }
55    #[must_use]
56    pub fn new_checked(minutes: u8, seconds: u8, frames: u8) -> Option<Self> {
57        (minutes < Self::MAX_MINUTES
58            && seconds < Self::SECONDS_PER_MINUTE
59            && frames < Self::FRAMES_PER_SECOND)
60            .then_some(Self { minutes, seconds, frames })
61    }
62
63    #[must_use]
64    pub fn to_sector_number(self) -> u32 {
65        (u32::from(Self::SECONDS_PER_MINUTE) * u32::from(self.minutes) + u32::from(self.seconds))
66            * u32::from(Self::FRAMES_PER_SECOND)
67            + u32::from(self.frames)
68    }

Convert an absolute sector number to a CdTime value.

Panics

This function will panic if sector_number is greater than 360_000, which is the number of sectors in an 80-minute CD.

76    #[must_use]
77    pub fn from_sector_number(sector_number: u32) -> Self {
78        // All Sega CD sector numbers are less than 360,000 (80 minutes)
79        assert!(sector_number < Self::MAX_SECTORS, "Invalid sector number: {sector_number}");
80
81        let frames = sector_number % u32::from(Self::FRAMES_PER_SECOND);
82        let seconds = (sector_number / u32::from(Self::FRAMES_PER_SECOND))
83            % u32::from(Self::SECONDS_PER_MINUTE);
84        let minutes = sector_number
85            / (u32::from(Self::FRAMES_PER_SECOND) * u32::from(Self::SECONDS_PER_MINUTE));
86
87        Self::new(minutes as u8, seconds as u8, frames as u8)
88    }
90    #[must_use]
91    pub fn to_frames(self) -> u32 {
92        let seconds: u32 = self.seconds.into();
93        let minutes: u32 = self.minutes.into();
94        let frames: u32 = self.frames.into();
95
96        let frames_per_second: u32 = Self::FRAMES_PER_SECOND.into();
97        let seconds_per_minute: u32 = Self::SECONDS_PER_MINUTE.into();
98
99        frames + frames_per_second * (seconds + seconds_per_minute * minutes)
100    }
101
102    #[must_use]
103    pub fn from_frames(frames: u32) -> Self {
104        let minutes =
105            frames / (u32::from(Self::FRAMES_PER_SECOND) * u32::from(Self::SECONDS_PER_MINUTE));
106        let seconds =
107            (frames / u32::from(Self::FRAMES_PER_SECOND)) % u32::from(Self::SECONDS_PER_MINUTE);
108        let frames = frames % u32::from(Self::FRAMES_PER_SECOND);
109
110        Self::new(minutes as u8, seconds as u8, frames as u8)
111    }
112
113    #[must_use]
114    pub fn saturating_sub(self, other: Self) -> Self {
115        if self <= other { CdTime::ZERO } else { self - other }
116    }
117}
118
119impl Add for CdTime {
120    type Output = Self;
121
122    fn add(self, rhs: Self) -> Self::Output {
123        let (frames, carried) = add(self.frames, rhs.frames, false, Self::FRAMES_PER_SECOND);
124        let (seconds, carried) = add(self.seconds, rhs.seconds, carried, Self::SECONDS_PER_MINUTE);
125        let (minutes, _) = add(self.minutes, rhs.minutes, carried, Self::MAX_MINUTES);
126
127        Self { minutes, seconds, frames }
128    }
129}
130
131impl AddAssign for CdTime {
132    fn add_assign(&mut self, rhs: Self) {
133        *self = *self + rhs;
134    }
135}
136
137impl Sub for CdTime {
138    type Output = Self;
139
140    fn sub(self, rhs: Self) -> Self::Output {
141        let (frames, borrowed) = sub(self.frames, rhs.frames, false, Self::FRAMES_PER_SECOND);
142        let (seconds, borrowed) =
143            sub(self.seconds, rhs.seconds, borrowed, Self::SECONDS_PER_MINUTE);
144        let (minutes, _) = sub(self.minutes, rhs.minutes, borrowed, Self::MAX_MINUTES);
145
146        Self { minutes, seconds, frames }
147    }
148}
149
150impl SubAssign for CdTime {
151    fn sub_assign(&mut self, rhs: Self) {
152        *self = *self - rhs;
153    }
154}
155
156impl PartialOrd for CdTime {
157    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
158        Some(self.cmp(other))
159    }
160}
161
162impl Ord for CdTime {
163    fn cmp(&self, other: &Self) -> Ordering {
164        self.minutes
165            .cmp(&other.minutes)
166            .then(self.seconds.cmp(&other.seconds))
167            .then(self.frames.cmp(&other.frames))
168    }
169}
170
171fn add(a: u8, b: u8, overflow: bool, base: u8) -> (u8, bool) {
172    let sum = a + b + u8::from(overflow);
173    (sum % base, sum >= base)
174}
175
176fn sub(a: u8, b: u8, overflow: bool, base: u8) -> (u8, bool) {
177    let operand_r = b + u8::from(overflow);
178    if a < operand_r { (base - (operand_r - a), true) } else { (a - operand_r, false) }
179}
180
181impl FromStr for CdTime {
182    type Err = String;
183
184    fn from_str(s: &str) -> Result<Self, Self::Err> {
185        let bytes = s.as_bytes();
186        if bytes.len() != 8 {
187            return Err(format!("Invalid time length: {}", bytes.len()));
188        }
189
190        if bytes[2] != b':' || bytes[5] != b':' {
191            return Err(format!("Unexpected time format: {s}"));
192        }
193
194        let err_fn = |_err| format!("Invalid time string: {s}");
195        let minutes: u8 = s[0..2].parse().map_err(err_fn)?;
196        let seconds: u8 = s[3..5].parse().map_err(err_fn)?;
197        let frames: u8 = s[6..8].parse().map_err(err_fn)?;
198
199        Ok(CdTime { minutes, seconds, frames })
200    }
201}
202
203impl Display for CdTime {
204    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
205        write!(f, "{:02}:{:02}:{:02}", self.minutes, self.seconds, self.frames)
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn cd_time_add() {
215        // No carries
216        assert_eq!(CdTime::new(10, 20, 30) + CdTime::new(15, 25, 35), CdTime::new(25, 45, 65));
217
218        // Frames carry
219        assert_eq!(CdTime::new(10, 20, 30) + CdTime::new(15, 25, 55), CdTime::new(25, 46, 10));
220
221        // Seconds carry
222        assert_eq!(CdTime::new(10, 20, 30) + CdTime::new(15, 55, 35), CdTime::new(26, 15, 65));
223    }
224
225    #[test]
226    fn cd_time_sub() {
227        // No borrows
228        assert_eq!(CdTime::new(12, 13, 14) - CdTime::new(7, 7, 7), CdTime::new(5, 6, 7));
229
230        // Frames borrow
231        assert_eq!(CdTime::new(5, 4, 3) - CdTime::new(1, 1, 10), CdTime::new(4, 2, 68));
232
233        // Seconds borrow
234        assert_eq!(CdTime::new(15, 5, 39) - CdTime::new(13, 16, 25), CdTime::new(1, 49, 14));
235    }
236}