Skip to main content

music21_rs/
notation.rs

1//! Notation attached to a note or a chord: ties, noteheads, stem direction,
2//! colour and lyrics.
3//!
4//! These are the parts of music21's `note.NotRest` and `note.GeneralNote`
5//! that carry musical intent rather than layout. A tie says two written notes
6//! sound as one; a diamond notehead says a string harmonic; a lyric is the
7//! text sung on the note. None of them need a stream to mean something, which
8//! is why they live in the data model here alongside pitch and duration.
9
10use std::fmt;
11use std::str::FromStr;
12
13use crate::{
14    defaults::IntegerType,
15    duration::DurationType,
16    error::{Error, Result},
17};
18
19/// How a tie joins this note to its neighbours: music21's `tie.Tie.type`.
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22pub enum TieType {
23    /// The note begins a tie.
24    Start,
25    /// The note ends a tie.
26    Stop,
27    /// The note is tied both from the one before and to the one after.
28    Continue,
29    /// The note rings on with no written end, as a piano pedal note does.
30    LetRing,
31    /// A `continue` that also lets the sound ring on.
32    ContinueLetRing,
33}
34
35impl TieType {
36    /// Every tie type, in music21's order.
37    pub const ALL: [TieType; 5] = [
38        TieType::Start,
39        TieType::Stop,
40        TieType::Continue,
41        TieType::LetRing,
42        TieType::ContinueLetRing,
43    ];
44
45    /// music21's name for the type.
46    pub fn as_str(self) -> &'static str {
47        match self {
48            TieType::Start => "start",
49            TieType::Stop => "stop",
50            TieType::Continue => "continue",
51            TieType::LetRing => "let-ring",
52            TieType::ContinueLetRing => "continue-let-ring",
53        }
54    }
55
56    /// Reads music21's name for the type.
57    pub fn from_name(name: &str) -> Result<Self> {
58        Self::ALL
59            .into_iter()
60            .find(|candidate| candidate.as_str() == name)
61            .ok_or_else(|| {
62                let valid = Self::ALL
63                    .iter()
64                    .map(|candidate| format!("'{}'", candidate.as_str()))
65                    .collect::<Vec<_>>()
66                    .join(", ");
67                Error::Notation(format!("Type must be one of ({valid}), not {name}"))
68            })
69    }
70}
71
72impl fmt::Display for TieType {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        f.write_str(self.as_str())
75    }
76}
77
78/// How the tie is drawn: music21's `tie.Tie.style`.
79#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
80#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
81pub enum TieStyle {
82    /// A solid tie.
83    #[default]
84    Normal,
85    /// A dotted tie.
86    Dotted,
87    /// A dashed tie.
88    Dashed,
89    /// A tie that sounds but is not drawn.
90    Hidden,
91}
92
93impl TieStyle {
94    /// Every tie style.
95    pub const ALL: [TieStyle; 4] = [
96        TieStyle::Normal,
97        TieStyle::Dotted,
98        TieStyle::Dashed,
99        TieStyle::Hidden,
100    ];
101
102    /// music21's name for the style.
103    pub fn as_str(self) -> &'static str {
104        match self {
105            TieStyle::Normal => "normal",
106            TieStyle::Dotted => "dotted",
107            TieStyle::Dashed => "dashed",
108            TieStyle::Hidden => "hidden",
109        }
110    }
111
112    /// Reads music21's name for the style.
113    pub fn from_name(name: &str) -> Result<Self> {
114        Self::ALL
115            .into_iter()
116            .find(|candidate| candidate.as_str() == name)
117            .ok_or_else(|| Error::Notation(format!("not a valid tie style: {name}")))
118    }
119}
120
121impl fmt::Display for TieStyle {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        f.write_str(self.as_str())
124    }
125}
126
127/// Which side of the note a mark sits on.
128#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
129#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
130pub enum Placement {
131    /// Above the note.
132    Above,
133    /// Below the note.
134    Below,
135}
136
137impl Placement {
138    /// music21's name for the placement.
139    pub fn as_str(self) -> &'static str {
140        match self {
141            Placement::Above => "above",
142            Placement::Below => "below",
143        }
144    }
145
146    /// Reads music21's name for the placement.
147    pub fn from_name(name: &str) -> Result<Self> {
148        match name {
149            "above" => Ok(Placement::Above),
150            "below" => Ok(Placement::Below),
151            other => Err(Error::Notation(format!("not a valid placement: {other}"))),
152        }
153    }
154}
155
156impl fmt::Display for Placement {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        f.write_str(self.as_str())
159    }
160}
161
162/// A tie joining a note to its neighbours: music21's `tie.Tie`.
163///
164/// A tie on the first of two notes is enough to say they sound as one; the
165/// matching `Stop` on the second is what a MusicXML writer needs, not what
166/// the music needs.
167#[derive(Clone, Debug, PartialEq, Eq, Hash)]
168#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
169#[must_use]
170pub struct Tie {
171    tie_type: TieType,
172    style: TieStyle,
173    placement: Option<Placement>,
174}
175
176impl Tie {
177    /// A tie of the given type, drawn normally, with no placement of its own.
178    pub fn new(tie_type: TieType) -> Self {
179        Self {
180            tie_type,
181            style: TieStyle::default(),
182            placement: None,
183        }
184    }
185
186    /// Reads a tie from music21's type name, `"start"` through
187    /// `"continue-let-ring"`.
188    pub fn from_name(name: &str) -> Result<Self> {
189        Ok(Self::new(TieType::from_name(name)?))
190    }
191
192    /// The tie type.
193    pub fn tie_type(&self) -> TieType {
194        self.tie_type
195    }
196
197    /// Replaces the tie type.
198    pub fn set_tie_type(&mut self, tie_type: TieType) {
199        self.tie_type = tie_type;
200    }
201
202    /// How the tie is drawn.
203    pub fn style(&self) -> TieStyle {
204        self.style
205    }
206
207    /// Sets how the tie is drawn.
208    pub fn set_style(&mut self, style: TieStyle) {
209        self.style = style;
210    }
211
212    /// Which side of the note the tie sits on, when it was said.
213    pub fn placement(&self) -> Option<Placement> {
214        self.placement
215    }
216
217    /// Sets which side of the note the tie sits on.
218    pub fn set_placement(&mut self, placement: Option<Placement>) {
219        self.placement = placement;
220    }
221}
222
223impl Default for Tie {
224    /// A starting tie, as music21's `Tie()` is.
225    fn default() -> Self {
226        Self::new(TieType::Start)
227    }
228}
229
230impl fmt::Display for Tie {
231    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232        write!(f, "Tie {}", self.tie_type)
233    }
234}
235
236impl FromStr for Tie {
237    type Err = Error;
238
239    fn from_str(name: &str) -> Result<Self> {
240        Self::from_name(name)
241    }
242}
243
244/// The shape drawn for a note head: music21's `noteheadTypeNames`.
245#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
246#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
247#[must_use]
248pub enum Notehead {
249    /// `arrow down`
250    ArrowDown,
251    /// `arrow up`
252    ArrowUp,
253    /// `back slashed`
254    BackSlashed,
255    /// `circle dot`
256    CircleDot,
257    /// `circle-x`
258    CircleX,
259    /// `circled`
260    Circled,
261    /// `cluster`
262    Cluster,
263    /// `cross`
264    Cross,
265    /// `diamond`, which is how a string harmonic is written.
266    Diamond,
267    /// `do`
268    Do,
269    /// `fa`
270    Fa,
271    /// `fa up`
272    FaUp,
273    /// `inverted triangle`
274    InvertedTriangle,
275    /// `la`
276    La,
277    /// `left triangle`
278    LeftTriangle,
279    /// `mi`
280    Mi,
281    /// `none`
282    NoneShape,
283    /// `normal`, the default.
284    #[default]
285    Normal,
286    /// `other`
287    Other,
288    /// `re`
289    Re,
290    /// `rectangle`
291    Rectangle,
292    /// `slash`
293    Slash,
294    /// `slashed`
295    Slashed,
296    /// `so`
297    So,
298    /// `square`
299    Square,
300    /// `ti`
301    Ti,
302    /// `triangle`
303    Triangle,
304    /// `x`
305    X,
306}
307
308impl Notehead {
309    /// Every notehead shape, in music21's order.
310    pub const ALL: [Notehead; 28] = [
311        Notehead::ArrowDown,
312        Notehead::ArrowUp,
313        Notehead::BackSlashed,
314        Notehead::CircleDot,
315        Notehead::CircleX,
316        Notehead::Circled,
317        Notehead::Cluster,
318        Notehead::Cross,
319        Notehead::Diamond,
320        Notehead::Do,
321        Notehead::Fa,
322        Notehead::FaUp,
323        Notehead::InvertedTriangle,
324        Notehead::La,
325        Notehead::LeftTriangle,
326        Notehead::Mi,
327        Notehead::NoneShape,
328        Notehead::Normal,
329        Notehead::Other,
330        Notehead::Re,
331        Notehead::Rectangle,
332        Notehead::Slash,
333        Notehead::Slashed,
334        Notehead::So,
335        Notehead::Square,
336        Notehead::Ti,
337        Notehead::Triangle,
338        Notehead::X,
339    ];
340
341    /// music21's name for the shape.
342    pub fn as_str(self) -> &'static str {
343        match self {
344            Notehead::ArrowDown => "arrow down",
345            Notehead::ArrowUp => "arrow up",
346            Notehead::BackSlashed => "back slashed",
347            Notehead::CircleDot => "circle dot",
348            Notehead::CircleX => "circle-x",
349            Notehead::Circled => "circled",
350            Notehead::Cluster => "cluster",
351            Notehead::Cross => "cross",
352            Notehead::Diamond => "diamond",
353            Notehead::Do => "do",
354            Notehead::Fa => "fa",
355            Notehead::FaUp => "fa up",
356            Notehead::InvertedTriangle => "inverted triangle",
357            Notehead::La => "la",
358            Notehead::LeftTriangle => "left triangle",
359            Notehead::Mi => "mi",
360            Notehead::NoneShape => "none",
361            Notehead::Normal => "normal",
362            Notehead::Other => "other",
363            Notehead::Re => "re",
364            Notehead::Rectangle => "rectangle",
365            Notehead::Slash => "slash",
366            Notehead::Slashed => "slashed",
367            Notehead::So => "so",
368            Notehead::Square => "square",
369            Notehead::Ti => "ti",
370            Notehead::Triangle => "triangle",
371            Notehead::X => "x",
372        }
373    }
374
375    /// Reads music21's name for the shape.
376    pub fn from_name(name: &str) -> Result<Self> {
377        Self::ALL
378            .into_iter()
379            .find(|candidate| candidate.as_str() == name)
380            .ok_or_else(|| Error::Notation(format!("not a valid notehead type name: '{name}'")))
381    }
382}
383
384impl fmt::Display for Notehead {
385    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
386        f.write_str(self.as_str())
387    }
388}
389
390/// How one beam of a note joins its neighbours: music21's `beam.Beam`.
391///
392/// A beam is a horizontal line joining the flags of consecutive short notes,
393/// and each note carries one for every level it is beamed at — an eighth has
394/// one, a sixteenth two. Whether the line starts here, carries through, ends
395/// here, or is only a stub is rhythmic grouping written down, which is why
396/// this lives here beside the ties and the noteheads and not among the
397/// things that need a page.
398#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
399#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
400pub enum BeamType {
401    /// The beam begins on this note.
402    #[default]
403    Start,
404    /// The beam carries through this note.
405    Continue,
406    /// The beam ends on this note.
407    Stop,
408    /// A stub reaching only part of the way, pointing left or right.
409    PartialBeam,
410}
411
412impl BeamType {
413    /// music21's name for the type.
414    pub fn as_str(self) -> &'static str {
415        match self {
416            Self::Start => "start",
417            Self::Continue => "continue",
418            Self::Stop => "stop",
419            Self::PartialBeam => "partial",
420        }
421    }
422
423    /// Reads music21's name.
424    pub fn from_music21_name(name: &str) -> Option<Self> {
425        match name {
426            "start" => Some(Self::Start),
427            "continue" => Some(Self::Continue),
428            "stop" => Some(Self::Stop),
429            "partial" => Some(Self::PartialBeam),
430            _ => None,
431        }
432    }
433}
434
435impl fmt::Display for BeamType {
436    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
437        f.write_str(self.as_str())
438    }
439}
440
441/// Which way a partial beam points: music21's `direction`.
442#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
443#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
444pub enum BeamDirection {
445    /// Back towards the note before.
446    Left,
447    /// On towards the note after.
448    Right,
449}
450
451impl BeamDirection {
452    /// music21's name for the direction.
453    pub fn as_str(self) -> &'static str {
454        match self {
455            Self::Left => "left",
456            Self::Right => "right",
457        }
458    }
459
460    /// Reads music21's name.
461    pub fn from_music21_name(name: &str) -> Option<Self> {
462        match name {
463            "left" => Some(Self::Left),
464            "right" => Some(Self::Right),
465            _ => None,
466        }
467    }
468}
469
470impl fmt::Display for BeamDirection {
471    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
472        f.write_str(self.as_str())
473    }
474}
475
476/// One beam at one level: music21's `beam.Beam`.
477#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
478#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
479#[must_use]
480pub struct Beam {
481    /// What the beam does at this note. music21 leaves it unsaid on a beam
482    /// that has been counted but not yet decided — which is what `fill`
483    /// makes, and what a beam built with no arguments is.
484    beam_type: Option<BeamType>,
485    direction: Option<BeamDirection>,
486    number: Option<u32>,
487}
488
489impl Beam {
490    /// A beam of the given type, with a direction only a partial one needs.
491    pub fn new(beam_type: impl Into<Option<BeamType>>, direction: Option<BeamDirection>) -> Self {
492        Self {
493            beam_type: beam_type.into(),
494            direction,
495            number: None,
496        }
497    }
498
499    /// Whether the beam starts, carries on, ends, or is a stub — and nothing
500    /// at all where that has not been decided.
501    pub fn beam_type(&self) -> Option<BeamType> {
502        self.beam_type
503    }
504
505    /// Says what the beam does at this note.
506    pub fn set_beam_type(&mut self, beam_type: Option<BeamType>) {
507        self.beam_type = beam_type;
508    }
509
510    /// Which way a stub points, and nothing for a beam that is not one.
511    pub fn direction(&self) -> Option<BeamDirection> {
512        self.direction
513    }
514
515    /// Points a stub the other way.
516    pub fn set_direction(&mut self, direction: Option<BeamDirection>) {
517        self.direction = direction;
518    }
519
520    /// Which level this beam is: the first is the one an eighth note has.
521    pub fn number(&self) -> Option<u32> {
522        self.number
523    }
524
525    /// Puts the beam at a level.
526    pub fn set_number(&mut self, number: Option<u32>) {
527        self.number = number;
528    }
529}
530
531impl fmt::Display for Beam {
532    /// music21's `_reprInternal`: the level, the type, and the direction of
533    /// a stub.
534    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
535        let number = match self.number {
536            Some(number) => number.to_string(),
537            None => "None".to_string(),
538        };
539        let beam_type = match self.beam_type {
540            Some(beam_type) => beam_type.to_string(),
541            None => "None".to_string(),
542        };
543        match self.direction {
544            Some(direction) => write!(f, "{number}/{beam_type}/{direction}"),
545            None => write!(f, "{number}/{beam_type}"),
546        }
547    }
548}
549
550/// The beams of one note, one for each level it is beamed at: music21's
551/// `beam.Beams`.
552#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
553#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
554#[must_use]
555pub struct Beams {
556    beams: Vec<Beam>,
557    /// Whether the beam group is drawn fanned out, for an accelerando.
558    feathered: bool,
559}
560
561/// The written values that can carry a beam at all, and how many levels each
562/// has: music21's `beamableDurationTypes`, an eighth through a 2048th.
563const BEAMABLE: [(DurationType, u32); 9] = [
564    (DurationType::Eighth, 1),
565    (DurationType::Sixteenth, 2),
566    (DurationType::ThirtySecond, 3),
567    (DurationType::SixtyFourth, 4),
568    (DurationType::HundredTwentyEighth, 5),
569    (DurationType::TwoHundredFiftySixth, 6),
570    (DurationType::FiveHundredTwelfth, 7),
571    (DurationType::TenTwentyFourth, 8),
572    (DurationType::TwentyFortyEighth, 9),
573];
574
575impl Beams {
576    /// No beams at all.
577    pub fn new() -> Self {
578        Self::default()
579    }
580
581    /// The beams, in level order.
582    pub fn beams(&self) -> &[Beam] {
583        &self.beams
584    }
585
586    /// How many levels are beamed.
587    pub fn len(&self) -> usize {
588        self.beams.len()
589    }
590
591    /// Whether the note carries no beam at all.
592    pub fn is_empty(&self) -> bool {
593        self.beams.is_empty()
594    }
595
596    /// Whether the group is drawn fanned out.
597    pub fn feathered(&self) -> bool {
598        self.feathered
599    }
600
601    /// Draws the group fanned out, or stops doing so.
602    pub fn set_feathered(&mut self, feathered: bool) {
603        self.feathered = feathered;
604    }
605
606    /// The beams, to be edited in place.
607    pub fn beams_mut(&mut self) -> &mut Vec<Beam> {
608        &mut self.beams
609    }
610
611    /// Replaces the beams outright.
612    pub fn set_beams(&mut self, beams: Vec<Beam>) {
613        self.beams = beams;
614    }
615
616    /// Adds a beam at the next level down: music21's `append`.
617    pub fn append(
618        &mut self,
619        beam_type: impl Into<Option<BeamType>>,
620        direction: Option<BeamDirection>,
621    ) {
622        let mut beam = Beam::new(beam_type, direction);
623        beam.set_number(Some(self.beams.len() as u32 + 1));
624        self.beams.push(beam);
625    }
626
627    /// How many beams a written value carries, where it carries any:
628    /// How many beams a written value can carry at the most: music21's
629    /// `beamableDurationTypes`, an eighth through a 2048th. It is how many
630    /// depths a run of notes is beamed at.
631    pub const LEVELS: usize = BEAMABLE.len();
632
633    /// music21's `beamableDurationTypes` read as a count of levels.
634    pub fn levels_for(duration_type: DurationType) -> Option<u32> {
635        BEAMABLE
636            .into_iter()
637            .find(|(candidate, _)| *candidate == duration_type)
638            .map(|(_, levels)| levels)
639    }
640
641    /// Gives the note as many beams as its written value has, all of the same
642    /// type: music21's `fill`.
643    ///
644    /// A value that carries no beam — anything a quarter or longer — is an
645    /// error, as it is upstream.
646    pub fn fill(&mut self, duration_type: DurationType, beam_type: Option<BeamType>) -> Result<()> {
647        let Some(levels) = Self::levels_for(duration_type) else {
648            return Err(Error::Notation(format!(
649                "cannot fill beams for a {} note",
650                duration_type.music21_name()
651            )));
652        };
653        self.fill_levels(levels, beam_type)
654    }
655
656    /// The same, counting the levels rather than naming the written value:
657    /// music21 takes either, and `fill(2)` is a sixteenth.
658    ///
659    /// The beams are left with nothing said about what they do, unless a type
660    /// is given — music21 counts the lines first and decides them after.
661    pub fn fill_levels(&mut self, levels: u32, beam_type: Option<BeamType>) -> Result<()> {
662        if levels == 0 || levels > BEAMABLE.len() as u32 {
663            return Err(Error::Notation(format!(
664                "cannot fill beams for level {levels}"
665            )));
666        }
667        self.beams.clear();
668        for _ in 0..levels {
669            self.append(None, None);
670        }
671        if let Some(beam_type) = beam_type {
672            self.set_all(beam_type, None);
673        }
674        Ok(())
675    }
676
677    /// Sets every beam to the same type: music21's `setAll`.
678    pub fn set_all(&mut self, beam_type: BeamType, direction: Option<BeamDirection>) {
679        for beam in &mut self.beams {
680            beam.beam_type = Some(beam_type);
681            beam.direction = direction;
682        }
683    }
684
685    /// The beam at a level, counting from one: music21's `getByNumber`.
686    pub fn by_number(&self, number: u32) -> Option<&Beam> {
687        self.beams.iter().find(|beam| beam.number == Some(number))
688    }
689
690    /// Sets the beam at a level, which must already be there.
691    pub fn set_by_number(
692        &mut self,
693        number: u32,
694        beam_type: BeamType,
695        direction: Option<BeamDirection>,
696    ) -> Result<()> {
697        let Some(beam) = self
698            .beams
699            .iter_mut()
700            .find(|beam| beam.number == Some(number))
701        else {
702            return Err(Error::Notation(format!(
703                "beam number {number} does not exist"
704            )));
705        };
706        beam.beam_type = Some(beam_type);
707        beam.direction = direction;
708        Ok(())
709    }
710
711    /// The type of every beam, in level order: music21's `getTypes`.
712    pub fn types(&self) -> Vec<Option<BeamType>> {
713        self.beams.iter().map(Beam::beam_type).collect()
714    }
715
716    /// The level of every beam: music21's `getNumbers`.
717    pub fn numbers(&self) -> Vec<Option<u32>> {
718        self.beams.iter().map(Beam::number).collect()
719    }
720}
721
722impl fmt::Display for Beams {
723    /// music21's `_reprInternal`: every beam, slash-separated.
724    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
725        let written: Vec<String> = self
726            .beams
727            .iter()
728            .map(|beam| format!("<music21.beam.Beam {beam}>"))
729            .collect();
730        f.write_str(&written.join("/"))
731    }
732}
733
734/// A note with nothing beamable on either side of it has nothing to beam to,
735/// so it carries no beam at all: music21's `removeSandwichedUnbeamables`.
736///
737/// The list is a run of notes, each entry the beams that note could carry or
738/// nothing where it could carry none.
739pub fn remove_sandwiched_unbeamables(beams: &mut [Option<Beams>]) {
740    let mut previous: Option<Beams> = None;
741    for index in 0..beams.len() {
742        let next = beams.get(index + 1).cloned().flatten();
743        if previous.is_none() && next.is_none() {
744            beams[index] = None;
745        }
746        previous = beams[index].clone();
747    }
748}
749
750/// Beams made only of stubs are no beams at all, and a stub beside a real
751/// beam points towards it: music21's `sanitizePartialBeams`.
752pub fn sanitize_partial_beams(beams: &mut [Option<Beams>]) {
753    for entry in beams.iter_mut() {
754        let Some(group) = entry else {
755            continue;
756        };
757        let joined = group.types().into_iter().flatten().any(|beam_type| {
758            matches!(
759                beam_type,
760                BeamType::Start | BeamType::Stop | BeamType::Continue
761            )
762        });
763        if !joined {
764            *entry = None;
765            continue;
766        }
767        let mut after_start = false;
768        let mut after_stop = false;
769        for beam in group.beams_mut() {
770            match beam.beam_type() {
771                Some(BeamType::Start) => after_start = true,
772                Some(BeamType::Stop) => after_stop = true,
773                Some(BeamType::PartialBeam) => {
774                    if after_start && beam.direction() == Some(BeamDirection::Left) {
775                        beam.set_direction(Some(BeamDirection::Right));
776                    } else if after_stop && beam.direction() == Some(BeamDirection::Right) {
777                        beam.set_direction(Some(BeamDirection::Left));
778                    }
779                }
780                _ => {}
781            }
782        }
783    }
784}
785
786/// A stub pointing right into a stub pointing left is really one beam, and is
787/// written as one: music21's `mergeConnectingPartialBeams`.
788pub fn merge_connecting_partial_beams(beams: &mut [Option<Beams>]) {
789    for index in 0..beams.len().saturating_sub(1) {
790        let Some(numbers) = beams[index]
791            .as_ref()
792            .map(|group| group.numbers().into_iter().flatten().collect::<Vec<_>>())
793        else {
794            continue;
795        };
796        if beams[index + 1].is_none() {
797            continue;
798        }
799        for number in numbers {
800            let this = beams[index]
801                .as_ref()
802                .and_then(|group| group.by_number(number))
803                .copied();
804            let next = beams[index + 1]
805                .as_ref()
806                .and_then(|group| group.by_number(number))
807                .copied();
808            let (Some(this), Some(next)) = (this, next) else {
809                continue;
810            };
811            if this.beam_type() != Some(BeamType::PartialBeam)
812                || this.direction() != Some(BeamDirection::Right)
813            {
814                continue;
815            }
816            if next.beam_type() == Some(BeamType::PartialBeam)
817                && next.direction() == Some(BeamDirection::Right)
818            {
819                continue;
820            }
821            // A partial pointing into a beam that carries on or ends is a
822            // notation nobody can draw; music21 leaves it alone and warns.
823            if matches!(
824                next.beam_type(),
825                Some(BeamType::Continue) | Some(BeamType::Stop)
826            ) {
827                continue;
828            }
829            set_beam(&mut beams[index], number, BeamType::Start, None);
830            let merged = match next.beam_type() {
831                Some(BeamType::PartialBeam) => BeamType::Stop,
832                Some(BeamType::Start) => BeamType::Continue,
833                other => other.unwrap_or(BeamType::Start),
834            };
835            set_beam(&mut beams[index + 1], number, merged, None);
836        }
837    }
838
839    // And a stub pointing left after a beam that has already ended joins it.
840    for index in 1..beams.len() {
841        let Some(numbers) = beams[index]
842            .as_ref()
843            .map(|group| group.numbers().into_iter().flatten().collect::<Vec<_>>())
844        else {
845            continue;
846        };
847        if beams[index - 1].is_none() {
848            continue;
849        }
850        for number in numbers {
851            let this = beams[index]
852                .as_ref()
853                .and_then(|group| group.by_number(number))
854                .copied();
855            let previous = beams[index - 1]
856                .as_ref()
857                .and_then(|group| group.by_number(number))
858                .copied();
859            let (Some(this), Some(previous)) = (this, previous) else {
860                continue;
861            };
862            if this.beam_type() != Some(BeamType::PartialBeam)
863                || this.direction() != Some(BeamDirection::Left)
864                || previous.beam_type() != Some(BeamType::Stop)
865            {
866                continue;
867            }
868            set_beam(&mut beams[index], number, BeamType::Stop, None);
869            set_beam(&mut beams[index - 1], number, BeamType::Continue, None);
870        }
871    }
872}
873
874/// Writes one beam of one group, where the group has one at that level.
875fn set_beam(
876    group: &mut Option<Beams>,
877    number: u32,
878    beam_type: BeamType,
879    direction: Option<BeamDirection>,
880) {
881    if let Some(group) = group {
882        let _ = group.set_by_number(number, beam_type, direction);
883    }
884}
885
886/// Which way the stem points: music21's `stemDirectionNames`.
887#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
888#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
889pub enum StemDirection {
890    /// Stems both up and down, for a divided part.
891    Double,
892    /// Stem down.
893    Down,
894    /// Written with no stem at all, as a harmonic sounding pitch is.
895    NoStem,
896    /// Not said, the default.
897    #[default]
898    Unspecified,
899    /// Stem up.
900    Up,
901}
902
903impl StemDirection {
904    /// Every stem direction a note can hold.
905    ///
906    /// One shorter than music21's [`StemDirection::NAMES`]: `"none"` is an
907    /// input spelling of `"noStem"` that music21's setter rewrites, so no
908    /// note ever reads back as `"none"`.
909    pub const ALL: [StemDirection; 5] = [
910        StemDirection::Double,
911        StemDirection::Down,
912        StemDirection::NoStem,
913        StemDirection::Unspecified,
914        StemDirection::Up,
915    ];
916
917    /// music21's `stemDirectionNames`: the names the setter accepts, which
918    /// includes the `"none"` spelling of `"noStem"`.
919    pub const NAMES: [&'static str; 6] = ["double", "down", "noStem", "none", "unspecified", "up"];
920
921    /// music21's name for the direction.
922    pub fn as_str(self) -> &'static str {
923        match self {
924            StemDirection::Double => "double",
925            StemDirection::Down => "down",
926            StemDirection::NoStem => "noStem",
927            StemDirection::Unspecified => "unspecified",
928            StemDirection::Up => "up",
929        }
930    }
931
932    /// Reads music21's name for the direction.
933    ///
934    /// `"none"` reads as [`StemDirection::NoStem`], which is what music21's
935    /// setter stores for it.
936    pub fn from_name(name: &str) -> Result<Self> {
937        if name == "none" {
938            return Ok(StemDirection::NoStem);
939        }
940        Self::ALL
941            .into_iter()
942            .find(|candidate| candidate.as_str() == name)
943            .ok_or_else(|| Error::Notation(format!("not a valid stem direction name: {name}")))
944    }
945}
946
947impl fmt::Display for StemDirection {
948    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
949        f.write_str(self.as_str())
950    }
951}
952
953/// Where a lyric sits in a word: music21's `Lyric.syllabic`.
954#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
955#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
956pub enum Syllabic {
957    /// The whole word.
958    Single,
959    /// The first syllable of a word, hyphenated to what follows.
960    Begin,
961    /// A middle syllable, hyphenated on both sides.
962    Middle,
963    /// The last syllable of a word.
964    End,
965    /// A syllable that carries no hyphens, written as `text` in MusicXML.
966    Composite,
967}
968
969impl Syllabic {
970    /// Every syllabic position.
971    pub const ALL: [Syllabic; 5] = [
972        Syllabic::Single,
973        Syllabic::Begin,
974        Syllabic::Middle,
975        Syllabic::End,
976        Syllabic::Composite,
977    ];
978
979    /// music21's name for the position.
980    pub fn as_str(self) -> &'static str {
981        match self {
982            Syllabic::Single => "single",
983            Syllabic::Begin => "begin",
984            Syllabic::Middle => "middle",
985            Syllabic::End => "end",
986            Syllabic::Composite => "composite",
987        }
988    }
989
990    /// Reads music21's name for the position.
991    pub fn from_name(name: &str) -> Result<Self> {
992        Self::ALL
993            .into_iter()
994            .find(|candidate| candidate.as_str() == name)
995            .ok_or_else(|| {
996                // music21 quotes the value and lists what it would have
997                // taken, `None` included even though it is not a position.
998                Error::Notation(format!(
999                    "Syllabic value '{name}' is not in note.SYLLABIC_CHOICES, namely: [None, 'begin', 'single', 'end', 'middle', 'composite']"
1000                ))
1001            })
1002    }
1003}
1004
1005impl fmt::Display for Syllabic {
1006    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1007        f.write_str(self.as_str())
1008    }
1009}
1010
1011/// A syllable of text sung on a note: music21's `note.Lyric`.
1012///
1013/// The hyphens in the written text say where the syllable falls in its word,
1014/// which is why [`Self::from_raw_text`] reads `"-ci-"` as a middle syllable
1015/// and [`Self::raw_text`] writes the hyphens back.
1016#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1017#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1018#[must_use]
1019pub struct Lyric {
1020    /// The syllable, or nothing at all: music21 leaves the text of a bare
1021    /// `Lyric()` unset, and a lyric nobody has sung anything to is not the
1022    /// same as one sung to silence.
1023    text: Option<String>,
1024    number: IntegerType,
1025    syllabic: Syllabic,
1026    /// Whether anything has said where this syllable falls in its word.
1027    ///
1028    /// music21 leaves `syllabic` unset on a lyric with nothing sung yet, and
1029    /// a lyric that says nothing about its place in a word is not the same
1030    /// as one that says it is a whole word.
1031    said_syllabic: bool,
1032    identifier: Option<String>,
1033    components: Vec<Lyric>,
1034    elision_before: String,
1035}
1036
1037/// What music21's `Lyric.elisionBefore` starts as: a space between this
1038/// syllable and the one before it inside a composite lyric.
1039const DEFAULT_ELISION: &str = " ";
1040
1041impl Lyric {
1042    /// A lyric on the first verse, spelled as a whole word.
1043    pub fn new(text: impl Into<String>) -> Self {
1044        let mut lyric = Self::unsung();
1045        lyric.text = Some(text.into());
1046        lyric.said_syllabic = true;
1047        lyric
1048    }
1049
1050    /// A lyric with nothing sung to it yet, which is music21's bare
1051    /// `Lyric()`: it says nothing about where it falls in a word, and
1052    /// writing its text does not make it say so.
1053    pub fn unsung() -> Self {
1054        Self {
1055            text: Some(String::new()),
1056            number: 1,
1057            syllabic: Syllabic::Single,
1058            said_syllabic: false,
1059            identifier: None,
1060            components: Vec::new(),
1061            elision_before: DEFAULT_ELISION.to_string(),
1062        }
1063    }
1064
1065    /// Reads a lyric from text whose hyphens say where it falls in its word:
1066    /// `"-ci-"` is a middle syllable, `"ci-"` a beginning, `"-us"` an end.
1067    pub fn from_raw_text(raw_text: &str) -> Self {
1068        let mut lyric = Self::unsung();
1069        lyric.set_raw_text(raw_text);
1070        lyric
1071    }
1072
1073    /// The syllable without its hyphens.
1074    ///
1075    /// A composite lyric has no text of its own: it reads as its components
1076    /// run together, each joined on by its own [`Self::elision_before`].
1077    pub fn text(&self) -> String {
1078        let Some((first, rest)) = self.components.split_first() else {
1079            return self.text.clone().unwrap_or_default();
1080        };
1081        let mut text = first.text();
1082        for component in rest {
1083            text.push_str(&component.elision_before);
1084            text.push_str(&component.text());
1085        }
1086        text
1087    }
1088
1089    /// Replaces the syllable text, leaving its position in the word alone.
1090    ///
1091    /// Setting the text of a composite lyric drops its components, which is
1092    /// what music21 does: the text you gave it is now the whole of it.
1093    pub fn set_text(&mut self, text: impl Into<String>) {
1094        self.components.clear();
1095        self.text = Some(text.into());
1096    }
1097
1098    /// The syllable as music21's `text` reads it, which may be nothing at
1099    /// all: music21 starts a lyric off sung to the empty string, and setting
1100    /// its text to nothing is how a score says the syllable was taken away.
1101    pub fn explicit_text(&self) -> Option<String> {
1102        if self.is_composite() {
1103            return Some(self.text());
1104        }
1105        self.text.clone()
1106    }
1107
1108    /// Takes the text away, leaving a lyric with nothing sung to it at all —
1109    /// which music21 tells apart from one sung to the empty string, and
1110    /// writes out as neither.
1111    pub fn clear_text(&mut self) {
1112        self.components.clear();
1113        self.text = None;
1114    }
1115
1116    /// Whether this lyric is several lyrics sung together rather than one
1117    /// syllable: music21's `isComposite`.
1118    pub fn is_composite(&self) -> bool {
1119        !self.components.is_empty()
1120    }
1121
1122    /// The lyrics this one is made of, empty unless it is composite.
1123    pub fn components(&self) -> &[Lyric] {
1124        &self.components
1125    }
1126
1127    /// Makes this lyric the several lyrics given, run together. An empty
1128    /// list makes it an ordinary one again, keeping the text it had.
1129    pub fn set_components(&mut self, components: Vec<Lyric>) {
1130        self.components = components;
1131    }
1132
1133    /// What joins this syllable to the one before it inside a composite
1134    /// lyric — a space by default, an underscore for an elision.
1135    pub fn elision_before(&self) -> &str {
1136        &self.elision_before
1137    }
1138
1139    /// Sets what joins this syllable to the one before it.
1140    pub fn set_elision_before(&mut self, elision: impl Into<String>) {
1141        self.elision_before = elision.into();
1142    }
1143
1144    /// The verse number, counting from one.
1145    pub fn number(&self) -> IntegerType {
1146        self.number
1147    }
1148
1149    /// Sets the verse number, which must be positive as music21 requires.
1150    /// Sets which verse this lyric belongs to.
1151    ///
1152    /// Any whole number will do, as music21's own setter allows: its
1153    /// MusicXML reader numbers a verse `0` when the score named it with
1154    /// something that is not a number at all, and keeps the name as the
1155    /// identifier.
1156    pub fn set_number(&mut self, number: IntegerType) {
1157        self.number = number;
1158    }
1159
1160    /// Where the syllable falls in its word. A composite lyric reads as
1161    /// [`Syllabic::Composite`] whatever it was set to.
1162    pub fn syllabic(&self) -> Syllabic {
1163        if self.is_composite() {
1164            return Syllabic::Composite;
1165        }
1166        self.syllabic
1167    }
1168
1169    /// The same, where something has actually said it: music21 leaves it
1170    /// unset on a lyric nobody has sung anything to.
1171    pub fn explicit_syllabic(&self) -> Option<Syllabic> {
1172        if self.is_composite() {
1173            return Some(Syllabic::Composite);
1174        }
1175        self.said_syllabic.then_some(self.syllabic)
1176    }
1177
1178    /// Sets where the syllable falls in its word.
1179    pub fn set_syllabic(&mut self, syllabic: Syllabic) {
1180        self.syllabic = syllabic;
1181        self.said_syllabic = true;
1182    }
1183
1184    /// The name this verse goes by, which music21 falls back to the number
1185    /// for when none was given.
1186    pub fn identifier(&self) -> String {
1187        self.identifier
1188            .clone()
1189            .unwrap_or_else(|| self.number.to_string())
1190    }
1191
1192    /// The name this verse was given, and nothing when it goes by its
1193    /// number. music21's `identifier` answers the number itself in that
1194    /// case, which is a different type, so a caller that cares asks here.
1195    pub fn explicit_identifier(&self) -> Option<&str> {
1196        self.identifier.as_deref()
1197    }
1198
1199    /// Names this verse, or clears the name so the number stands in.
1200    pub fn set_identifier(&mut self, identifier: Option<String>) {
1201        self.identifier = identifier;
1202    }
1203
1204    /// The syllable written with the hyphens its position implies.
1205    ///
1206    /// A composite lyric takes its hyphens from the ends of the run: the
1207    /// first component decides whether one leads, the last whether one
1208    /// trails.
1209    pub fn raw_text(&self) -> String {
1210        if self.explicit_text().is_none() {
1211            return String::new();
1212        }
1213        let text = self.text();
1214        let (Some(first), Some(last)) = (self.components.first(), self.components.last()) else {
1215            return match self.syllabic {
1216                Syllabic::Begin => format!("{text}-"),
1217                Syllabic::Middle => format!("-{text}-"),
1218                Syllabic::End => format!("-{text}"),
1219                Syllabic::Single | Syllabic::Composite => text,
1220            };
1221        };
1222        let leading = matches!(first.syllabic(), Syllabic::Middle | Syllabic::End);
1223        let trailing = matches!(last.syllabic(), Syllabic::Begin | Syllabic::Middle);
1224        format!(
1225            "{}{text}{}",
1226            if leading { "-" } else { "" },
1227            if trailing { "-" } else { "" }
1228        )
1229    }
1230
1231    /// Reads the syllable and its position out of hyphenated text. Like
1232    /// [`Self::set_text`], this drops any components.
1233    pub fn set_raw_text(&mut self, raw_text: &str) {
1234        self.set_text_and_syllabic(raw_text, false);
1235    }
1236
1237    /// music21's `setTextAndSyllabic`: [`Self::set_raw_text`], unless
1238    /// `apply_raw` says the hyphens are the text itself. Then nothing about
1239    /// the word is read out of them and the position stands as it was,
1240    /// except that a lyric that had said nothing about where it falls is now
1241    /// a whole word. Either way drops any components.
1242    pub fn set_text_and_syllabic(&mut self, raw_text: &str, apply_raw: bool) {
1243        if apply_raw {
1244            self.set_text(raw_text);
1245            if !self.said_syllabic {
1246                self.set_syllabic(Syllabic::Single);
1247            }
1248            return;
1249        }
1250        self.components.clear();
1251        let starts = raw_text.starts_with('-');
1252        let ends = raw_text.ends_with('-') && raw_text.len() > 1;
1253        let trimmed = raw_text.strip_prefix('-').unwrap_or(raw_text).to_string();
1254        let trimmed = if ends {
1255            trimmed.strip_suffix('-').unwrap_or(&trimmed).to_string()
1256        } else {
1257            trimmed
1258        };
1259        self.set_syllabic(match (starts, ends) {
1260            (true, true) => Syllabic::Middle,
1261            (true, false) => Syllabic::End,
1262            (false, true) => Syllabic::Begin,
1263            (false, false) => Syllabic::Single,
1264        });
1265        self.text = Some(trimmed);
1266    }
1267}
1268
1269impl fmt::Display for Lyric {
1270    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1271        f.write_str(&self.text())
1272    }
1273}
1274
1275#[cfg(test)]
1276mod tests {
1277    #[test]
1278    fn ties_beams_and_placements_are_written_and_read_by_name() {
1279        use super::{
1280            Beam, BeamDirection, BeamType, Beams, Lyric, Placement, Syllabic, Tie, TieStyle,
1281            TieType,
1282        };
1283        use std::str::FromStr;
1284
1285        let mut tie = Tie::from_str("start").unwrap();
1286        assert_eq!(tie.tie_type(), TieType::Start);
1287        tie.set_tie_type(TieType::Stop);
1288        assert_eq!(tie.tie_type(), TieType::Stop);
1289        assert!(Tie::from_str("sideways").is_err());
1290        assert_eq!(TieStyle::Dotted.to_string(), "dotted");
1291        assert_eq!(Placement::Above.to_string(), "above");
1292        assert_eq!(Syllabic::Middle.to_string(), "middle");
1293
1294        assert_eq!(
1295            BeamType::from_music21_name("partial"),
1296            Some(BeamType::PartialBeam)
1297        );
1298        assert_eq!(BeamType::from_music21_name("sideways"), None);
1299        assert_eq!(
1300            BeamDirection::from_music21_name("left"),
1301            Some(BeamDirection::Left)
1302        );
1303        assert_eq!(BeamDirection::from_music21_name("up"), None);
1304
1305        let mut beam = Beam::new(BeamType::Start, None);
1306        beam.set_beam_type(Some(BeamType::Continue));
1307        beam.set_direction(Some(BeamDirection::Right));
1308        assert_eq!(beam.beam_type(), Some(BeamType::Continue));
1309        assert_eq!(beam.direction(), Some(BeamDirection::Right));
1310
1311        let mut beams = Beams::default();
1312        assert!(beams.is_empty());
1313        beams.set_beams(vec![beam]);
1314        assert_eq!(beams.beams().len(), 1);
1315        beams.beams_mut()[0].set_beam_type(Some(BeamType::Stop));
1316        assert_eq!(beams.beams()[0].beam_type(), Some(BeamType::Stop));
1317        assert!(!beams.feathered());
1318        beams.set_feathered(true);
1319        assert!(beams.feathered());
1320
1321        let mut lyric = Lyric::new("hel");
1322        assert_eq!(lyric.elision_before(), " ");
1323        assert_eq!(lyric.explicit_syllabic(), Some(Syllabic::Single));
1324        lyric.clear_text();
1325        assert_eq!(lyric.explicit_text(), None);
1326        assert_eq!(Lyric::unsung().explicit_syllabic(), None);
1327    }
1328
1329    /// The three passes music21 runs over a bar's naive beams, on the
1330    /// shapes each one exists for.
1331    #[test]
1332    fn the_beam_passes_tidy_a_run_of_beams() {
1333        use super::{
1334            BeamDirection, BeamType, Beams, merge_connecting_partial_beams,
1335            remove_sandwiched_unbeamables, sanitize_partial_beams,
1336        };
1337        use crate::duration::DurationType;
1338
1339        let filled = |beam_type: BeamType| {
1340            let mut beams = Beams::default();
1341            beams.fill(DurationType::Eighth, Some(beam_type)).unwrap();
1342            Some(beams)
1343        };
1344        let partial = |direction: BeamDirection| {
1345            let mut beams = Beams::default();
1346            beams
1347                .fill(DurationType::Eighth, Some(BeamType::PartialBeam))
1348                .unwrap();
1349            beams.beams_mut()[0].set_direction(Some(direction));
1350            Some(beams)
1351        };
1352
1353        // A beamable note between two unbeamable ones cannot be beamed.
1354        let mut run = vec![
1355            None,
1356            filled(BeamType::Start),
1357            None,
1358            filled(BeamType::Start),
1359            filled(BeamType::Stop),
1360        ];
1361        remove_sandwiched_unbeamables(&mut run);
1362        assert!(run[1].is_none());
1363        assert!(run[3].is_some());
1364
1365        // A group of nothing but partials is dropped; a partial after a
1366        // start points right and one after a stop points left.
1367        let mut lone = vec![partial(BeamDirection::Left)];
1368        sanitize_partial_beams(&mut lone);
1369        assert!(lone[0].is_none());
1370        let mut mixed = Beams::default();
1371        mixed.append(BeamType::Start, None);
1372        mixed.append(BeamType::PartialBeam, Some(BeamDirection::Left));
1373        let mut run = vec![Some(mixed)];
1374        sanitize_partial_beams(&mut run);
1375        let group = run[0].as_ref().unwrap();
1376        assert_eq!(group.beams()[1].direction(), Some(BeamDirection::Right));
1377
1378        // Two partials pointing at each other are one beam.
1379        let mut run = vec![partial(BeamDirection::Right), partial(BeamDirection::Left)];
1380        merge_connecting_partial_beams(&mut run);
1381        assert_eq!(
1382            run[0].as_ref().unwrap().beams()[0].beam_type(),
1383            Some(BeamType::Start)
1384        );
1385        assert_eq!(
1386            run[1].as_ref().unwrap().beams()[0].beam_type(),
1387            Some(BeamType::Stop)
1388        );
1389        // A partial pointing into a beam that starts carries it on.
1390        let mut run = vec![partial(BeamDirection::Right), filled(BeamType::Start)];
1391        merge_connecting_partial_beams(&mut run);
1392        assert_eq!(
1393            run[1].as_ref().unwrap().beams()[0].beam_type(),
1394            Some(BeamType::Continue)
1395        );
1396        // One pointing into a beam that ends is left alone.
1397        let mut run = vec![partial(BeamDirection::Right), filled(BeamType::Stop)];
1398        merge_connecting_partial_beams(&mut run);
1399        assert_eq!(
1400            run[0].as_ref().unwrap().beams()[0].beam_type(),
1401            Some(BeamType::PartialBeam)
1402        );
1403    }
1404
1405    /// music21's own `setTextAndSyllabic` examples.
1406    #[test]
1407    fn text_and_syllabic_are_read_from_the_hyphens_unless_told_not_to() {
1408        use super::{Lyric, Syllabic};
1409
1410        let mut lyric = Lyric::unsung();
1411        lyric.set_text_and_syllabic("hel-", false);
1412        assert_eq!(
1413            (lyric.text(), lyric.syllabic()),
1414            ("hel".to_string(), Syllabic::Begin)
1415        );
1416        lyric.set_text_and_syllabic("-lo", false);
1417        assert_eq!(
1418            (lyric.text(), lyric.syllabic()),
1419            ("lo".to_string(), Syllabic::End)
1420        );
1421        lyric.set_text_and_syllabic("the", false);
1422        assert_eq!(
1423            (lyric.text(), lyric.syllabic()),
1424            ("the".to_string(), Syllabic::Single)
1425        );
1426
1427        let mut raw = Lyric::unsung();
1428        raw.set_text_and_syllabic("hel-", true);
1429        assert_eq!(
1430            (raw.text(), raw.syllabic()),
1431            ("hel-".to_string(), Syllabic::Single)
1432        );
1433        raw.set_syllabic(Syllabic::Begin);
1434        raw.set_text_and_syllabic("-lo", true);
1435        assert_eq!(
1436            (raw.text(), raw.syllabic()),
1437            ("-lo".to_string(), Syllabic::Begin)
1438        );
1439    }
1440
1441    #[test]
1442    fn beams_fill_one_level_for_each_flag() {
1443        // music21's own example: a sixteenth is beamed at two levels.
1444        let mut beams = Beams::new();
1445        beams
1446            .fill(DurationType::Sixteenth, Some(BeamType::Start))
1447            .unwrap();
1448        assert_eq!(beams.len(), 2);
1449        assert_eq!(
1450            beams.types(),
1451            [Some(BeamType::Start), Some(BeamType::Start)]
1452        );
1453        assert_eq!(beams.numbers(), [Some(1), Some(2)]);
1454        assert_eq!(
1455            beams.to_string(),
1456            "<music21.beam.Beam 1/start>/<music21.beam.Beam 2/start>"
1457        );
1458
1459        beams.set_all(BeamType::Stop, None);
1460        assert_eq!(beams.types(), [Some(BeamType::Stop), Some(BeamType::Stop)]);
1461        assert_eq!(
1462            beams.by_number(1).and_then(Beam::beam_type),
1463            Some(BeamType::Stop)
1464        );
1465
1466        // Counted but not decided: `fill` alone says how many lines there
1467        // are and leaves what each does unsaid, as music21 does.
1468        let mut counted = Beams::new();
1469        counted.fill(DurationType::Sixteenth, None).unwrap();
1470        assert_eq!(counted.types(), [None, None]);
1471        assert_eq!(
1472            counted.to_string(),
1473            "<music21.beam.Beam 1/None>/<music21.beam.Beam 2/None>"
1474        );
1475        assert!(Beams::new().fill_levels(12, None).is_err());
1476        assert!(beams.set_by_number(3, BeamType::Start, None).is_err());
1477
1478        // A quarter carries no beam at all, and music21 refuses to fill one.
1479        assert!(Beams::new().fill(DurationType::Quarter, None).is_err());
1480
1481        // A stub says which way it points.
1482        let mut stub = Beams::new();
1483        stub.append(BeamType::PartialBeam, Some(BeamDirection::Left));
1484        assert_eq!(stub.to_string(), "<music21.beam.Beam 1/partial/left>");
1485    }
1486    use super::*;
1487
1488    #[test]
1489    fn tie_types_round_trip_and_reject_others() {
1490        for tie_type in TieType::ALL {
1491            assert_eq!(TieType::from_name(tie_type.as_str()).unwrap(), tie_type);
1492        }
1493        assert_eq!(Tie::default().tie_type(), TieType::Start);
1494        assert_eq!(Tie::from_name("stop").unwrap().to_string(), "Tie stop");
1495        let error = TieType::from_name("hello").unwrap_err().to_string();
1496        assert!(error.contains("Type must be one of"), "{error}");
1497        assert!(error.ends_with("not hello"), "{error}");
1498    }
1499
1500    #[test]
1501    fn tie_carries_style_and_placement() {
1502        let mut tie = Tie::new(TieType::Continue);
1503        assert_eq!(tie.style(), TieStyle::Normal);
1504        assert_eq!(tie.placement(), None);
1505        tie.set_style(TieStyle::Dashed);
1506        tie.set_placement(Some(Placement::Above));
1507        assert_eq!(tie.style().as_str(), "dashed");
1508        assert_eq!(tie.placement().map(Placement::as_str), Some("above"));
1509        assert!(TieStyle::from_name("wavy").is_err());
1510        assert!(Placement::from_name("sideways").is_err());
1511    }
1512
1513    #[test]
1514    fn notehead_and_stem_names_round_trip() {
1515        for notehead in Notehead::ALL {
1516            assert_eq!(Notehead::from_name(notehead.as_str()).unwrap(), notehead);
1517        }
1518        for direction in StemDirection::ALL {
1519            assert_eq!(
1520                StemDirection::from_name(direction.as_str()).unwrap(),
1521                direction
1522            );
1523        }
1524        assert_eq!(Notehead::default(), Notehead::Normal);
1525        // "none" is an input spelling of "noStem", never a state of its own.
1526        assert_eq!(
1527            StemDirection::from_name("none").unwrap(),
1528            StemDirection::NoStem
1529        );
1530        assert_eq!(StemDirection::NAMES.len(), StemDirection::ALL.len() + 1);
1531        assert_eq!(StemDirection::default(), StemDirection::Unspecified);
1532        assert_eq!(Notehead::Diamond.to_string(), "diamond");
1533        assert_eq!(StemDirection::NoStem.to_string(), "noStem");
1534        assert!(Notehead::from_name("blah").is_err());
1535        assert!(StemDirection::from_name("sideways").is_err());
1536    }
1537
1538    #[test]
1539    fn lyric_hyphens_say_where_the_syllable_falls() {
1540        let cases = [
1541            ("hello", Syllabic::Single, "hello"),
1542            ("dic-", Syllabic::Begin, "dic"),
1543            ("-ci-", Syllabic::Middle, "ci"),
1544            ("-us", Syllabic::End, "us"),
1545        ];
1546        for (raw, syllabic, text) in cases {
1547            let lyric = Lyric::from_raw_text(raw);
1548            assert_eq!(lyric.syllabic(), syllabic, "{raw}");
1549            assert_eq!(lyric.text(), text, "{raw}");
1550            assert_eq!(lyric.raw_text(), raw, "{raw}");
1551        }
1552        let mut lyric = Lyric::new("shine");
1553        assert_eq!(lyric.number(), 1);
1554        assert_eq!(lyric.identifier(), "1");
1555        lyric.set_number(3);
1556        assert_eq!(lyric.identifier(), "3");
1557        lyric.set_identifier(Some("chorus".to_string()));
1558        assert_eq!(lyric.identifier(), "chorus");
1559        // Any whole number, as music21 allows: a verse the score named
1560        // with something that is not a number at all is numbered nought.
1561        lyric.set_number(0);
1562        assert_eq!(lyric.number(), 0);
1563        assert_eq!(lyric.to_string(), "shine");
1564        assert_eq!(Syllabic::from_name("middle").unwrap(), Syllabic::Middle);
1565        assert!(Syllabic::from_name("half").is_err());
1566    }
1567
1568    #[test]
1569    fn a_composite_lyric_reads_as_its_components_run_together() {
1570        // music21's own example: "bianco" sung as "co" then "e".
1571        let mut co = Lyric::new("co");
1572        co.set_syllabic(Syllabic::End);
1573        let mut e = Lyric::new("e");
1574        e.set_syllabic(Syllabic::Single);
1575
1576        let mut bianco = Lyric::new("");
1577        assert!(!bianco.is_composite());
1578        bianco.set_components(vec![co, e]);
1579        assert!(bianco.is_composite());
1580        assert_eq!(bianco.text(), "co e");
1581        assert_eq!(bianco.syllabic(), Syllabic::Composite);
1582        // The first component leads with a hyphen; the last ends the word.
1583        assert_eq!(bianco.raw_text(), "-co e");
1584
1585        // An elision joins the two rather than a space, and a middle
1586        // syllable at the end leaves the word open.
1587        let mut components = bianco.components().to_vec();
1588        components[1].set_elision_before("_");
1589        components[1].set_syllabic(Syllabic::Middle);
1590        bianco.set_components(components);
1591        assert_eq!(bianco.text(), "co_e");
1592        assert_eq!(bianco.raw_text(), "-co_e-");
1593
1594        // Setting the text outright is music21's way of un-compositing.
1595        bianco.set_text("bianco");
1596        assert!(!bianco.is_composite());
1597        assert_eq!(bianco.text(), "bianco");
1598        assert_eq!(bianco.syllabic(), Syllabic::Single);
1599    }
1600
1601    #[test]
1602    fn an_identifier_is_only_there_when_it_was_named() {
1603        let mut lyric = Lyric::new("shine");
1604        assert_eq!(lyric.explicit_identifier(), None);
1605        assert_eq!(lyric.identifier(), "1");
1606        lyric.set_identifier(Some("chorus".to_string()));
1607        assert_eq!(lyric.explicit_identifier(), Some("chorus"));
1608    }
1609
1610    #[test]
1611    fn a_lone_hyphen_is_its_own_syllable() {
1612        // "-" is a beginning-of-word marker in music21, not an end marker
1613        // with empty text, so the trailing hyphen is only stripped when
1614        // something else is there to strip it from.
1615        let lyric = Lyric::from_raw_text("-");
1616        assert_eq!(lyric.syllabic(), Syllabic::End);
1617        assert_eq!(lyric.text(), "");
1618    }
1619}