Skip to main content

music21_rs/chord/
guitar.rs

1use super::Chord;
2use crate::{
3    defaults::{FloatType, IntegerType},
4    pitch::Pitch,
5};
6use std::collections::BTreeSet;
7
8const STANDARD_TUNING: [&str; 6] = ["E2", "A2", "D3", "G3", "B3", "E4"];
9const MAX_FRET: u8 = 12;
10const MAX_FRET_SPAN: u8 = 4;
11
12/// Open-string pitch data for a guitar tuning.
13///
14/// Tunings are ordered from low string to high string and use concrete pitches,
15/// not just pitch classes, so fingering generation can respect octaves.
16#[derive(Clone, Debug, Eq, PartialEq)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18#[must_use]
19pub struct GuitarTuningString {
20    /// Open-string pitch name, including octave.
21    pub name: String,
22    /// Open-string pitch space, where C4 is 60.
23    pub pitch_space: IntegerType,
24    /// Open-string pitch class.
25    pub pitch_class: u8,
26}
27
28/// Guitar tuning used for fingering generation.
29///
30/// Strings are ordered from low to high, for example standard six-string guitar
31/// tuning is `["E2", "A2", "D3", "G3", "B3", "E4"]`.
32#[derive(Clone, Debug, Eq, PartialEq)]
33#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
34#[must_use]
35pub struct GuitarTuning {
36    strings: Vec<GuitarTuningString>,
37}
38
39impl GuitarTuning {
40    /// Builds a tuning from low-to-high open-string pitch names.
41    pub fn new<I, S>(strings: I) -> crate::Result<Self>
42    where
43        I: IntoIterator<Item = S>,
44        S: AsRef<str>,
45    {
46        let strings = strings
47            .into_iter()
48            .map(|name| {
49                let pitch = Pitch::from_name(name.as_ref())?;
50                let pitch_space = pitch_space(&pitch)?;
51                Ok(GuitarTuningString {
52                    name: pitch.name_with_octave(),
53                    pitch_space,
54                    pitch_class: pitch_class(&pitch),
55                })
56            })
57            .collect::<crate::Result<Vec<_>>>()?;
58
59        if strings.is_empty() {
60            return Err(crate::Error::Chord(
61                "guitar tuning must contain at least one string".to_string(),
62            ));
63        }
64        if strings.len() > u8::MAX as usize {
65            return Err(crate::Error::Chord(
66                "guitar tuning cannot contain more than 255 strings".to_string(),
67            ));
68        }
69
70        Ok(Self { strings })
71    }
72
73    /// Returns standard six-string guitar tuning.
74    pub fn standard() -> Self {
75        Self::new(STANDARD_TUNING).expect("standard guitar tuning should be valid")
76    }
77
78    /// Returns the tuning strings from low to high.
79    pub fn strings(&self) -> &[GuitarTuningString] {
80        &self.strings
81    }
82
83    fn len(&self) -> usize {
84        self.strings.len()
85    }
86}
87
88impl Default for GuitarTuning {
89    fn default() -> Self {
90        Self::standard()
91    }
92}
93
94/// One string in a suggested guitar fingering.
95#[derive(Clone, Debug, Eq, PartialEq)]
96#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
97#[must_use]
98pub struct GuitarStringFingering {
99    /// Guitar string number, where the lowest string has the highest number.
100    pub string_number: u8,
101    /// Open-string pitch name, including octave.
102    pub string_name: String,
103    /// Open-string pitch space, where C4 is 60.
104    pub open_pitch_space: IntegerType,
105    /// Open-string pitch class.
106    pub open_pitch_class: u8,
107    /// Fret to play, or `None` for a muted string.
108    pub fret: Option<u8>,
109    /// Suggested fretting finger, where `1` is index and `4` is pinky.
110    ///
111    /// Open and muted strings do not use a finger.
112    pub finger: Option<u8>,
113    /// Sounding pitch space, or `None` for a muted string.
114    pub pitch_space: Option<IntegerType>,
115    /// Sounding pitch class, or `None` for a muted string.
116    pub pitch_class: Option<u8>,
117}
118
119/// A suggested guitar fingering for a chord.
120#[derive(Clone, Debug, Eq, PartialEq)]
121#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
122#[must_use]
123pub struct GuitarFingering {
124    /// String fingerings from low string to high string.
125    pub strings: Vec<GuitarStringFingering>,
126    /// Lowest fretted position in the shape. `0` means the shape uses open strings.
127    pub base_fret: u8,
128    /// Distance between the lowest and highest fretted notes.
129    pub fret_span: u8,
130    /// Pitch spaces sounded by the fingering.
131    pub covered_pitch_spaces: Vec<IntegerType>,
132    /// Chord pitch spaces not present in the fingering.
133    pub omitted_pitch_spaces: Vec<IntegerType>,
134    /// Pitch classes sounded by the fingering.
135    pub covered_pitch_classes: Vec<u8>,
136    /// Chord pitch classes not present in the fingering.
137    pub omitted_pitch_classes: Vec<u8>,
138}
139
140#[derive(Clone, Debug)]
141struct StringChoice {
142    fret: Option<u8>,
143    pitch_space: Option<IntegerType>,
144    pitch_class: Option<u8>,
145}
146
147#[derive(Clone, Debug)]
148struct Candidate {
149    fingering: GuitarFingering,
150    score: usize,
151}
152
153#[derive(Clone, Debug)]
154struct Barre {
155    fret: u8,
156    start: usize,
157    end: usize,
158    covers: Vec<usize>,
159}
160
161#[derive(Clone, Debug)]
162struct FingerGroup {
163    fret: u8,
164    start: usize,
165    covers: Vec<usize>,
166}
167
168pub(crate) fn suggested_guitar_fingering(chord: &Chord) -> Option<GuitarFingering> {
169    suggested_guitar_fingering_with_tuning(chord, &GuitarTuning::standard())
170}
171
172pub(crate) fn suggested_guitar_fingering_with_tuning(
173    chord: &Chord,
174    tuning: &GuitarTuning,
175) -> Option<GuitarFingering> {
176    if chord.iter_pitches().any(|pitch| {
177        let ps = pitch.ps();
178        (ps - ps.round()).abs() > FloatType::EPSILON
179    }) {
180        return None;
181    }
182
183    let target_pitch_spaces = chord
184        .iter_pitches()
185        .map(pitch_space)
186        .collect::<crate::Result<BTreeSet<_>>>()
187        .ok()?;
188    if target_pitch_spaces.is_empty() {
189        return None;
190    }
191    let target_pitch_classes = chord.pitch_classes().into_iter().collect::<BTreeSet<_>>();
192
193    let root_pitch_class = chord
194        .root_pitch_name()
195        .and_then(|name| Pitch::from_name(name).ok())
196        .map(|pitch| pitch_class(&pitch));
197
198    let options = tuning
199        .strings()
200        .iter()
201        .map(|string| string_choices(string.pitch_space, &target_pitch_classes))
202        .collect::<Vec<_>>();
203
204    let mut best: Option<Candidate> = None;
205    let mut current = Vec::with_capacity(tuning.len());
206    collect_candidates(
207        &options,
208        0,
209        &mut current,
210        &target_pitch_spaces,
211        &target_pitch_classes,
212        root_pitch_class,
213        tuning,
214        &mut best,
215    );
216
217    best.map(|candidate| candidate.fingering)
218}
219
220/// Frets on one string that sound a chord tone, plus the option of muting it.
221///
222/// Matches by **pitch class**, not by absolute pitch. A guitar voicing uses
223/// whatever octave of a chord tone falls under the hand — requiring the exact
224/// written octave is what confined this to the top three strings and made every
225/// shape a high-position stack with the bass strings muted.
226///
227/// The whole neck is enumerated once and each shape's position is derived from
228/// the frets it uses, rather than sweeping a window across the neck: the
229/// windows overlap heavily, so sweeping found every shape five times over.
230fn string_choices(
231    open_pitch_space: IntegerType,
232    target_pitch_classes: &BTreeSet<u8>,
233) -> Vec<StringChoice> {
234    let mut choices = vec![StringChoice {
235        fret: None,
236        pitch_space: None,
237        pitch_class: None,
238    }];
239
240    for fret in 0..=MAX_FRET {
241        let pitch_space = open_pitch_space + IntegerType::from(fret);
242        let pitch_class = pitch_space.rem_euclid(12) as u8;
243        if target_pitch_classes.contains(&pitch_class) {
244            choices.push(StringChoice {
245                fret: Some(fret),
246                pitch_space: Some(pitch_space),
247                pitch_class: Some(pitch_class),
248            });
249        }
250    }
251
252    choices
253}
254
255fn collect_candidates(
256    options: &[Vec<StringChoice>],
257    string_index: usize,
258    current: &mut Vec<StringChoice>,
259    target_pitch_spaces: &BTreeSet<IntegerType>,
260    target_pitch_classes: &BTreeSet<u8>,
261    root_pitch_class: Option<u8>,
262    tuning: &GuitarTuning,
263    best: &mut Option<Candidate>,
264) {
265    if string_index == options.len() {
266        let bound = best.as_ref().map_or(usize::MAX, |best| best.score);
267        if let Some(candidate) = score_candidate(
268            current,
269            target_pitch_spaces,
270            target_pitch_classes,
271            root_pitch_class,
272            tuning,
273            bound,
274        ) && best
275            .as_ref()
276            .is_none_or(|current| candidate.score < current.score)
277        {
278            *best = Some(candidate);
279        }
280        return;
281    }
282
283    for choice in &options[string_index] {
284        // Prune on the fret span before recursing. A partial shape whose
285        // fretted notes already exceed a hand's reach cannot be rescued by
286        // anything the remaining strings do, and this is what keeps the search
287        // small now that every octave of a chord tone is a candidate.
288        if let Some(fret) = choice.fret.filter(|fret| *fret > 0) {
289            let (mut low, mut high) = (fret, fret);
290            for played in current.iter().filter_map(|c| c.fret.filter(|f| *f > 0)) {
291                low = low.min(played);
292                high = high.max(played);
293            }
294            if high - low > MAX_FRET_SPAN {
295                continue;
296            }
297        }
298
299        current.push(choice.clone());
300        collect_candidates(
301            options,
302            string_index + 1,
303            current,
304            target_pitch_spaces,
305            target_pitch_classes,
306            root_pitch_class,
307            tuning,
308            best,
309        );
310        current.pop();
311    }
312}
313
314fn unreachable_same_fret_pairs(fretted_positions: &[(usize, u8)]) -> usize {
315    let mut count = 0;
316    for (index, (left_string, fret)) in fretted_positions.iter().enumerate() {
317        for (right_string, right_fret) in fretted_positions.iter().skip(index + 1) {
318            if right_fret != fret || right_string.saturating_sub(*left_string) < 3 {
319                continue;
320            }
321            let crossed = fretted_positions.iter().any(|(between, between_fret)| {
322                between > left_string && between < right_string && between_fret > fret
323            });
324            if crossed {
325                count += 1;
326            }
327        }
328    }
329    count
330}
331
332fn score_candidate(
333    choices: &[StringChoice],
334    target_pitch_spaces: &BTreeSet<IntegerType>,
335    target_pitch_classes: &BTreeSet<u8>,
336    root_pitch_class: Option<u8>,
337    tuning: &GuitarTuning,
338    bound: usize,
339) -> Option<Candidate> {
340    let sounding_indices = choices
341        .iter()
342        .enumerate()
343        .filter_map(|(index, choice)| choice.fret.map(|_| index))
344        .collect::<Vec<_>>();
345    if sounding_indices.is_empty() {
346        return None;
347    }
348
349    let covered_pitch_spaces = choices
350        .iter()
351        .filter_map(|choice| choice.pitch_space)
352        .collect::<BTreeSet<_>>();
353    let omitted_pitch_spaces = target_pitch_spaces
354        .difference(&covered_pitch_spaces)
355        .copied()
356        .collect::<Vec<_>>();
357    let covered_pitch_classes = choices
358        .iter()
359        .filter_map(|choice| choice.pitch_class)
360        .collect::<BTreeSet<_>>();
361    let omitted_pitch_classes = target_pitch_classes
362        .difference(&covered_pitch_classes)
363        .copied()
364        .collect::<Vec<_>>();
365    let fretted = choices
366        .iter()
367        .filter_map(|choice| choice.fret.filter(|fret| *fret > 0))
368        .collect::<Vec<_>>();
369    let base_fret = fretted.iter().copied().min().unwrap_or(0);
370    let fret_span = fretted
371        .iter()
372        .copied()
373        .max()
374        .zip(fretted.iter().copied().min())
375        .map(|(max, min)| max - min)
376        .unwrap_or(0);
377    let muted_count = choices
378        .iter()
379        .filter(|choice| choice.fret.is_none())
380        .count();
381    let internal_mutes = internal_muted_string_count(choices, &sounding_indices);
382    let bass_pitch_class = sounding_indices
383        .first()
384        .and_then(|index| choices[*index].pitch_class);
385    let root_is_missing =
386        root_pitch_class.is_some_and(|root| !covered_pitch_classes.contains(&root));
387    let bass_is_not_root =
388        root_pitch_class.is_some_and(|root| bass_pitch_class.is_some_and(|bass| bass != root));
389    let duplicate_count = sounding_indices
390        .len()
391        .saturating_sub(covered_pitch_spaces.len());
392    let fret_sum = fretted.iter().map(|fret| *fret as usize).sum::<usize>();
393
394    // Scored as a guitarist would judge a shape, not as a pitch-set match.
395    //
396    // Missing a chord *tone* is the real fault; missing the written octave of
397    // one is not, because a voicing is free to place chord tones wherever they
398    // fall under the hand. Weighting octaves instead is what produced
399    // three-string shapes high up the neck that dropped sevenths.
400    let open_strings = choices
401        .iter()
402        .filter(|choice| choice.fret == Some(0))
403        .count();
404
405    // Everything above is cheap. Bail before assigning fingers if the shape
406    // cannot beat the incumbent anyway — that assignment is the expensive part
407    // of the search, and most shapes lose on chord tones long before it.
408    let cheap_floor = omitted_pitch_classes.len() * 1000
409        + usize::from(root_is_missing) * 400
410        + internal_mutes * 120
411        + muted_count * 30;
412    // The final score can still fall below this floor, because open strings are
413    // rewarded by subtraction, so allow for the largest bonus a six-string shape
414    // could earn. Pruning tighter than this would drop voicings that win.
415    const MAX_OPEN_BONUS: usize = 6 * 12;
416    if cheap_floor.saturating_sub(MAX_OPEN_BONUS) >= bound {
417        return None;
418    }
419
420    let finger_assignment = finger_assignment(choices)?;
421
422    let unreachable = unreachable_same_fret_pairs(
423        &choices
424            .iter()
425            .enumerate()
426            .filter_map(|(index, choice)| {
427                choice
428                    .fret
429                    .filter(|fret| *fret > 0)
430                    .map(|fret| (index, fret))
431            })
432            .collect::<Vec<_>>(),
433    );
434
435    let penalties = unreachable * 300
436        + omitted_pitch_classes.len() * 1000
437        + usize::from(root_is_missing) * 400
438        + internal_mutes * 120
439        + muted_count * 30
440        + usize::from(bass_is_not_root) * 60
441        + fret_span as usize * 12
442        + base_fret as usize * 6
443        + fretted.len() * 2
444        + duplicate_count
445        + fret_sum;
446
447    // Open strings ring and cost no finger, so first-position shapes should
448    // beat the equivalent barre when both are available. That only holds near
449    // the nut: an open string mixed into a shape held at the seventh fret is a
450    // stretch across the neck, not a convenience, so the reward tapers off and
451    // then becomes a penalty.
452    let score = if base_fret <= 2 {
453        penalties.saturating_sub(open_strings * 12)
454    } else {
455        penalties + open_strings * 20
456    };
457
458    let strings = choices
459        .iter()
460        .enumerate()
461        .map(|(index, choice)| {
462            let tuning_string = &tuning.strings()[index];
463            GuitarStringFingering {
464                string_number: (tuning.len() - index) as u8,
465                string_name: tuning_string.name.clone(),
466                open_pitch_space: tuning_string.pitch_space,
467                open_pitch_class: tuning_string.pitch_class,
468                fret: choice.fret,
469                finger: finger_assignment[index],
470                pitch_space: choice.pitch_space,
471                pitch_class: choice.pitch_class,
472            }
473        })
474        .collect::<Vec<_>>();
475
476    Some(Candidate {
477        fingering: GuitarFingering {
478            strings,
479            base_fret,
480            fret_span,
481            covered_pitch_spaces: covered_pitch_spaces.into_iter().collect(),
482            omitted_pitch_spaces,
483            covered_pitch_classes: covered_pitch_classes.into_iter().collect(),
484            omitted_pitch_classes,
485        },
486        score,
487    })
488}
489
490fn finger_assignment(choices: &[StringChoice]) -> Option<Vec<Option<u8>>> {
491    let fretted_positions = choices
492        .iter()
493        .enumerate()
494        .filter_map(|(string_index, choice)| {
495            choice
496                .fret
497                .filter(|fret| *fret > 0)
498                .map(|fret| (string_index, fret))
499        })
500        .collect::<Vec<_>>();
501
502    if fretted_positions.is_empty() {
503        return Some(vec![None; choices.len()]);
504    }
505
506    if !fret_span_is_reachable(&fretted_positions) {
507        return None;
508    }
509
510    if fretted_positions.len() <= 4 {
511        let groups = fretted_positions
512            .iter()
513            .enumerate()
514            .map(|(position_index, (string_index, fret))| FingerGroup {
515                fret: *fret,
516                start: *string_index,
517                covers: vec![position_index],
518            })
519            .collect::<Vec<_>>();
520        return assignment_from_groups(groups, choices.len(), &fretted_positions);
521    }
522
523    let barres = possible_barres(choices, &fretted_positions);
524    let mut best: Option<(usize, usize, Vec<FingerGroup>)> = None;
525    choose_barres(&barres, 0, Vec::new(), &fretted_positions, &mut best);
526
527    let (_, _, groups) = best?;
528    assignment_from_groups(groups, choices.len(), &fretted_positions)
529}
530
531fn fret_span_is_reachable(fretted_positions: &[(usize, u8)]) -> bool {
532    let Some(lowest) = fretted_positions.iter().map(|(_, fret)| *fret).min() else {
533        return true;
534    };
535    let highest = fretted_positions
536        .iter()
537        .map(|(_, fret)| *fret)
538        .max()
539        .unwrap_or(lowest);
540    let span = highest - lowest;
541
542    span <= if lowest >= 5 { 4 } else { 3 }
543}
544
545fn assignment_from_groups(
546    mut groups: Vec<FingerGroup>,
547    string_count: usize,
548    fretted_positions: &[(usize, u8)],
549) -> Option<Vec<Option<u8>>> {
550    if groups.len() > 4 {
551        return None;
552    }
553
554    groups.sort_by(|left, right| {
555        left.fret
556            .cmp(&right.fret)
557            .then_with(|| left.start.cmp(&right.start))
558    });
559
560    let mut assignment = vec![None; string_count];
561    for (finger_index, group) in groups.into_iter().enumerate() {
562        let finger = (finger_index + 1) as u8;
563        for position_index in group.covers {
564            let string_index = fretted_positions[position_index].0;
565            assignment[string_index] = Some(finger);
566        }
567    }
568    Some(assignment)
569}
570
571fn possible_barres(choices: &[StringChoice], fretted_positions: &[(usize, u8)]) -> Vec<Barre> {
572    let frets = fretted_positions
573        .iter()
574        .map(|(_, fret)| *fret)
575        .collect::<BTreeSet<_>>();
576    let mut barres = Vec::new();
577
578    for fret in frets {
579        for start in 0..choices.len() {
580            for end in (start + 1)..choices.len() {
581                if !barre_range_is_clear(choices, fret, start, end) {
582                    continue;
583                }
584
585                let covers = fretted_positions
586                    .iter()
587                    .enumerate()
588                    .filter_map(|(position_index, (string_index, position_fret))| {
589                        (*position_fret == fret && (start..=end).contains(string_index))
590                            .then_some(position_index)
591                    })
592                    .collect::<Vec<_>>();
593
594                if covers.len() >= 2 {
595                    barres.push(Barre {
596                        fret,
597                        start,
598                        end,
599                        covers,
600                    });
601                }
602            }
603        }
604    }
605
606    barres
607}
608
609fn barre_range_is_clear(choices: &[StringChoice], fret: u8, start: usize, end: usize) -> bool {
610    choices[start..=end]
611        .iter()
612        .all(|choice| choice.fret.is_some_and(|string_fret| string_fret >= fret))
613}
614
615fn choose_barres(
616    barres: &[Barre],
617    index: usize,
618    selected: Vec<Barre>,
619    fretted_positions: &[(usize, u8)],
620    best: &mut Option<(usize, usize, Vec<FingerGroup>)>,
621) {
622    if index == barres.len() {
623        let Some(groups) = finger_groups_from_barres(selected, fretted_positions) else {
624            return;
625        };
626        if groups.len() > 4 {
627            return;
628        }
629
630        let barre_span = groups
631            .iter()
632            .filter(|group| group.covers.len() > 1)
633            .map(|group| group.covers.len())
634            .sum::<usize>();
635        let key = (groups.len(), usize::MAX - barre_span);
636        if best
637            .as_ref()
638            .is_none_or(|(best_count, best_barre_score, _)| key < (*best_count, *best_barre_score))
639        {
640            *best = Some((key.0, key.1, groups));
641        }
642        return;
643    }
644
645    choose_barres(barres, index + 1, selected.clone(), fretted_positions, best);
646
647    let mut next = selected;
648    next.push(barres[index].clone());
649    choose_barres(barres, index + 1, next, fretted_positions, best);
650}
651
652fn finger_groups_from_barres(
653    barres: Vec<Barre>,
654    fretted_positions: &[(usize, u8)],
655) -> Option<Vec<FingerGroup>> {
656    let mut covered = vec![false; fretted_positions.len()];
657    let mut groups = Vec::new();
658
659    for barre in barres {
660        if barre.covers.iter().any(|position| covered[*position]) {
661            return None;
662        }
663        for position in &barre.covers {
664            covered[*position] = true;
665        }
666        groups.push(FingerGroup {
667            fret: barre.fret,
668            start: barre.start.min(barre.end),
669            covers: barre.covers,
670        });
671    }
672
673    for (position_index, (string_index, fret)) in fretted_positions.iter().enumerate() {
674        if !covered[position_index] {
675            groups.push(FingerGroup {
676                fret: *fret,
677                start: *string_index,
678                covers: vec![position_index],
679            });
680        }
681    }
682
683    Some(groups)
684}
685
686fn internal_muted_string_count(choices: &[StringChoice], sounding_indices: &[usize]) -> usize {
687    let Some(first) = sounding_indices.first() else {
688        return 0;
689    };
690    let Some(last) = sounding_indices.last() else {
691        return 0;
692    };
693
694    choices[*first..=*last]
695        .iter()
696        .filter(|choice| choice.fret.is_none())
697        .count()
698}
699
700fn pitch_class(pitch: &Pitch) -> u8 {
701    (pitch.ps().round() as IntegerType).rem_euclid(12) as u8
702}
703
704fn pitch_space(pitch: &Pitch) -> crate::Result<IntegerType> {
705    let pitch_space = pitch.ps();
706    let rounded = pitch_space.round();
707    if (pitch_space - rounded).abs() > FloatType::EPSILON {
708        return Err(crate::Error::Chord(
709            "guitar fingering requires chromatic pitches".to_string(),
710        ));
711    }
712    Ok(rounded as IntegerType)
713}
714
715#[cfg(test)]
716mod tests {
717    use super::*;
718
719    #[test]
720    fn a_tuning_is_built_from_its_open_strings() {
721        let standard = GuitarTuning::default();
722        let custom = GuitarTuning::new(["E2", "A2", "D3", "G3", "B3", "E4"]).unwrap();
723        assert_eq!(standard.strings().len(), custom.strings().len());
724        assert!(GuitarTuning::new(["E2", "H2"]).is_err());
725    }
726
727    fn choices(frets: &[Option<u8>]) -> Vec<StringChoice> {
728        frets
729            .iter()
730            .copied()
731            .map(|fret| StringChoice {
732                fret,
733                pitch_space: None,
734                pitch_class: None,
735            })
736            .collect()
737    }
738
739    #[test]
740    fn finger_assignment_rejects_more_than_four_independent_fingers() {
741        assert!(
742            finger_assignment(&choices(&[
743                Some(1),
744                Some(2),
745                Some(3),
746                Some(4),
747                Some(5),
748                None
749            ]))
750            .is_none()
751        );
752    }
753
754    #[test]
755    fn finger_assignment_accepts_barre_shapes() {
756        let assignment = finger_assignment(&choices(&[
757            Some(1),
758            Some(3),
759            Some(3),
760            Some(2),
761            Some(1),
762            Some(1),
763        ]))
764        .unwrap();
765
766        assert_eq!(
767            assignment.iter().flatten().collect::<BTreeSet<_>>().len(),
768            3
769        );
770    }
771
772    #[test]
773    fn finger_assignment_rejects_low_position_five_fret_stretches() {
774        assert!(
775            finger_assignment(&choices(&[Some(1), Some(2), Some(3), Some(5), None, None]))
776                .is_none()
777        );
778    }
779
780    #[test]
781    fn finger_assignment_allows_higher_position_extended_reaches() {
782        let assignment =
783            finger_assignment(&choices(&[Some(5), Some(7), Some(8), Some(9), None, None])).unwrap();
784
785        assert_eq!(assignment.iter().flatten().count(), 4);
786    }
787
788    #[test]
789    fn finger_assignment_does_not_barre_over_open_string() {
790        let assignment = finger_assignment(&choices(&[
791            Some(1),
792            Some(2),
793            Some(0),
794            Some(3),
795            Some(1),
796            Some(1),
797        ]))
798        .unwrap();
799
800        assert!(assignment.iter().flatten().collect::<BTreeSet<_>>().len() <= 4);
801        assert_ne!(assignment[0], assignment[4]);
802    }
803}