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