Skip to main content

music21_rs/pitch/
accidental.rs

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