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