Skip to main content

music21_rs/pitch/
accidental.rs

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