1use crate::{Error, Pitch, Result};
8
9pub fn abc_note(pitch: &Pitch) -> Result<String> {
14 let name = pitch.name();
15 let mut chars = name.chars();
16 let step = chars
17 .next()
18 .ok_or_else(|| Error::Pitch("cannot write empty pitch name as ABC".to_string()))?;
19 if !matches!(step, 'A'..='G') {
20 return Err(Error::Pitch(format!(
21 "cannot write pitch step {step:?} as ABC"
22 )));
23 }
24
25 let accidental = chars
26 .map(|modifier| match modifier {
27 '#' => Ok('^'),
28 '-' => Ok('_'),
29 _ => Err(Error::Pitch(format!(
30 "cannot write accidental modifier {modifier:?} as ABC"
31 ))),
32 })
33 .collect::<Result<String>>()?;
34 let octave = pitch.octave().unwrap_or(4);
35
36 if octave >= 5 {
37 Ok(format!(
38 "{accidental}{}{}",
39 step.to_ascii_lowercase(),
40 "'".repeat((octave - 5) as usize)
41 ))
42 } else {
43 Ok(format!(
44 "{accidental}{step}{}",
45 ",".repeat((4 - octave).max(0) as usize)
46 ))
47 }
48}
49
50pub fn abc_rest() -> &'static str {
52 "z"
53}
54
55pub fn abc_chord(pitches: &[Pitch]) -> Result<String> {
57 let notes = pitches.iter().map(abc_note).collect::<Result<Vec<_>>>()?;
58 if notes.is_empty() {
59 Ok(abc_rest().to_string())
60 } else {
61 Ok(format!("[{}]", notes.join("")))
62 }
63}
64
65pub fn abc_duration(numerator: u32, denominator: u32) -> Result<String> {
67 if denominator == 0 {
68 return Err(Error::Music21Object(
69 "ABC duration denominator cannot be zero".to_string(),
70 ));
71 }
72
73 let divisor = gcd(numerator, denominator);
74 let top = numerator / divisor;
75 let bottom = denominator / divisor;
76
77 if bottom == 1 {
78 if top == 1 {
79 Ok(String::new())
80 } else {
81 Ok(top.to_string())
82 }
83 } else if top == 1 {
84 Ok(format!("/{bottom}"))
85 } else {
86 Ok(format!("{top}/{bottom}"))
87 }
88}
89
90pub fn pitch_name_from_abc_note(token: &str) -> Result<Option<String>> {
94 let token = token.trim();
95 if token.is_empty() {
96 return Err(Error::Pitch("ABC note token cannot be empty".to_string()));
97 }
98
99 let token = token
100 .strip_prefix('{')
101 .and_then(|inner| inner.strip_suffix('}'))
102 .unwrap_or(token);
103
104 let mut chars = token.char_indices().peekable();
105 let mut accidental = String::new();
106
107 while let Some((_, ch)) = chars.peek().copied() {
108 match ch {
109 '^' => {
110 accidental.push('#');
111 chars.next();
112 }
113 '_' => {
114 accidental.push('-');
115 chars.next();
116 }
117 '=' => {
118 accidental.clear();
119 accidental.push('n');
120 chars.next();
121 }
122 _ => break,
123 }
124 }
125
126 let Some((_, step)) = chars.next() else {
127 return Err(Error::Pitch(format!(
128 "ABC note token {token:?} is missing a pitch step"
129 )));
130 };
131
132 if matches!(step, 'z' | 'Z' | 'x' | 'X') {
133 ensure_duration_suffix(token, chars.map(|(_, ch)| ch))?;
134 return Ok(None);
135 }
136
137 if !matches!(step, 'A'..='G' | 'a'..='g') {
138 return Err(Error::Pitch(format!(
139 "ABC note token {token:?} has invalid pitch step {step:?}"
140 )));
141 }
142
143 let mut octave = if step.is_ascii_lowercase() { 5 } else { 4 };
144 while let Some((_, ch)) = chars.peek().copied() {
145 match ch {
146 '\'' => {
147 octave += 1;
148 chars.next();
149 }
150 ',' => {
151 octave -= 1;
152 chars.next();
153 }
154 _ => break,
155 }
156 }
157
158 ensure_duration_suffix(token, chars.map(|(_, ch)| ch))?;
159 Ok(Some(format!(
160 "{}{accidental}{octave}",
161 step.to_ascii_uppercase()
162 )))
163}
164
165pub fn pitch_names_from_abc_chord(token: &str) -> Result<Vec<String>> {
169 let token = token.trim();
170 let Some(open) = token.find('[') else {
171 return match pitch_name_from_abc_note(token)? {
172 Some(pitch_name) => Ok(vec![pitch_name]),
173 None => Ok(Vec::new()),
174 };
175 };
176 let Some(close_offset) = token[open + 1..].find(']') else {
177 return Err(Error::Pitch(format!(
178 "ABC chord token {token:?} is missing a closing bracket"
179 )));
180 };
181 let close = open + 1 + close_offset;
182 ensure_duration_suffix(token, token[close + 1..].chars())?;
183
184 let mut names = Vec::new();
185 let inner = &token[open + 1..close];
186 let mut start = 0;
187 while start < inner.len() {
188 let end = next_abc_note_end(inner, start)?;
189 if let Some(name) = pitch_name_from_abc_note(&inner[start..end])? {
190 names.push(name);
191 }
192 start = end;
193 }
194
195 Ok(names)
196}
197
198fn next_abc_note_end(value: &str, start: usize) -> Result<usize> {
199 let mut end = start;
200 let mut saw_step = false;
201
202 for (offset, ch) in value[start..].char_indices() {
203 let idx = start + offset;
204 if !saw_step {
205 end = idx + ch.len_utf8();
206 match ch {
207 '^' | '_' | '=' => {}
208 'A'..='G' | 'a'..='g' | 'z' | 'Z' | 'x' | 'X' => saw_step = true,
209 _ => {
210 return Err(Error::Pitch(format!(
211 "ABC chord token has invalid note character {ch:?}"
212 )));
213 }
214 }
215 continue;
216 }
217
218 if matches!(ch, '\'' | ',' | '/' | '0'..='9') {
219 end = idx + ch.len_utf8();
220 } else {
221 break;
222 }
223 }
224
225 if saw_step {
226 Ok(end)
227 } else {
228 Err(Error::Pitch(
229 "ABC chord token is missing a pitch step".to_string(),
230 ))
231 }
232}
233
234fn ensure_duration_suffix(token: &str, suffix: impl IntoIterator<Item = char>) -> Result<()> {
235 let invalid = suffix
236 .into_iter()
237 .find(|ch| !ch.is_ascii_digit() && *ch != '/');
238
239 if let Some(ch) = invalid {
240 Err(Error::Pitch(format!(
241 "ABC token {token:?} has unsupported suffix character {ch:?}"
242 )))
243 } else {
244 Ok(())
245 }
246}
247
248fn gcd(mut left: u32, mut right: u32) -> u32 {
249 while right != 0 {
250 (left, right) = (right, left % right);
251 }
252 left
253}
254
255#[cfg(test)]
256mod tests {
257 use super::{
258 abc_chord, abc_duration, abc_note, abc_rest, pitch_name_from_abc_note,
259 pitch_names_from_abc_chord,
260 };
261 use crate::{Pitch, Result};
262
263 fn pitches(names: &[&str]) -> Result<Vec<Pitch>> {
264 names.iter().map(|name| Pitch::from_name(*name)).collect()
265 }
266
267 #[test]
268 fn writes_pitch_tokens_with_accidentals_and_octaves() -> Result<()> {
269 assert_eq!(abc_note(&Pitch::from_name("C4")?)?, "C");
270 assert_eq!(abc_note(&Pitch::from_name("C#4")?)?, "^C");
271 assert_eq!(abc_note(&Pitch::from_name("E-4")?)?, "_E");
272 assert_eq!(abc_note(&Pitch::from_name("C5")?)?, "c");
273 assert_eq!(abc_note(&Pitch::from_name("B3")?)?, "B,");
274 Ok(())
275 }
276
277 #[test]
278 fn writes_chord_and_rest_tokens_without_score_layout() -> Result<()> {
279 assert_eq!(abc_rest(), "z");
280 assert_eq!(abc_chord(&pitches(&["C4", "E4", "G4"])?)?, "[CEG]");
281 assert_eq!(abc_chord(&[])?, "z");
282 Ok(())
283 }
284
285 #[test]
286 fn writes_duration_suffixes() -> Result<()> {
287 assert_eq!(abc_duration(1, 1)?, "");
288 assert_eq!(abc_duration(4, 1)?, "4");
289 assert_eq!(abc_duration(1, 4)?, "/4");
290 assert_eq!(abc_duration(4, 10)?, "2/5");
291 assert!(abc_duration(1, 0).is_err());
292 Ok(())
293 }
294
295 #[test]
296 fn reads_pitch_names_from_note_tokens() -> Result<()> {
297 assert_eq!(pitch_name_from_abc_note("C")?.as_deref(), Some("C4"));
298 assert_eq!(pitch_name_from_abc_note("c")?.as_deref(), Some("C5"));
299 assert_eq!(pitch_name_from_abc_note("B,,")?.as_deref(), Some("B2"));
300 assert_eq!(pitch_name_from_abc_note("c''")?.as_deref(), Some("C7"));
301 assert_eq!(pitch_name_from_abc_note("^g2")?.as_deref(), Some("G#5"));
302 assert_eq!(pitch_name_from_abc_note("_g''")?.as_deref(), Some("G-7"));
303 assert_eq!(pitch_name_from_abc_note("=c")?.as_deref(), Some("Cn5"));
304 assert_eq!(pitch_name_from_abc_note("z4")?, None);
305 Ok(())
306 }
307
308 #[test]
309 fn reads_pitch_names_from_chord_tokens() -> Result<()> {
310 assert_eq!(
311 pitch_names_from_abc_chord("[CEG]4")?,
312 vec!["C4", "E4", "G4"]
313 );
314 assert_eq!(
315 pitch_names_from_abc_chord("[^C_Eg']2")?,
316 vec!["C#4", "E-4", "G6"]
317 );
318 Ok(())
319 }
320}