1use crate::defaults::{FloatType, UnsignedIntegerType};
16use crate::duration::Duration;
17use crate::error::{Error, Result};
18
19const BEAT_COUNT_NAMES: [&str; 9] = [
23 "Empty",
24 "Single",
25 "Duple",
26 "Triple",
27 "Quadruple",
28 "Quintuple",
29 "Sextuple",
30 "Septuple",
31 "Octuple",
32];
33
34#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
39pub enum BeatDivision {
40 Other,
43 Simple,
45 Compound,
47}
48
49impl BeatDivision {
50 pub fn music21_name(self) -> &'static str {
52 match self {
53 Self::Other => "Other",
54 Self::Simple => "Simple",
55 Self::Compound => "Compound",
56 }
57 }
58
59 pub fn count(self) -> UnsignedIntegerType {
64 match self {
65 Self::Other => 1,
66 Self::Simple => 2,
67 Self::Compound => 3,
68 }
69 }
70}
71
72#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
84#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
85pub struct TimeSignature {
86 numerator: UnsignedIntegerType,
87 denominator: UnsignedIntegerType,
88}
89
90impl Default for TimeSignature {
91 fn default() -> Self {
93 Self::common()
94 }
95}
96
97impl TimeSignature {
98 pub fn new(numerator: UnsignedIntegerType, denominator: UnsignedIntegerType) -> Result<Self> {
103 if numerator == 0 {
104 return Err(Error::Meter(
105 "time signature numerator must be non-zero".to_string(),
106 ));
107 }
108 if denominator == 0 {
109 return Err(Error::Meter(
110 "time signature denominator must be non-zero".to_string(),
111 ));
112 }
113 Ok(Self {
114 numerator,
115 denominator,
116 })
117 }
118
119 pub fn from_ratio_string(ratio: &str) -> Result<Self> {
121 let (numerator, denominator) = ratio.split_once('/').ok_or_else(|| {
122 Error::Meter(format!(
123 "time signature {ratio:?} is not `numerator/denominator`"
124 ))
125 })?;
126 let parse = |part: &str, label: &str| {
127 part.trim().parse::<UnsignedIntegerType>().map_err(|_| {
128 Error::Meter(format!("cannot read a {label} from {part:?} in {ratio:?}"))
129 })
130 };
131 Self::new(
132 parse(numerator, "numerator")?,
133 parse(denominator, "denominator")?,
134 )
135 }
136
137 pub fn common() -> Self {
139 Self {
140 numerator: 4,
141 denominator: 4,
142 }
143 }
144
145 pub fn cut() -> Self {
147 Self {
148 numerator: 2,
149 denominator: 2,
150 }
151 }
152
153 pub fn numerator(self) -> UnsignedIntegerType {
155 self.numerator
156 }
157
158 pub fn denominator(self) -> UnsignedIntegerType {
160 self.denominator
161 }
162
163 pub fn ratio_string(self) -> String {
165 format!("{}/{}", self.numerator, self.denominator)
166 }
167
168 pub fn bar_quarter_length(self) -> FloatType {
170 FloatType::from(self.numerator) * 4.0 / FloatType::from(self.denominator)
171 }
172
173 pub fn bar_duration(self) -> Duration {
175 Duration::new(self.bar_quarter_length())
176 .expect("a non-zero numerator and denominator give a positive finite bar length")
177 }
178
179 pub fn beat_count(self) -> UnsignedIntegerType {
185 match self.numerator {
186 1 => 1,
187 2 => 2,
188 3 if self.denominator > 4 => 1,
191 3 => 3,
192 4 => 4,
193 6 => 2,
194 9 => 3,
195 12 => 4,
196 numerator if numerator >= 15 && numerator.is_multiple_of(3) => numerator / 3,
197 numerator => numerator,
198 }
199 }
200
201 pub fn beat_count_name(self) -> String {
205 let count = self.beat_count();
206 BEAT_COUNT_NAMES
207 .get(count as usize)
208 .map_or_else(|| format!("{count}-uple"), |name| (*name).to_string())
209 }
210
211 pub fn beat_quarter_length(self) -> FloatType {
218 self.bar_quarter_length() / FloatType::from(self.beat_count())
219 }
220
221 pub fn beat_duration(self) -> Duration {
223 Duration::new(self.beat_quarter_length())
224 .expect("a positive bar length divided by a positive beat count stays positive")
225 }
226
227 pub fn beat_division(self) -> BeatDivision {
229 if self.beat_count() == 1 {
230 BeatDivision::Other
231 } else if matches!(self.numerator, 6 | 9 | 12)
232 || (self.numerator >= 15 && self.numerator.is_multiple_of(3))
233 {
234 BeatDivision::Compound
235 } else {
236 BeatDivision::Simple
237 }
238 }
239
240 pub fn beat_division_count(self) -> UnsignedIntegerType {
242 self.beat_division().count()
243 }
244
245 pub fn is_compound(self) -> bool {
247 self.beat_division() == BeatDivision::Compound
248 }
249
250 pub fn classification(self) -> String {
252 format!(
253 "{} {}",
254 self.beat_division().music21_name(),
255 self.beat_count_name()
256 )
257 }
258
259 pub fn beat_offsets(self) -> Vec<FloatType> {
261 let beat = self.beat_quarter_length();
262 (0..self.beat_count())
263 .map(|index| FloatType::from(index) * beat)
264 .collect()
265 }
266
267 pub fn beat_at_offset(self, offset: FloatType) -> Result<UnsignedIntegerType> {
272 if !offset.is_finite() || offset < 0.0 || offset >= self.bar_quarter_length() {
273 return Err(Error::Meter(format!(
274 "offset {offset} is outside a {} bar of {} quarter lengths",
275 self.ratio_string(),
276 self.bar_quarter_length()
277 )));
278 }
279 let beat = (offset / self.beat_quarter_length()).floor();
280 Ok(beat as UnsignedIntegerType + 1)
281 }
282}
283
284impl std::fmt::Display for TimeSignature {
285 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286 f.write_str(&self.ratio_string())
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293
294 fn ts(ratio: &str) -> TimeSignature {
295 TimeSignature::from_ratio_string(ratio).expect("valid time signature")
296 }
297
298 #[test]
299 fn common_and_cut_time_match_their_ratios() {
300 assert_eq!(TimeSignature::common().ratio_string(), "4/4");
301 assert_eq!(TimeSignature::cut().ratio_string(), "2/2");
302 assert_eq!(TimeSignature::default(), TimeSignature::common());
303 }
304
305 #[test]
306 fn bar_and_beat_lengths_follow_the_ratio() {
307 assert_eq!(ts("4/4").bar_quarter_length(), 4.0);
308 assert_eq!(ts("5/16").bar_quarter_length(), 1.25);
309 assert_eq!(ts("3/8").bar_quarter_length(), 1.5);
310 assert_eq!(ts("6/8").beat_quarter_length(), 1.5);
311 assert_eq!(ts("4/4").beat_quarter_length(), 1.0);
312 assert_eq!(ts("2/2").beat_duration().quarter_length(), 2.0);
313 }
314
315 #[test]
316 fn compound_meters_beat_in_threes() {
317 for (ratio, beats, division) in [
318 ("6/8", 2, BeatDivision::Compound),
319 ("9/8", 3, BeatDivision::Compound),
320 ("12/8", 4, BeatDivision::Compound),
321 ("15/8", 5, BeatDivision::Compound),
322 ("18/8", 6, BeatDivision::Compound),
323 ("24/8", 8, BeatDivision::Compound),
324 ] {
325 assert_eq!(ts(ratio).beat_count(), beats, "{ratio}");
326 assert_eq!(ts(ratio).beat_division(), division, "{ratio}");
327 assert!(ts(ratio).is_compound(), "{ratio}");
328 }
329 }
330
331 #[test]
332 fn three_is_the_one_denominator_sensitive_numerator() {
333 assert_eq!(ts("3/2").beat_count(), 3);
335 assert_eq!(ts("3/4").beat_count(), 3);
336 assert_eq!(ts("3/8").beat_count(), 1);
337 assert_eq!(ts("3/16").beat_count(), 1);
338 assert_eq!(ts("3/32").beat_count(), 1);
339 for denominator in [2, 4, 8, 16] {
341 assert_eq!(TimeSignature::new(6, denominator).unwrap().beat_count(), 2);
342 assert_eq!(TimeSignature::new(5, denominator).unwrap().beat_count(), 5);
343 }
344 }
345
346 #[test]
347 fn classification_joins_division_and_count() {
348 assert_eq!(ts("4/4").classification(), "Simple Quadruple");
349 assert_eq!(ts("6/8").classification(), "Compound Duple");
350 assert_eq!(ts("3/8").classification(), "Other Single");
351 assert_eq!(ts("5/4").classification(), "Simple Quintuple");
352 assert_eq!(ts("13/8").classification(), "Simple 13-uple");
353 assert_eq!(ts("21/16").classification(), "Compound Septuple");
354 }
355
356 #[test]
357 fn beat_offsets_partition_the_bar() {
358 assert_eq!(ts("4/4").beat_offsets(), [0.0, 1.0, 2.0, 3.0]);
359 assert_eq!(ts("6/8").beat_offsets(), [0.0, 1.5]);
360 assert_eq!(ts("5/8").beat_offsets(), [0.0, 0.5, 1.0, 1.5, 2.0]);
361 }
362
363 #[test]
364 fn beat_at_offset_is_one_based_and_bounded() {
365 assert_eq!(ts("4/4").beat_at_offset(1.5).unwrap(), 2);
366 assert_eq!(ts("6/8").beat_at_offset(1.5).unwrap(), 2);
367 assert_eq!(ts("5/8").beat_at_offset(1.5).unwrap(), 4);
368 assert_eq!(ts("4/4").beat_at_offset(0.0).unwrap(), 1);
369 assert!(ts("4/4").beat_at_offset(4.0).is_err());
370 assert!(ts("4/4").beat_at_offset(-0.5).is_err());
371 assert!(ts("4/4").beat_at_offset(FloatType::NAN).is_err());
372 }
373
374 #[test]
375 fn irrational_denominators_are_accepted_as_music21_accepts_them() {
376 let four_three = ts("4/3");
377 assert!((four_three.bar_quarter_length() - 16.0 / 3.0).abs() < 1e-12);
378 assert_eq!(four_three.beat_count(), 4);
379 }
380
381 #[test]
382 fn malformed_ratios_error_instead_of_panicking() {
383 for ratio in [
384 "", "4", "4/", "/4", "4/4/4", "x/4", "4/x", "0/4", "4/0", "-1/4",
385 ] {
386 assert!(
387 TimeSignature::from_ratio_string(ratio).is_err(),
388 "{ratio:?} should not parse"
389 );
390 }
391 }
392
393 #[test]
394 fn display_is_the_ratio_string() {
395 assert_eq!(ts("7/8").to_string(), "7/8");
396 }
397}