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