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