Skip to main content

music21_rs/pitch/
accidental.rs

1use crate::defaults::{FloatType, IntegerType};
2use crate::display::{DisplayLocation, DisplaySize, DisplayStyle, DisplayType};
3use crate::error::{Error, Result};
4
5use std::fmt::{Display, Formatter};
6use std::str::FromStr;
7
8enum AccidentalEnum {
9    Natural,
10    HalfSharp,
11    Sharp,
12    OneAndAHalfSharp,
13    DoubleSharp,
14    TripleSharp,
15    QuadrupleSharp,
16    HalfFlat,
17    Flat,
18    OneAndAHalfFlat,
19    DoubleFlat,
20    TripleFlat,
21    QuadrupleFlat,
22}
23
24impl AccidentalEnum {
25    fn to_name(&self) -> &'static str {
26        match self {
27            AccidentalEnum::Natural => "natural",
28            AccidentalEnum::HalfSharp => "half-sharp",
29            AccidentalEnum::Sharp => "sharp",
30            AccidentalEnum::OneAndAHalfSharp => "one-and-a-half-sharp",
31            AccidentalEnum::DoubleSharp => "double-sharp",
32            AccidentalEnum::TripleSharp => "triple-sharp",
33            AccidentalEnum::QuadrupleSharp => "quadruple-sharp",
34            AccidentalEnum::HalfFlat => "half-flat",
35            AccidentalEnum::Flat => "flat",
36            AccidentalEnum::OneAndAHalfFlat => "one-and-a-half-flat",
37            AccidentalEnum::DoubleFlat => "double-flat",
38            AccidentalEnum::TripleFlat => "triple-flat",
39            AccidentalEnum::QuadrupleFlat => "quadruple-flat",
40        }
41    }
42
43    fn from_name(s: &str) -> Option<Self> {
44        match s {
45            "natural" => Some(AccidentalEnum::Natural),
46            "half-sharp" => Some(AccidentalEnum::HalfSharp),
47            "sharp" => Some(AccidentalEnum::Sharp),
48            "one-and-a-half-sharp" => Some(AccidentalEnum::OneAndAHalfSharp),
49            "double-sharp" => Some(AccidentalEnum::DoubleSharp),
50            "triple-sharp" => Some(AccidentalEnum::TripleSharp),
51            "quadruple-sharp" => Some(AccidentalEnum::QuadrupleSharp),
52            "half-flat" => Some(AccidentalEnum::HalfFlat),
53            "flat" => Some(AccidentalEnum::Flat),
54            "one-and-a-half-flat" => Some(AccidentalEnum::OneAndAHalfFlat),
55            "double-flat" => Some(AccidentalEnum::DoubleFlat),
56            "triple-flat" => Some(AccidentalEnum::TripleFlat),
57            "quadruple-flat" => Some(AccidentalEnum::QuadrupleFlat),
58            _ => None,
59        }
60    }
61
62    fn to_alter(&self) -> FloatType {
63        match self {
64            AccidentalEnum::Natural => 0.0,
65            AccidentalEnum::HalfSharp => 0.5,
66            AccidentalEnum::Sharp => 1.0,
67            AccidentalEnum::OneAndAHalfSharp => 1.5,
68            AccidentalEnum::DoubleSharp => 2.0,
69            AccidentalEnum::TripleSharp => 3.0,
70            AccidentalEnum::QuadrupleSharp => 4.0,
71            AccidentalEnum::HalfFlat => -0.5,
72            AccidentalEnum::Flat => -1.0,
73            AccidentalEnum::OneAndAHalfFlat => -1.5,
74            AccidentalEnum::DoubleFlat => -2.0,
75            AccidentalEnum::TripleFlat => -3.0,
76            AccidentalEnum::QuadrupleFlat => -4.0,
77        }
78    }
79
80    fn from_alter_str(s: &str) -> Option<Self> {
81        match s {
82            "-4.0" => Some(AccidentalEnum::QuadrupleFlat),
83            "-3.0" => Some(AccidentalEnum::TripleFlat),
84            "-2.0" => Some(AccidentalEnum::DoubleFlat),
85            "-1.5" => Some(AccidentalEnum::OneAndAHalfFlat),
86            "-1.0" => Some(AccidentalEnum::Flat),
87            "-0.5" => Some(AccidentalEnum::HalfFlat),
88            "0.0" => Some(AccidentalEnum::Natural),
89            "0.5" => Some(AccidentalEnum::HalfSharp),
90            "1.0" => Some(AccidentalEnum::Sharp),
91            "1.5" => Some(AccidentalEnum::OneAndAHalfSharp),
92            "2.0" => Some(AccidentalEnum::DoubleSharp),
93            "3.0" => Some(AccidentalEnum::TripleSharp),
94            "4.0" => Some(AccidentalEnum::QuadrupleSharp),
95            _ => None,
96        }
97    }
98
99    fn to_modifier(&self) -> &'static str {
100        match self {
101            AccidentalEnum::Natural => "",
102            AccidentalEnum::HalfSharp => "~",
103            AccidentalEnum::Sharp => "#",
104            AccidentalEnum::OneAndAHalfSharp => "#~",
105            AccidentalEnum::DoubleSharp => "##",
106            AccidentalEnum::TripleSharp => "###",
107            AccidentalEnum::QuadrupleSharp => "####",
108            AccidentalEnum::HalfFlat => "`",
109            AccidentalEnum::Flat => "-",
110            AccidentalEnum::OneAndAHalfFlat => "-`",
111            AccidentalEnum::DoubleFlat => "--",
112            AccidentalEnum::TripleFlat => "---",
113            AccidentalEnum::QuadrupleFlat => "----",
114        }
115    }
116
117    fn from_modifier(s: &str) -> Option<Self> {
118        match s {
119            "" => Some(AccidentalEnum::Natural),
120            "~" => Some(AccidentalEnum::HalfSharp),
121            "#" => Some(AccidentalEnum::Sharp),
122            "#~" => Some(AccidentalEnum::OneAndAHalfSharp),
123            "##" => Some(AccidentalEnum::DoubleSharp),
124            "###" => Some(AccidentalEnum::TripleSharp),
125            "####" => Some(AccidentalEnum::QuadrupleSharp),
126            "`" => Some(AccidentalEnum::HalfFlat),
127            "-" => Some(AccidentalEnum::Flat),
128            "-`" => Some(AccidentalEnum::OneAndAHalfFlat),
129            "--" => Some(AccidentalEnum::DoubleFlat),
130            "---" => Some(AccidentalEnum::TripleFlat),
131            "----" => Some(AccidentalEnum::QuadrupleFlat),
132            _ => None,
133        }
134    }
135
136    const fn to_unicode(&self) -> &'static str {
137        match self {
138            AccidentalEnum::QuadrupleSharp => "\u{1d12a}\u{1d12a}",
139            AccidentalEnum::TripleSharp => "\u{266f}\u{1d12a}",
140            AccidentalEnum::DoubleSharp => "\u{1d12a}",
141            AccidentalEnum::OneAndAHalfSharp => "\u{266f}\u{1d132}",
142            AccidentalEnum::Sharp => "\u{266f}",
143            AccidentalEnum::HalfSharp => "\u{1d132}",
144            AccidentalEnum::QuadrupleFlat => "\u{1d12b}\u{1d12b}",
145            AccidentalEnum::TripleFlat => "\u{266d}",
146            AccidentalEnum::DoubleFlat => "\u{1d12b}",
147            AccidentalEnum::OneAndAHalfFlat => "\u{266d}\u{1d132}",
148            AccidentalEnum::Flat => "\u{266d}",
149            AccidentalEnum::HalfFlat => "\u{1d132}",
150            AccidentalEnum::Natural => "\u{266e}",
151        }
152    }
153
154    fn from_unicode(s: &str) -> Option<Self> {
155        match s {
156            "\u{1d12a}\u{1d12a}" => Some(AccidentalEnum::QuadrupleSharp),
157            "\u{266f}\u{1d12a}" => Some(AccidentalEnum::TripleSharp),
158            "\u{1d12a}" => Some(AccidentalEnum::DoubleSharp),
159            "\u{266f}\u{1d132}" => Some(AccidentalEnum::OneAndAHalfSharp),
160            "\u{266f}" => Some(AccidentalEnum::Sharp),
161            "\u{1d12b}\u{1d12b}" => Some(AccidentalEnum::QuadrupleFlat),
162            "\u{266d}" => Some(AccidentalEnum::Flat),
163            "\u{1d12b}" => Some(AccidentalEnum::DoubleFlat),
164            "\u{266d}\u{1d132}" => Some(AccidentalEnum::OneAndAHalfFlat),
165            "\u{1d132}" => Some(AccidentalEnum::HalfSharp),
166            "\u{266e}" => Some(AccidentalEnum::Natural),
167            _ => None,
168        }
169    }
170
171    fn from_alternate_name(s: &str) -> Option<Self> {
172        match s {
173            "n" => Some(AccidentalEnum::Natural),
174            "is" => Some(AccidentalEnum::Sharp),
175            "isis" => Some(AccidentalEnum::DoubleSharp),
176            "isisis" => Some(AccidentalEnum::TripleSharp),
177            "isisisis" => Some(AccidentalEnum::QuadrupleSharp),
178            "ih" | "quarter-sharp" | "semisharp" => Some(AccidentalEnum::HalfSharp),
179            "isih" | "three-quarter-sharp" | "three-quarters-sharp" | "sesquisharp" => {
180                Some(AccidentalEnum::OneAndAHalfSharp)
181            }
182            "b" | "es" => Some(AccidentalEnum::Flat),
183            "eses" => Some(AccidentalEnum::DoubleFlat),
184            "eseses" => Some(AccidentalEnum::TripleFlat),
185            "eseseses" => Some(AccidentalEnum::QuadrupleFlat),
186            "eh" | "quarter-flat" | "semiflat" => Some(AccidentalEnum::HalfFlat),
187            "eseh" | "three-quarter-flat" | "three-quarters-flat" | "sesquiflat" => {
188                Some(AccidentalEnum::OneAndAHalfFlat)
189            }
190            _ => None,
191        }
192    }
193
194    fn from_float(f: FloatType) -> Option<Self> {
195        match f {
196            -4.0 => Some(AccidentalEnum::QuadrupleFlat),
197            -3.0 => Some(AccidentalEnum::TripleFlat),
198            -2.0 => Some(AccidentalEnum::DoubleFlat),
199            -1.5 => Some(AccidentalEnum::OneAndAHalfFlat),
200            -1.0 => Some(AccidentalEnum::Flat),
201            -0.5 => Some(AccidentalEnum::HalfFlat),
202            0.0 => Some(AccidentalEnum::Natural),
203            0.5 => Some(AccidentalEnum::HalfSharp),
204            1.0 => Some(AccidentalEnum::Sharp),
205            1.5 => Some(AccidentalEnum::OneAndAHalfSharp),
206            2.0 => Some(AccidentalEnum::DoubleSharp),
207            3.0 => Some(AccidentalEnum::TripleSharp),
208            4.0 => Some(AccidentalEnum::QuadrupleSharp),
209            _ => None,
210        }
211    }
212
213    fn from_string(s: &str) -> Option<Self> {
214        AccidentalEnum::from_name(s)
215            .or_else(|| AccidentalEnum::from_modifier(s))
216            .or_else(|| AccidentalEnum::from_alter_str(s))
217            .or_else(|| AccidentalEnum::from_unicode(s))
218            .or_else(|| AccidentalEnum::from_alternate_name(s))
219    }
220}
221
222/// Input accepted by [`Accidental::new`] and [`Accidental::set`].
223#[derive(Clone, Debug, PartialEq)]
224#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
225pub enum AccidentalSpecifier {
226    /// An accidental name, modifier, alternate name, or unicode accidental.
227    Name(String),
228    /// A semitone alteration such as `1.0` for sharp or `-0.5` for half-flat.
229    Alter(FloatType),
230    /// An existing accidental to clone.
231    Accidental(Accidental),
232}
233
234impl From<&str> for AccidentalSpecifier {
235    fn from(value: &str) -> Self {
236        Self::Name(value.to_string())
237    }
238}
239
240impl From<String> for AccidentalSpecifier {
241    fn from(value: String) -> Self {
242        Self::Name(value)
243    }
244}
245
246impl From<i8> for AccidentalSpecifier {
247    fn from(value: i8) -> Self {
248        Self::Alter(value as FloatType)
249    }
250}
251
252impl From<IntegerType> for AccidentalSpecifier {
253    fn from(value: IntegerType) -> Self {
254        Self::Alter(value as FloatType)
255    }
256}
257
258impl From<FloatType> for AccidentalSpecifier {
259    fn from(value: FloatType) -> Self {
260        Self::Alter(value)
261    }
262}
263
264impl From<Accidental> for AccidentalSpecifier {
265    fn from(value: Accidental) -> Self {
266        Self::Accidental(value)
267    }
268}
269
270impl Display for AccidentalSpecifier {
271    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
272        match self {
273            Self::Name(name) => write!(f, "{name}"),
274            Self::Alter(alter) => write!(f, "{alter}"),
275            Self::Accidental(accidental) => write!(f, "{accidental}"),
276        }
277    }
278}
279
280#[derive(Clone, Debug)]
281#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
282/// Symbolic and numerical accidental information for a pitch.
283///
284/// This mirrors the main behavior of Python music21's
285/// `music21.pitch.Accidental`: a standard accidental has a `name`, `modifier`,
286/// and semitone `alter`; names compare by spelling while ordering uses `alter`.
287pub struct Accidental {
288    _display_type: DisplayType,
289    _display_status: Option<bool>,
290    display_style: DisplayStyle,
291    display_size: DisplaySize,
292    display_location: DisplayLocation,
293    _name: String,
294    _modifier: String,
295    pub(crate) _alter: FloatType,
296}
297
298impl PartialEq for Accidental {
299    fn eq(&self, other: &Self) -> bool {
300        self._name == other._name
301    }
302}
303
304impl PartialOrd for Accidental {
305    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
306        self._alter.partial_cmp(&other._alter)
307    }
308}
309
310impl FromStr for Accidental {
311    type Err = Error;
312
313    fn from_str(value: &str) -> Result<Self> {
314        Self::new(value)
315    }
316}
317
318impl TryFrom<&str> for Accidental {
319    type Error = Error;
320
321    fn try_from(value: &str) -> Result<Self> {
322        Self::new(value)
323    }
324}
325
326impl TryFrom<String> for Accidental {
327    type Error = Error;
328
329    fn try_from(value: String) -> Result<Self> {
330        Self::new(value)
331    }
332}
333
334impl TryFrom<IntegerType> for Accidental {
335    type Error = Error;
336
337    fn try_from(value: IntegerType) -> Result<Self> {
338        Self::new(value)
339    }
340}
341
342impl TryFrom<i8> for Accidental {
343    type Error = Error;
344
345    fn try_from(value: i8) -> Result<Self> {
346        Self::new(value)
347    }
348}
349
350impl TryFrom<FloatType> for Accidental {
351    type Error = Error;
352
353    fn try_from(value: FloatType) -> Result<Self> {
354        Self::new(value)
355    }
356}
357
358impl Accidental {
359    /// Creates an accidental from a name, modifier, alternate name, unicode
360    /// symbol, semitone alteration, or another accidental.
361    pub fn new(specifier: impl Into<AccidentalSpecifier>) -> Result<Self> {
362        let specifier = specifier.into();
363        if let AccidentalSpecifier::Accidental(accidental) = specifier {
364            return Ok(accidental);
365        }
366
367        let mut acci = Self {
368            _display_type: DisplayType::Normal,
369            _display_status: None,
370            display_style: DisplayStyle::Normal,
371            display_size: DisplaySize::Full,
372            display_location: DisplayLocation::Normal,
373            _name: "".to_string(),
374            _modifier: "".to_string(),
375            _alter: 0.0,
376        };
377
378        acci.set_specifier(specifier, false)?;
379        Ok(acci)
380    }
381
382    /// Returns the standard accidental names known to music21.
383    pub fn list_names() -> Vec<&'static str> {
384        let mut names = [
385            "double-flat",
386            "double-sharp",
387            "flat",
388            "half-flat",
389            "half-sharp",
390            "natural",
391            "one-and-a-half-flat",
392            "one-and-a-half-sharp",
393            "quadruple-flat",
394            "quadruple-sharp",
395            "sharp",
396            "triple-flat",
397            "triple-sharp",
398        ];
399        names.sort_unstable();
400        names.to_vec()
401    }
402
403    /// Returns whether `name` is a supported accidental name, modifier, or
404    /// alternate music21/LilyPond-style accidental name.
405    pub fn is_valid_name(name: &str) -> bool {
406        AccidentalEnum::from_string(&name.to_lowercase()).is_some()
407    }
408
409    /// Converts a supported accidental spelling to its standard music21 name.
410    pub fn standardize_name(name: &str) -> Result<String> {
411        AccidentalEnum::from_string(&name.to_lowercase())
412            .map(|accidental| accidental.to_name().to_string())
413            .ok_or_else(|| {
414                Error::Accidental(format!("{name:?} is not a supported accidental type"))
415            })
416    }
417
418    /// Changes this accidental to a supported accidental.
419    pub fn set(&mut self, specifier: impl Into<AccidentalSpecifier>) -> Result<()> {
420        self.set_specifier(specifier.into(), false)
421    }
422
423    /// Changes this accidental, preserving non-standard names or alteration
424    /// values in the same way Python music21's `allowNonStandardValue=True`
425    /// does.
426    pub fn set_allowing_non_standard_value(
427        &mut self,
428        specifier: impl Into<AccidentalSpecifier>,
429    ) -> Result<()> {
430        self.set_specifier(specifier.into(), true)
431    }
432
433    fn set_specifier(
434        &mut self,
435        specifier: AccidentalSpecifier,
436        allow_non_standard_value: bool,
437    ) -> Result<()> {
438        if let AccidentalSpecifier::Accidental(accidental) = specifier {
439            *self = accidental;
440            return Ok(());
441        }
442
443        if let Some(accidental) = Self::specifier_to_standard(&specifier) {
444            self._name = accidental.to_name().to_string();
445            self._alter = accidental.to_alter();
446            self._modifier = accidental.to_modifier().to_string();
447            return Ok(());
448        }
449
450        if !allow_non_standard_value {
451            return Err(Error::Accidental(format!(
452                "{specifier} is not a supported accidental type"
453            )));
454        }
455
456        match specifier {
457            AccidentalSpecifier::Name(name) => self._name = name.to_lowercase(),
458            AccidentalSpecifier::Alter(alter) => self._alter = alter,
459            AccidentalSpecifier::Accidental(_) => unreachable!(),
460        }
461        Ok(())
462    }
463
464    fn specifier_to_standard(specifier: &AccidentalSpecifier) -> Option<AccidentalEnum> {
465        match specifier {
466            AccidentalSpecifier::Name(name) => AccidentalEnum::from_string(&name.to_lowercase()),
467            AccidentalSpecifier::Alter(alter) => AccidentalEnum::from_float(*alter),
468            AccidentalSpecifier::Accidental(accidental) => {
469                AccidentalEnum::from_name(accidental.name())
470            }
471        }
472    }
473
474    /// Returns the standard or non-standard accidental name.
475    pub fn name(&self) -> &str {
476        &self._name
477    }
478
479    /// Sets the accidental name. Standard names update `alter` and `modifier`;
480    /// non-standard names are preserved without changing them.
481    pub fn set_name(&mut self, name: impl Into<String>) -> Result<()> {
482        self.set_allowing_non_standard_value(name.into())
483    }
484
485    /// Returns the semitone alteration from the natural step.
486    pub fn alter(&self) -> FloatType {
487        self._alter
488    }
489
490    /// Sets the semitone alteration. Standard values update `name` and
491    /// `modifier`; non-standard values are preserved without changing them.
492    pub fn set_alter(&mut self, alter: FloatType) -> Result<()> {
493        self.set_allowing_non_standard_value(alter)
494    }
495
496    /// Returns the music21 modifier string, such as `#`, `-`, `##`, or `~`.
497    pub fn modifier(&self) -> &str {
498        &self._modifier
499    }
500
501    /// Sets the modifier. Unknown modifiers are preserved without changing the
502    /// name or alteration, matching Python music21.
503    pub fn set_modifier(&mut self, modifier: impl Into<String>) {
504        let modifier = modifier.into();
505        if let Some(accidental) = AccidentalEnum::from_modifier(&modifier) {
506            self._name = accidental.to_name().to_string();
507            self._alter = accidental.to_alter();
508            self._modifier = accidental.to_modifier().to_string();
509        } else {
510            self._modifier = modifier;
511        }
512    }
513
514    /// Returns a unicode representation of the accidental.
515    pub fn unicode(&self) -> String {
516        AccidentalEnum::from_modifier(&self._modifier)
517            .map(|accidental| accidental.to_unicode().to_string())
518            .unwrap_or_else(|| self._modifier.clone())
519    }
520
521    /// Returns the most complete accidental name. This is currently the same as
522    /// [`Self::name`], like Python music21.
523    pub fn full_name(&self) -> &str {
524        self.name()
525    }
526
527    /// Returns whether this accidental describes a twelve-tone alteration.
528    pub fn is_twelve_tone(&self) -> bool {
529        !matches!(
530            self._name.as_str(),
531            "half-sharp" | "one-and-a-half-sharp" | "half-flat" | "one-and-a-half-flat"
532        )
533    }
534
535    /// Sets `name` without updating `alter` or `modifier`.
536    pub fn set_name_independently(&mut self, name: impl Into<String>) {
537        self._name = name.into();
538    }
539
540    /// Sets `alter` without updating `name` or `modifier`.
541    pub fn set_alter_independently(&mut self, alter: FloatType) {
542        self._alter = alter;
543    }
544
545    /// Sets `modifier` without updating `name` or `alter`.
546    pub fn set_modifier_independently(&mut self, modifier: impl Into<String>) {
547        self._modifier = modifier.into();
548    }
549
550    /// Copies display-related settings from another accidental.
551    pub fn inherit_display(&mut self, other: &Accidental) {
552        self._display_type = other._display_type.clone();
553        self._display_status = other._display_status;
554        self.display_style = other.display_style.clone();
555        self.display_size = other.display_size.clone();
556        self.display_location = other.display_location.clone();
557    }
558
559    /// Returns the accidental display type.
560    pub fn display_type(&self) -> &'static str {
561        display_type_to_str(&self._display_type)
562    }
563
564    /// Sets the accidental display type.
565    pub fn set_display_type(&mut self, value: &str) -> Result<()> {
566        self._display_type = display_type_from_str(value).ok_or_else(|| {
567            Error::Accidental(format!("Supplied display type is not supported: {value:?}"))
568        })?;
569        Ok(())
570    }
571
572    /// Returns whether notation processing has decided to display this
573    /// accidental. `None` means no decision has been made.
574    pub fn display_status(&self) -> Option<bool> {
575        self._display_status
576    }
577
578    /// Sets the display status.
579    pub fn set_display_status(&mut self, value: Option<bool>) {
580        self._display_status = value;
581    }
582
583    /// Returns the display style.
584    pub fn display_style(&self) -> &'static str {
585        display_style_to_str(&self.display_style)
586    }
587
588    /// Sets the display style.
589    pub fn set_display_style(&mut self, value: &str) -> Result<()> {
590        self.display_style = display_style_from_str(value).ok_or_else(|| {
591            Error::Accidental(format!(
592                "Supplied display style is not supported: {value:?}"
593            ))
594        })?;
595        Ok(())
596    }
597
598    /// Returns the display size.
599    pub fn display_size(&self) -> String {
600        display_size_to_string(&self.display_size)
601    }
602
603    /// Sets the display size.
604    pub fn set_display_size(&mut self, value: &str) -> Result<()> {
605        self.display_size = display_size_from_str(value)?;
606        Ok(())
607    }
608
609    /// Returns the display location.
610    pub fn display_location(&self) -> &'static str {
611        display_location_to_str(&self.display_location)
612    }
613
614    /// Sets the display location.
615    pub fn set_display_location(&mut self, value: &str) -> Result<()> {
616        self.display_location = display_location_from_str(value).ok_or_else(|| {
617            Error::Accidental(format!(
618                "Supplied display location is not supported: {value:?}"
619            ))
620        })?;
621        Ok(())
622    }
623
624    /// Returns a natural accidental.
625    pub fn natural() -> Accidental {
626        let x = Accidental::new("natural");
627        assert!(x.is_ok());
628        match x {
629            Ok(val) => val,
630            Err(err) => panic!("creating a natural Accidental should never fail: {err}"),
631        }
632    }
633
634    /// Returns a flat accidental.
635    pub fn flat() -> Accidental {
636        let x = Accidental::new("flat");
637        assert!(x.is_ok());
638        match x {
639            Ok(val) => val,
640            Err(err) => panic!("creating a flat Accidental should never fail: {err}"),
641        }
642    }
643
644    /// Returns a sharp accidental.
645    pub fn sharp() -> Accidental {
646        let x = Accidental::new("sharp");
647        assert!(x.is_ok());
648        match x {
649            Ok(val) => val,
650            Err(err) => panic!("creating a sharp Accidental should never fail: {err}"),
651        }
652    }
653}
654
655fn display_type_to_str(value: &DisplayType) -> &'static str {
656    match value {
657        DisplayType::Normal => "normal",
658        DisplayType::Always => "always",
659        DisplayType::Never => "never",
660        DisplayType::UnlessRepeated => "unless-repeated",
661        DisplayType::EvenTied => "even-tied",
662        DisplayType::IfAbsolutelyNecessary => "if-absolutely-necessary",
663    }
664}
665
666fn display_type_from_str(value: &str) -> Option<DisplayType> {
667    match value {
668        "normal" => Some(DisplayType::Normal),
669        "always" => Some(DisplayType::Always),
670        "never" => Some(DisplayType::Never),
671        "unless-repeated" => Some(DisplayType::UnlessRepeated),
672        "even-tied" => Some(DisplayType::EvenTied),
673        "if-absolutely-necessary" => Some(DisplayType::IfAbsolutelyNecessary),
674        _ => None,
675    }
676}
677
678fn display_style_to_str(value: &DisplayStyle) -> &'static str {
679    match value {
680        DisplayStyle::Normal => "normal",
681        DisplayStyle::Parentheses => "parentheses",
682        DisplayStyle::Bracket => "bracket",
683        DisplayStyle::Both => "both",
684    }
685}
686
687fn display_style_from_str(value: &str) -> Option<DisplayStyle> {
688    match value {
689        "normal" => Some(DisplayStyle::Normal),
690        "parentheses" => Some(DisplayStyle::Parentheses),
691        "bracket" => Some(DisplayStyle::Bracket),
692        "both" => Some(DisplayStyle::Both),
693        _ => None,
694    }
695}
696
697fn display_size_to_string(value: &DisplaySize) -> String {
698    match value {
699        DisplaySize::Full => "full".to_string(),
700        DisplaySize::Cue => "cue".to_string(),
701        DisplaySize::Large => "large".to_string(),
702        DisplaySize::Percentage(percentage) => percentage.to_string(),
703    }
704}
705
706fn display_size_from_str(value: &str) -> Result<DisplaySize> {
707    match value {
708        "full" => Ok(DisplaySize::Full),
709        "cue" => Ok(DisplaySize::Cue),
710        "large" => Ok(DisplaySize::Large),
711        _ => value
712            .parse::<FloatType>()
713            .map(DisplaySize::Percentage)
714            .map_err(|_| {
715                Error::Accidental(format!("Supplied display size is not supported: {value:?}"))
716            }),
717    }
718}
719
720fn display_location_to_str(value: &DisplayLocation) -> &'static str {
721    match value {
722        DisplayLocation::Normal => "normal",
723        DisplayLocation::Above => "above",
724        DisplayLocation::Ficta => "ficta",
725        DisplayLocation::Below => "below",
726    }
727}
728
729fn display_location_from_str(value: &str) -> Option<DisplayLocation> {
730    match value {
731        "normal" => Some(DisplayLocation::Normal),
732        "above" => Some(DisplayLocation::Above),
733        "ficta" => Some(DisplayLocation::Ficta),
734        "below" => Some(DisplayLocation::Below),
735        _ => None,
736    }
737}
738
739impl Default for Accidental {
740    fn default() -> Self {
741        Self::natural()
742    }
743}
744
745impl std::fmt::Display for Accidental {
746    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
747        write!(f, "{}", self._name)
748    }
749}
750
751pub(crate) trait IntoAccidental: Display + Clone {
752    fn is_accidental(&self) -> bool;
753    fn into_accidental(self) -> Result<Accidental>;
754    fn accidental(self) -> Accidental;
755}
756
757impl IntoAccidental for i8 {
758    fn is_accidental(&self) -> bool {
759        false
760    }
761
762    fn into_accidental(self) -> Result<Accidental> {
763        Accidental::new(self)
764    }
765
766    fn accidental(self) -> Accidental {
767        panic!("call into_accidental instead")
768    }
769}
770
771impl IntoAccidental for IntegerType {
772    fn is_accidental(&self) -> bool {
773        false
774    }
775
776    fn into_accidental(self) -> Result<Accidental> {
777        Accidental::new(self)
778    }
779
780    fn accidental(self) -> Accidental {
781        panic!("call into_accidental instead")
782    }
783}
784
785impl IntoAccidental for FloatType {
786    fn is_accidental(&self) -> bool {
787        false
788    }
789
790    fn into_accidental(self) -> Result<Accidental> {
791        Accidental::new(self)
792    }
793
794    fn accidental(self) -> Accidental {
795        panic!("call into_accidental instead")
796    }
797}
798
799impl IntoAccidental for &str {
800    fn is_accidental(&self) -> bool {
801        false
802    }
803
804    fn into_accidental(self) -> Result<Accidental> {
805        Accidental::new(self)
806    }
807
808    fn accidental(self) -> Accidental {
809        panic!("call into_accidental instead")
810    }
811}
812
813impl IntoAccidental for String {
814    fn is_accidental(&self) -> bool {
815        false
816    }
817
818    fn into_accidental(self) -> Result<Accidental> {
819        Accidental::new(self)
820    }
821
822    fn accidental(self) -> Accidental {
823        panic!("call into_accidental instead")
824    }
825}
826
827impl IntoAccidental for Accidental {
828    fn is_accidental(&self) -> bool {
829        true
830    }
831
832    fn into_accidental(self) -> Result<Accidental> {
833        panic!("don't call into_accidental on an accidental");
834    }
835
836    fn accidental(self) -> Accidental {
837        self
838    }
839}
840
841impl IntoAccidental for AccidentalSpecifier {
842    fn is_accidental(&self) -> bool {
843        matches!(self, AccidentalSpecifier::Accidental(_))
844    }
845
846    fn into_accidental(self) -> Result<Accidental> {
847        Accidental::new(self)
848    }
849
850    fn accidental(self) -> Accidental {
851        match self {
852            AccidentalSpecifier::Accidental(accidental) => accidental,
853            _ => panic!("call into_accidental instead"),
854        }
855    }
856}
857
858#[cfg(test)]
859mod tests {
860    use super::{Accidental, AccidentalSpecifier, IntoAccidental};
861
862    #[test]
863    fn test_natural() {
864        let acc = Accidental::natural();
865        assert_eq!(acc._name, "natural");
866        assert_eq!(acc._alter, 0.0);
867        assert_eq!(acc.modifier(), "");
868    }
869
870    #[test]
871    fn test_sharp() {
872        let acc = Accidental::sharp();
873        assert_eq!(acc._name, "sharp");
874        assert_eq!(acc._alter, 1.0);
875        assert_eq!(acc.modifier(), "#");
876    }
877
878    #[test]
879    fn test_flat() {
880        let acc = Accidental::flat();
881        assert_eq!(acc._name, "flat");
882        assert_eq!(acc._alter, -1.0);
883        assert_eq!(acc.modifier(), "-");
884    }
885
886    #[test]
887    fn test_creation_from_int() {
888        let acc_sharp = 1.into_accidental().unwrap();
889        assert_eq!(acc_sharp._name, "sharp");
890        assert_eq!(acc_sharp._alter, 1.0);
891
892        let acc_flat = (-1).into_accidental().unwrap();
893        assert_eq!(acc_flat._name, "flat");
894        assert_eq!(acc_flat._alter, -1.0);
895
896        let acc_natural = 0.into_accidental().unwrap();
897        assert_eq!(acc_natural._name, "natural");
898        assert_eq!(acc_natural._alter, 0.0);
899    }
900
901    #[test]
902    fn accidental_supports_rust_conversion_traits() {
903        let parsed: Accidental = "#".parse().unwrap();
904        assert_eq!(parsed.name(), "sharp");
905
906        let from_str = Accidental::try_from("flat").unwrap();
907        assert_eq!(from_str.modifier(), "-");
908
909        let from_alter = Accidental::try_from(-0.5).unwrap();
910        assert_eq!(from_alter.name(), "half-flat");
911    }
912
913    #[test]
914    fn test_creation_from_float() {
915        let acc_double_sharp: Accidental = 2.0.into_accidental().unwrap();
916        assert_eq!(acc_double_sharp._name, "double-sharp");
917        assert_eq!(acc_double_sharp._alter, 2.0);
918
919        let acc_half_flat: Accidental = (-0.5).into_accidental().unwrap();
920        assert_eq!(acc_half_flat._name, "half-flat");
921        assert_eq!(acc_half_flat._alter, -0.5);
922    }
923
924    #[test]
925    fn test_creation_from_str() {
926        let acc1: Accidental = <&str>::into_accidental("sharp").unwrap();
927        assert_eq!(acc1._name, "sharp");
928        assert_eq!(acc1._alter, 1.0);
929
930        // Case insensitivity: "Flat" should be accepted as "flat"
931        let acc2: Accidental = <&str>::into_accidental("Flat").unwrap();
932        assert_eq!(acc2._name, "flat");
933        assert_eq!(acc2._alter, -1.0);
934    }
935
936    #[test]
937    fn test_creation_from_string() {
938        let acc: Accidental = String::into_accidental("double-flat".to_string()).unwrap();
939        assert_eq!(acc._name, "double-flat");
940        assert_eq!(acc._alter, -2.0);
941    }
942
943    #[test]
944    fn test_invalid_accidental() {
945        let result = Accidental::new("invalid");
946        assert!(
947            result.is_err(),
948            "An invalid accidental should return an error"
949        );
950    }
951
952    #[test]
953    fn test_display_trait() {
954        let acc = Accidental::sharp();
955        assert_eq!(format!("{acc}"), "sharp");
956    }
957
958    #[test]
959    fn test_equality() {
960        let acc1 = Accidental::sharp();
961        let acc2 = Accidental::sharp();
962        let acc3 = Accidental::flat();
963        assert_eq!(acc1, acc2);
964        assert_ne!(acc1, acc3);
965    }
966
967    #[test]
968    fn test_alternate_names() {
969        // Using alternate names from AccidentalEnum::from_alternate_name
970        let acc_sharp: Accidental = String::into_accidental("is".to_string()).unwrap();
971        assert_eq!(acc_sharp._name, "sharp");
972        assert_eq!(acc_sharp._alter, 1.0);
973
974        let acc_double_sharp: Accidental = String::into_accidental("isis".to_string()).unwrap();
975        assert_eq!(acc_double_sharp._name, "double-sharp");
976        assert_eq!(acc_double_sharp._alter, 2.0);
977
978        let acc_triple_sharp: Accidental = String::into_accidental("isisis".to_string()).unwrap();
979        assert_eq!(acc_triple_sharp._name, "triple-sharp");
980        assert_eq!(acc_triple_sharp._alter, 3.0);
981
982        let acc_double_flat: Accidental = String::into_accidental("eses".to_string()).unwrap();
983        assert_eq!(acc_double_flat._name, "double-flat");
984        assert_eq!(acc_double_flat._alter, -2.0);
985    }
986
987    #[test]
988    fn public_api_matches_music21_accidental_basics() {
989        let mut accidental = Accidental::new("sharp").unwrap();
990        assert_eq!(accidental.name(), "sharp");
991        assert_eq!(accidental.alter(), 1.0);
992        assert_eq!(accidental.modifier(), "#");
993        assert_eq!(accidental.unicode(), "\u{266f}");
994        assert_eq!(accidental.full_name(), "sharp");
995        assert!(accidental.is_twelve_tone());
996
997        accidental.set("--").unwrap();
998        assert_eq!(accidental.name(), "double-flat");
999        assert_eq!(accidental.alter(), -2.0);
1000        assert_eq!(accidental.modifier(), "--");
1001
1002        accidental.set("quarter-sharp").unwrap();
1003        assert_eq!(accidental.name(), "half-sharp");
1004        assert_eq!(accidental.alter(), 0.5);
1005        assert!(!accidental.is_twelve_tone());
1006    }
1007
1008    #[test]
1009    fn public_api_preserves_non_standard_values_when_requested() {
1010        let mut accidental = Accidental::new("flat").unwrap();
1011        accidental
1012            .set_allowing_non_standard_value("flat-flat-up")
1013            .unwrap();
1014        assert_eq!(accidental.name(), "flat-flat-up");
1015        assert_eq!(accidental.alter(), -1.0);
1016        assert_eq!(accidental.modifier(), "-");
1017
1018        accidental.set_alter(-0.9).unwrap();
1019        assert_eq!(accidental.name(), "flat-flat-up");
1020        assert_eq!(accidental.alter(), -0.9);
1021        assert_eq!(accidental.modifier(), "-");
1022
1023        accidental.set_modifier("&");
1024        assert_eq!(accidental.name(), "flat-flat-up");
1025        assert_eq!(accidental.alter(), -0.9);
1026        assert_eq!(accidental.modifier(), "&");
1027    }
1028
1029    #[test]
1030    fn public_api_supports_display_metadata() {
1031        let mut source = Accidental::new("double-flat").unwrap();
1032        source.set_display_type("always").unwrap();
1033        source.set_display_status(Some(true));
1034        source.set_display_style("parentheses").unwrap();
1035        source.set_display_size("cue").unwrap();
1036        source.set_display_location("above").unwrap();
1037
1038        let mut target = Accidental::new("sharp").unwrap();
1039        target.inherit_display(&source);
1040        assert_eq!(target.display_type(), "always");
1041        assert_eq!(target.display_status(), Some(true));
1042        assert_eq!(target.display_style(), "parentheses");
1043        assert_eq!(target.display_size(), "cue");
1044        assert_eq!(target.display_location(), "above");
1045    }
1046
1047    #[test]
1048    fn public_api_covers_name_helpers_and_standardization_errors() {
1049        let names = Accidental::list_names();
1050        assert_eq!(names.first(), Some(&"double-flat"));
1051        assert!(names.contains(&"quadruple-sharp"));
1052
1053        assert!(Accidental::is_valid_name("quarter-sharp"));
1054        assert!(Accidental::is_valid_name("es"));
1055        assert!(!Accidental::is_valid_name("not-an-accidental"));
1056
1057        assert_eq!(Accidental::standardize_name("##").unwrap(), "double-sharp");
1058        let err = Accidental::standardize_name("not-an-accidental").unwrap_err();
1059        assert!(err.to_string().contains("not a supported accidental type"));
1060    }
1061
1062    #[test]
1063    fn public_api_covers_setter_branches_and_independent_updates() {
1064        let mut accidental = Accidental::default();
1065        assert_eq!(accidental.name(), "natural");
1066
1067        accidental.set(Accidental::flat()).unwrap();
1068        assert_eq!(accidental.name(), "flat");
1069        assert_eq!(accidental.unicode(), "\u{266d}");
1070
1071        accidental.set_name("sharp").unwrap();
1072        assert_eq!(accidental.alter(), 1.0);
1073        assert_eq!(accidental.modifier(), "#");
1074
1075        accidental.set_modifier("~~");
1076        assert_eq!(accidental.name(), "sharp");
1077        assert_eq!(accidental.modifier(), "~~");
1078        assert_eq!(accidental.unicode(), "~~");
1079
1080        accidental.set_name_independently("custom");
1081        accidental.set_alter_independently(0.25);
1082        accidental.set_modifier_independently("^");
1083        assert_eq!(accidental.full_name(), "custom");
1084        assert_eq!(accidental.alter(), 0.25);
1085        assert_eq!(accidental.modifier(), "^");
1086
1087        let cloned = Accidental::new(AccidentalSpecifier::from(accidental.clone())).unwrap();
1088        assert_eq!(cloned, accidental);
1089    }
1090
1091    #[test]
1092    fn public_api_rejects_invalid_display_metadata() {
1093        let mut accidental = Accidental::new("natural").unwrap();
1094        assert!(accidental.set_display_type("sometimes").is_err());
1095        assert!(accidental.set_display_style("curly").is_err());
1096        assert!(accidental.set_display_size("huge").is_err());
1097        assert!(accidental.set_display_location("beside").is_err());
1098
1099        accidental.set_display_type("never").unwrap();
1100        accidental.set_display_style("both").unwrap();
1101        accidental.set_display_size("125.5").unwrap();
1102        accidental.set_display_location("below").unwrap();
1103
1104        assert_eq!(accidental.display_type(), "never");
1105        assert_eq!(accidental.display_style(), "both");
1106        assert_eq!(accidental.display_size(), "125.5");
1107        assert_eq!(accidental.display_location(), "below");
1108    }
1109
1110    #[test]
1111    fn accidental_ordering_follows_alter() {
1112        assert!(Accidental::flat() < Accidental::natural());
1113        assert!(Accidental::sharp() > Accidental::natural());
1114    }
1115}