redact_composer_musical/scale/degree.rs
1use std::ops::{Add, Sub};
2
3#[cfg(feature = "serde")]
4use serde::{Deserialize, Serialize};
5
6#[cfg(feature = "redact-composer")]
7use redact_composer_core::derive::Element;
8
9/// Scale degree, based on a 7-note scale.
10#[derive(Debug, Copy, Clone, Eq, PartialEq)]
11#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
12#[cfg_attr(feature = "redact-composer", derive(Element))]
13#[allow(missing_docs)]
14pub enum Degree {
15 I,
16 II,
17 III,
18 IV,
19 V,
20 VI,
21 VII,
22}
23
24impl Degree {
25 const VALUES: [Degree; 7] = [
26 Degree::I,
27 Degree::II,
28 Degree::III,
29 Degree::IV,
30 Degree::V,
31 Degree::VI,
32 Degree::VII,
33 ];
34
35 /// All [`Degree`] variants.
36 /// ```
37 /// # use redact_composer_musical::{Degree, Degree::*};
38 /// assert_eq!(Degree::values(), [I, II, III, IV, V, VI, VII]);
39 /// ```
40 pub fn values() -> [Degree; 7] {
41 Self::VALUES
42 }
43
44 /// Returns the next [`Degree`] following this one.
45 /// ```
46 /// # use redact_composer_musical::Degree;
47 /// assert_eq!(Degree::I.next(), Degree::II);
48 /// assert_eq!(Degree::VII.next(), Degree::I)
49 /// ```
50 pub fn next(&self) -> Degree {
51 match self {
52 Degree::I => Degree::II,
53 Degree::II => Degree::III,
54 Degree::III => Degree::IV,
55 Degree::IV => Degree::V,
56 Degree::V => Degree::VI,
57 Degree::VI => Degree::VII,
58 Degree::VII => Degree::I,
59 }
60 }
61
62 /// Returns the [`Degree`] previous to this one.
63 /// ```
64 /// # use redact_composer_musical::Degree;
65 /// assert_eq!(Degree::I.prev(), Degree::VII);
66 /// assert_eq!(Degree::VII.prev(), Degree::VI);
67 /// ```
68 pub fn prev(&self) -> Degree {
69 match self {
70 Degree::I => Degree::VII,
71 Degree::II => Degree::I,
72 Degree::III => Degree::II,
73 Degree::IV => Degree::III,
74 Degree::V => Degree::IV,
75 Degree::VI => Degree::V,
76 Degree::VII => Degree::VI,
77 }
78 }
79
80 /// Returns the minimum absolute difference from this degree to another.
81 /// ```
82 /// use redact_composer_musical::Degree;
83 /// assert_eq!(Degree::I.diff(&Degree::I), 0);
84 /// assert_eq!(Degree::I.diff(&Degree::II), 1);
85 /// // Even though VI - II = 4, there is a shorter path traversing the cycle boundary
86 /// assert_eq!(Degree::II.diff(&Degree::VI), 3);
87 /// ```
88 pub fn diff(&self, other: &Degree) -> u8 {
89 let (lower, higher) = if *other as u8 >= *self as u8 {
90 (*self as u8, *other as u8)
91 } else {
92 (*other as u8, *self as u8)
93 };
94
95 (higher - lower).min(lower + 7 - higher)
96 }
97}
98
99impl From<Degree> for u8 {
100 /// Returns the 0-based value of this degree.
101 /// ```
102 /// # use redact_composer_musical::Degree;
103 /// assert_eq!(0_u8, Degree::I.into());
104 /// ```
105 fn from(value: Degree) -> Self {
106 value as u8
107 }
108}
109
110impl From<u8> for Degree {
111 /// Produces a [`Degree`] using a 0-based [`u8`] index.
112 /// ```
113 /// # use redact_composer_musical::Degree;
114 /// assert_eq!(Degree::I, Degree::from(0_u8));
115 /// assert_eq!(Degree::I, Degree::from(7_u8));
116 /// ```
117 fn from(value: u8) -> Self {
118 match value % 7 {
119 0 => Degree::I,
120 1 => Degree::II,
121 2 => Degree::III,
122 3 => Degree::IV,
123 4 => Degree::V,
124 5 => Degree::VI,
125 6 => Degree::VII,
126 _ => unreachable!(),
127 }
128 }
129}
130
131impl Add<u8> for Degree {
132 type Output = Degree;
133
134 /// Adds an offset to a scale degree, wrapping after [`Degree::VII`].
135 /// ```
136 /// # use redact_composer_musical::Degree;
137 /// assert_eq!(Degree::I + 1, Degree::II);
138 /// assert_eq!(Degree::I + 7, Degree::I);
139 /// ```
140 fn add(self, rhs: u8) -> Self::Output {
141 Degree::from(self as u8 + rhs)
142 }
143}
144
145impl Sub<u8> for Degree {
146 type Output = Degree;
147
148 /// Subtracts an offset from a scale degree, wrapping at [`Degree::I`].
149 /// ```
150 /// # use redact_composer_musical::Degree;
151 /// assert_eq!(Degree::II - 1, Degree::I);
152 /// assert_eq!(Degree::I - 1, Degree::VII);
153 /// ```
154 fn sub(self, rhs: u8) -> Self::Output {
155 Degree::from(7_u8 + self as u8 - rhs)
156 }
157}