1use crate::{
2 error::{TabError, UnplayablePitch},
3 guitar::{Guitar, PitchFingering, generate_pitch_fingerings},
4 pitch::Pitch,
5};
6use average::Mean;
7use itertools::Itertools;
8use memoize::memoize;
9use ordered_float::OrderedFloat;
10use pathfinding::prelude::yen;
11use std::{collections::HashSet, rc::Rc};
12
13#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
19pub enum Line<T> {
20 MeasureBreak,
21 Rest,
22 Playable(T),
23}
24use Line::{MeasureBreak, Playable, Rest};
25
26#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
27enum Node {
28 Start,
29 Rest {
30 line_index: u16,
31 },
32 Playable {
33 line_index: u16,
34 scored_beat_fingering: Rc<ScoredBeatFingering>,
35 },
36}
37
38pub(crate) type PitchVec<T> = Vec<T>;
40pub type BeatVec<T> = Vec<T>;
42
43pub(crate) fn first_playable_index<T>(lines: &[Line<T>]) -> usize {
48 lines
49 .iter()
50 .position(|line| matches!(line, Playable(_)))
51 .unwrap_or(0)
52}
53
54#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
57pub(crate) struct ScoredBeatFingering {
58 beat_fingering: BeatVec<PitchFingering>,
59 avg_non_zero_fret: Option<OrderedFloat<f64>>,
60 non_zero_fret_span: u8,
61}
62impl ScoredBeatFingering {
63 pub(crate) fn new(beat_fingering_candidate: BeatVec<PitchFingering>) -> Self {
66 let avg_non_zero_fret = calc_avg_non_zero_fret(&beat_fingering_candidate);
67 let non_zero_fret_span = calc_fret_span(&beat_fingering_candidate).unwrap_or(0);
68
69 ScoredBeatFingering {
70 beat_fingering: beat_fingering_candidate,
71 avg_non_zero_fret,
72 non_zero_fret_span,
73 }
74 }
75}
76#[cfg(test)]
77mod test_create_scored_beat_fingering {
78 use super::*;
79 use crate::string_number::StringNumber;
80
81 #[test]
82 fn simple() {
83 let pitch_fingering_1 = PitchFingering {
84 pitch: Pitch::A0,
85 string_number: StringNumber::new(1).unwrap(),
86 fret: 2,
87 };
88
89 let ScoredBeatFingering {
90 beat_fingering,
91 avg_non_zero_fret,
92 non_zero_fret_span,
93 } = ScoredBeatFingering::new(vec![pitch_fingering_1]);
94
95 assert_eq!(beat_fingering, vec![pitch_fingering_1]);
96 assert_eq!(avg_non_zero_fret, Some(OrderedFloat(2.0)));
97 assert_eq!(non_zero_fret_span, 0);
98 }
99 #[test]
100 fn complex() {
101 let pitch_fingering_1 = PitchFingering {
102 pitch: Pitch::A0,
103 string_number: StringNumber::new(1).unwrap(),
104 fret: 2,
105 };
106 let pitch_fingering_2 = PitchFingering {
107 pitch: Pitch::B1,
108 string_number: StringNumber::new(2).unwrap(),
109 fret: 5,
110 };
111 let pitch_fingering_3 = PitchFingering {
112 pitch: Pitch::C2,
113 string_number: StringNumber::new(3).unwrap(),
114 fret: 0,
115 };
116 let pitch_fingering_4 = PitchFingering {
117 pitch: Pitch::D3,
118 string_number: StringNumber::new(4).unwrap(),
119 fret: 1,
120 };
121
122 let ScoredBeatFingering {
123 beat_fingering,
124 avg_non_zero_fret,
125 non_zero_fret_span,
126 } = ScoredBeatFingering::new(vec![
127 pitch_fingering_1,
128 pitch_fingering_2,
129 pitch_fingering_3,
130 pitch_fingering_4,
131 ]);
132
133 assert_eq!(
134 beat_fingering,
135 vec![
136 pitch_fingering_1,
137 pitch_fingering_2,
138 pitch_fingering_3,
139 pitch_fingering_4
140 ]
141 );
142 assert_eq!(avg_non_zero_fret, Some(OrderedFloat(8.0 / 3.0)));
143 assert_eq!(non_zero_fret_span, 4);
144 }
145}
146
147fn calc_avg_non_zero_fret(
148 beat_fingering_candidate: &[PitchFingering],
149) -> Option<OrderedFloat<f64>> {
150 let non_zero_fingerings = beat_fingering_candidate
151 .iter()
152 .filter(|fingering| fingering.fret != 0)
153 .map(|fingering| fingering.fret as f64)
154 .collect::<Mean>();
155
156 if non_zero_fingerings.is_empty() {
157 None
158 } else {
159 Some(OrderedFloat(non_zero_fingerings.mean()))
160 }
161}
162#[cfg(test)]
163mod test_calc_avg_non_zero_fret {
164 use super::*;
165 use crate::string_number::StringNumber;
166
167 #[test]
168 fn single_non_zero_fret() {
169 let pitch_fingering_1 = PitchFingering {
170 pitch: Pitch::A0,
171 string_number: StringNumber::new(1).unwrap(),
172 fret: 2,
173 };
174
175 assert_eq!(
176 calc_avg_non_zero_fret(&[pitch_fingering_1]),
177 Some(OrderedFloat(2.0))
178 );
179 }
180 #[test]
181 fn single_zero_fret() {
182 let pitch_fingering_1 = PitchFingering {
183 pitch: Pitch::A0,
184 string_number: StringNumber::new(1).unwrap(),
185 fret: 0,
186 };
187
188 assert_eq!(calc_avg_non_zero_fret(&[pitch_fingering_1]), None);
189 }
190 #[test]
191 fn multiple_zero_frets() {
192 let pitch_fingering_1 = PitchFingering {
193 pitch: Pitch::A0,
194 string_number: StringNumber::new(1).unwrap(),
195 fret: 0,
196 };
197 let pitch_fingering_2 = PitchFingering {
198 pitch: Pitch::B2,
199 string_number: StringNumber::new(2).unwrap(),
200 fret: 0,
201 };
202
203 assert_eq!(
204 calc_avg_non_zero_fret(&[pitch_fingering_1, pitch_fingering_2]),
205 None
206 );
207 }
208 #[test]
209 fn multiple_mixed_frets() {
210 let pitch_fingering_1 = PitchFingering {
211 pitch: Pitch::A0,
212 string_number: StringNumber::new(1).unwrap(),
213 fret: 2,
214 };
215 let pitch_fingering_2 = PitchFingering {
216 pitch: Pitch::B1,
217 string_number: StringNumber::new(2).unwrap(),
218 fret: 5,
219 };
220 let pitch_fingering_3 = PitchFingering {
221 pitch: Pitch::C2,
222 string_number: StringNumber::new(3).unwrap(),
223 fret: 0,
224 };
225 let pitch_fingering_4 = PitchFingering {
226 pitch: Pitch::D3,
227 string_number: StringNumber::new(4).unwrap(),
228 fret: 1,
229 };
230
231 assert_eq!(
232 calc_avg_non_zero_fret(&[
233 pitch_fingering_1,
234 pitch_fingering_2,
235 pitch_fingering_3,
236 pitch_fingering_4,
237 ]),
238 Some(OrderedFloat(8.0 / 3.0))
239 );
240 }
241}
242
243#[derive(Debug, Clone, Eq, PartialEq)]
245pub struct Arrangement {
246 pub(crate) lines: Vec<Line<BeatVec<PitchFingering>>>,
247 difficulty: i32,
248 max_fret_span: u8,
249}
250impl Arrangement {
251 #[must_use]
253 pub fn lines(&self) -> &[Line<BeatVec<PitchFingering>>] {
254 &self.lines
255 }
256
257 #[must_use]
261 pub fn max_fret_span(&self) -> u8 {
262 self.max_fret_span
263 }
264
265 #[must_use]
268 pub fn difficulty(&self) -> i32 {
269 self.difficulty
270 }
271}
272#[cfg(test)]
273mod test_max_fret_span {
274 use super::*;
275
276 #[test]
277 fn test_max_fret_span() {
278 let arrangement = Arrangement {
279 lines: vec![],
280 difficulty: 4,
281 max_fret_span: 5,
282 };
283 assert_eq!(arrangement.max_fret_span(), 5);
284 }
285}
286
287#[memoize(Capacity: 10)]
303pub fn create_arrangements(
304 guitar: Guitar,
305 input_lines: Vec<Line<BeatVec<Pitch>>>,
306 num_arrangements: crate::NumArrangements,
307 max_fret_span_filter: Option<u8>,
308) -> Result<Vec<Arrangement>, TabError> {
309 if input_lines.len() > crate::parser::MAX_INPUT_LINES {
313 return Err(TabError::InputTooManyLines {
314 max: crate::parser::MAX_INPUT_LINES as u32,
315 });
316 }
317
318 let input_playable_lines = input_lines
319 .iter()
320 .filter(|line| matches!(line, Line::Playable(_)))
321 .collect_vec();
322 if input_playable_lines.is_empty() {
323 let empty_arrangements = vec![
324 Arrangement {
325 lines: vec![],
326 difficulty: 0,
327 max_fret_span: 0,
328 };
329 num_arrangements.get() as usize
330 ];
331 return Ok(empty_arrangements);
332 }
333
334 let first_playable_index = first_playable_index(&input_lines);
335
336 let pitch_fingering_candidates: Vec<Line<BeatVec<PitchVec<PitchFingering>>>> =
340 validate_fingerings(&guitar, &input_lines)?
341 .into_iter()
342 .skip(first_playable_index)
343 .collect_vec();
344
345 let measure_break_indices: Vec<usize> = pitch_fingering_candidates
346 .iter()
347 .enumerate()
348 .filter(|(.., line_candidate)| matches!(line_candidate, MeasureBreak))
349 .map(|(line_index, ..)| line_index)
350 .collect_vec();
351
352 let path_node_groups: Vec<BeatVec<Node>> = pitch_fingering_candidates
353 .into_iter()
354 .filter(|line_candidate| !matches!(line_candidate, MeasureBreak))
355 .enumerate()
356 .map(|(line_index, line_candidate)| match line_candidate {
357 MeasureBreak => unreachable!("Measure breaks should have been filtered out."),
358 Rest => vec![Node::Rest {
361 line_index: line_index as u16,
362 }],
363 Playable(beat_fingerings_per_pitch) => {
364 generate_beat_fingerings(&beat_fingerings_per_pitch)
365 .into_iter()
366 .map(|pitch_fingering_group| Node::Playable {
367 line_index: line_index as u16,
368 scored_beat_fingering: Rc::new(ScoredBeatFingering::new(
369 pitch_fingering_group,
370 )),
371 })
372 .collect()
373 }
374 })
375 .collect::<Vec<_>>();
376
377 let num_path_node_groups = path_node_groups.len();
378
379 let path_nodes: Vec<Node> = path_node_groups.into_iter().flatten().collect_vec();
380
381 let path_results: Vec<(Vec<Node>, i32)> = yen(
382 &Node::Start,
383 |current_node| calc_next_nodes(current_node, &path_nodes),
384 |current_node| match current_node {
385 Node::Start => false,
386 Node::Rest { line_index } | Node::Playable { line_index, .. } => {
387 *line_index == (num_path_node_groups - 1) as u16
389 }
390 },
391 num_arrangements.get() as usize,
392 );
393 if path_results.is_empty() {
394 return Err(TabError::NoArrangementsFound);
395 }
396
397 let mut arrangements = path_results
398 .into_iter()
399 .map(|path_result| process_path(path_result.0, path_result.1, &measure_break_indices))
400 .collect_vec();
401
402 if let Some(max_span) = max_fret_span_filter {
403 arrangements.retain(|a| a.max_fret_span() <= max_span);
404 }
405
406 Ok(arrangements)
407}
408#[cfg(test)]
409mod test_create_arrangements {
410 use super::*;
411 use crate::NumArrangements;
412 use crate::parser::parse_lines;
413 use crate::string_number::StringNumber;
414
415 #[test]
416 fn unreachable_pitch_returns_unplayable_pitches_variant() {
417 let lines = parse_lines("A1".to_owned()).unwrap();
418 let n = NumArrangements::try_new(1).unwrap();
419 let err = create_arrangements(Guitar::default(), lines, n, None).unwrap_err();
420 match err {
421 TabError::UnplayablePitches { pitches } => {
422 assert_eq!(pitches.len(), 1);
423 assert_eq!(pitches[0].value, "A1");
424 assert_eq!(pitches[0].line, 1);
425 }
426 other => panic!("expected UnplayablePitches, got {other:?}"),
427 }
428 }
429
430 #[test]
431 fn empty_playable_beat_yields_no_arrangements() {
432 let input_pitches = vec![Line::Playable(vec![])];
436
437 let err = create_arrangements(
438 Guitar::default(),
439 input_pitches,
440 NumArrangements::try_new(1).unwrap(),
441 None,
442 )
443 .unwrap_err();
444
445 assert!(matches!(err, TabError::NoArrangementsFound), "got {err:?}");
446 }
447
448 #[test]
449 fn single_line_single_pitch() {
450 let input_pitches: Vec<Line<BeatVec<Pitch>>> = vec![Line::Playable(vec![Pitch::E4])];
451 let expected_arrangements: Vec<Arrangement> = vec![Arrangement {
452 lines: vec![Line::Playable(vec![PitchFingering {
453 pitch: Pitch::E4,
454 string_number: StringNumber::new(1).unwrap(),
455 fret: 0,
456 }])],
457 difficulty: 0,
458 max_fret_span: 0,
459 }];
460
461 let arrangements = create_arrangements(
462 Guitar::default(),
463 input_pitches,
464 NumArrangements::try_new(1).unwrap(),
465 None,
466 )
467 .unwrap();
468
469 assert_eq!(arrangements, expected_arrangements);
470 }
471 #[test]
472 fn single_line_single_pitch_multiple_arrangements() {
473 let input_pitches: Vec<Line<BeatVec<Pitch>>> = vec![Line::Playable(vec![Pitch::E4])];
474 let expected_arrangements: Vec<Arrangement> = vec![
475 Arrangement {
476 lines: vec![Line::Playable(vec![PitchFingering {
477 pitch: Pitch::E4,
478 string_number: StringNumber::new(1).unwrap(),
479 fret: 0,
480 }])],
481 difficulty: 0,
482 max_fret_span: 0,
483 },
484 Arrangement {
485 lines: vec![Line::Playable(vec![PitchFingering {
486 pitch: Pitch::E4,
487 string_number: StringNumber::new(2).unwrap(),
488 fret: 5,
489 }])],
490 difficulty: 5,
491 max_fret_span: 0,
492 },
493 Arrangement {
494 lines: vec![Line::Playable(vec![PitchFingering {
495 pitch: Pitch::E4,
496 string_number: StringNumber::new(3).unwrap(),
497 fret: 9,
498 }])],
499 difficulty: 9,
500 max_fret_span: 0,
501 },
502 Arrangement {
503 lines: vec![Line::Playable(vec![PitchFingering {
504 pitch: Pitch::E4,
505 string_number: StringNumber::new(4).unwrap(),
506 fret: 14,
507 }])],
508 difficulty: 14,
509 max_fret_span: 0,
510 },
511 ];
512
513 let arrangements = create_arrangements(
514 Guitar::default(),
515 input_pitches,
516 NumArrangements::try_new(10).unwrap(),
517 None,
518 )
519 .unwrap();
520
521 assert_eq!(arrangements, expected_arrangements);
522 }
523 #[test]
524 fn single_lines_all_variants() {
525 let input_pitches: Vec<Line<BeatVec<Pitch>>> = vec![
526 Line::Playable(vec![Pitch::E4]),
527 Line::Rest,
528 Line::MeasureBreak,
529 ];
530 let expected_arrangements: Vec<Arrangement> = vec![Arrangement {
531 lines: vec![
532 Line::Playable(vec![PitchFingering {
533 pitch: Pitch::E4,
534 string_number: StringNumber::new(1).unwrap(),
535 fret: 0,
536 }]),
537 Line::Rest,
538 Line::MeasureBreak,
539 ],
540 difficulty: 0,
541 max_fret_span: 0,
542 }];
543
544 let arrangements = create_arrangements(
545 Guitar::default(),
546 input_pitches,
547 NumArrangements::try_new(1).unwrap(),
548 None,
549 )
550 .unwrap();
551
552 assert_eq!(arrangements, expected_arrangements);
553 }
554 #[test]
555 fn duplicate_pitches_in_beat_yield_no_arrangements() {
556 let input_pitches = vec![Line::Playable(vec![Pitch::E2, Pitch::E2])];
560
561 let err = create_arrangements(
562 Guitar::default(),
563 input_pitches,
564 NumArrangements::try_new(1).unwrap(),
565 None,
566 )
567 .unwrap_err();
568
569 assert!(matches!(err, TabError::NoArrangementsFound), "got {err:?}");
570 }
571 #[test]
572 fn input_beyond_max_lines_returns_input_too_many_lines() {
573 let input_pitches = vec![Line::Rest; crate::parser::MAX_INPUT_LINES + 1];
577
578 let result = create_arrangements(
579 Guitar::default(),
580 input_pitches,
581 NumArrangements::try_new(1).unwrap(),
582 None,
583 );
584
585 match result {
586 Err(TabError::InputTooManyLines { max }) => {
587 assert_eq!(max, crate::parser::MAX_INPUT_LINES as u32);
588 }
589 other => panic!("expected Err(InputTooManyLines), got {other:?}"),
590 }
591 }
592 #[test]
593 fn empty_input() {
594 let input_pitches: Vec<Line<BeatVec<Pitch>>> = vec![];
595
596 let arrangements = create_arrangements(
597 Guitar::default(),
598 input_pitches,
599 NumArrangements::try_new(2).unwrap(),
600 None,
601 )
602 .unwrap();
603
604 let expected_arrangements: Vec<Arrangement> = vec![
605 Arrangement {
606 lines: vec![],
607 difficulty: 0,
608 max_fret_span: 0,
609 };
610 2
611 ];
612
613 assert_eq!(arrangements, expected_arrangements);
614 }
615 #[test]
616 fn empty_start_lines_input() {
617 let input_pitches: Vec<Line<BeatVec<Pitch>>> = vec![
618 Line::Rest,
619 Line::MeasureBreak,
620 Line::Rest,
621 Line::Playable(vec![Pitch::E4]),
622 Line::Rest,
623 ];
624
625 let arrangements = create_arrangements(
626 Guitar::default(),
627 input_pitches,
628 NumArrangements::try_new(1).unwrap(),
629 None,
630 )
631 .unwrap();
632
633 let expected_arrangements: Vec<Arrangement> = vec![Arrangement {
634 lines: vec![
635 Line::Playable(vec![PitchFingering {
636 pitch: Pitch::E4,
637 string_number: StringNumber::new(1).unwrap(),
638 fret: 0,
639 }]),
640 Line::Rest,
641 ],
642 difficulty: 0,
643 max_fret_span: 0,
644 }];
645
646 assert_eq!(arrangements, expected_arrangements);
647 }
648 #[test]
649 fn max_fret_span_filter_drops_high_span_arrangements() {
650 let tuning =
651 crate::guitar::create_string_tuning(&crate::guitar::STD_6_STRING_TUNING_OPEN_PITCHES)
652 .unwrap();
653 let guitar = crate::guitar::Guitar::new(tuning, 20, 0).unwrap();
654 let lines = crate::parser::parse_lines("G2B4".to_owned()).unwrap();
657
658 let unfiltered = create_arrangements(
660 guitar.clone(),
661 lines.clone(),
662 NumArrangements::try_new(5).unwrap(),
663 None,
664 )
665 .unwrap();
666 assert!(unfiltered.iter().any(|a| a.max_fret_span() > 0));
667
668 let filtered = create_arrangements(
670 guitar.clone(),
671 lines,
672 NumArrangements::try_new(5).unwrap(),
673 Some(0),
674 )
675 .unwrap();
676 assert!(filtered.iter().all(|a| a.max_fret_span() == 0));
677 assert!(filtered.len() <= 5);
678 }
679 #[test]
680 fn max_fret_span_filter_can_produce_empty_set() {
681 let tuning =
682 crate::guitar::create_string_tuning(&crate::guitar::STD_6_STRING_TUNING_OPEN_PITCHES)
683 .unwrap();
684 let guitar = crate::guitar::Guitar::new(tuning, 20, 0).unwrap();
685 let lines = crate::parser::parse_lines("C3E3".to_owned()).unwrap();
688
689 let filtered =
690 create_arrangements(guitar, lines, NumArrangements::try_new(5).unwrap(), Some(0))
691 .expect("filter dropping every candidate is not an error");
692 assert!(
693 filtered.is_empty(),
694 "max_fret_span_filter=Some(0) on an all-fretted chord must drop every candidate",
695 );
696 }
697
698 #[test]
699 fn max_fret_span_filter_keeps_only_low_span_arrangements() {
700 let tuning =
701 crate::guitar::create_string_tuning(&crate::guitar::STD_6_STRING_TUNING_OPEN_PITCHES)
702 .unwrap();
703 let guitar = crate::guitar::Guitar::new(tuning, 20, 0).unwrap();
704 let lines = crate::parser::parse_lines("C3G3".to_owned()).unwrap();
708
709 let unfiltered = create_arrangements(
710 guitar.clone(),
711 lines.clone(),
712 NumArrangements::try_new(5).unwrap(),
713 None,
714 )
715 .unwrap();
716 assert_eq!(unfiltered.len(), 5);
717 assert!(
718 unfiltered.iter().any(|a| a.max_fret_span() == 0),
719 "expected at least one span-0 arrangement"
720 );
721 assert!(
722 unfiltered.iter().any(|a| a.max_fret_span() > 0),
723 "expected at least one span>0 arrangement so the filter does real work"
724 );
725
726 let filtered =
727 create_arrangements(guitar, lines, NumArrangements::try_new(5).unwrap(), Some(0))
728 .unwrap();
729 assert_eq!(filtered.len(), 2);
732 assert!(filtered.iter().all(|a| a.max_fret_span() == 0));
733 }
734}
735
736fn validate_fingerings(
745 guitar: &Guitar,
746 input_pitches: &[Line<BeatVec<Pitch>>],
747) -> Result<Vec<Line<BeatVec<PitchVec<PitchFingering>>>>, TabError> {
748 let mut unplayable_pitches: Vec<UnplayablePitch> = vec![];
749 let fingerings: Vec<Line<BeatVec<PitchVec<PitchFingering>>>> = input_pitches
750 .iter()
751 .enumerate()
752 .map(|(beat_index, beat_input)| match beat_input {
753 MeasureBreak => MeasureBreak,
754 Rest => Rest,
755 Playable(beat_pitches) => Playable(
756 beat_pitches
757 .iter()
758 .map(|beat_pitch| {
759 let pitch_fingerings: PitchVec<PitchFingering> =
760 generate_pitch_fingerings(&guitar.string_ranges, beat_pitch);
761 if pitch_fingerings.is_empty() {
762 unplayable_pitches.push(UnplayablePitch {
763 value: beat_pitch.plain_text().to_owned(),
764 line: (beat_index as u32) + 1,
765 })
766 }
767 pitch_fingerings
768 })
769 .collect(),
770 ),
771 })
772 .collect();
773
774 if !unplayable_pitches.is_empty() {
775 return Err(TabError::UnplayablePitches {
776 pitches: unplayable_pitches,
777 });
778 }
779
780 Ok(fingerings)
781}
782#[cfg(test)]
783mod test_validate_fingerings {
784 use super::*;
785
786 #[test]
787 fn valid_simple() {
788 let guitar = Guitar::default();
789 let input_pitches = vec![Playable(vec![Pitch::G3])];
790 let expected_fingerings = vec![Playable(vec![generate_pitch_fingerings(
791 &guitar.string_ranges,
792 &Pitch::G3,
793 )])];
794
795 assert_eq!(
796 validate_fingerings(&guitar, &input_pitches).unwrap(),
797 expected_fingerings
798 );
799 }
800 #[test]
801 fn valid_complex() {
802 let guitar = Guitar::default();
803 let input_pitches = vec![
804 Playable(vec![Pitch::G3]),
805 MeasureBreak,
806 Playable(vec![Pitch::B3]),
807 Rest,
808 Playable(vec![Pitch::D4, Pitch::G4]),
809 ];
810 let expected_fingerings = vec![
811 Playable(vec![generate_pitch_fingerings(
812 &guitar.string_ranges,
813 &Pitch::G3,
814 )]),
815 MeasureBreak,
816 Playable(vec![generate_pitch_fingerings(
817 &guitar.string_ranges,
818 &Pitch::B3,
819 )]),
820 Rest,
821 Playable(vec![
822 generate_pitch_fingerings(&guitar.string_ranges, &Pitch::D4),
823 generate_pitch_fingerings(&guitar.string_ranges, &Pitch::G4),
824 ]),
825 ];
826
827 assert_eq!(
828 validate_fingerings(&guitar, &input_pitches).unwrap(),
829 expected_fingerings
830 );
831 }
832 #[test]
833 fn invalid_simple() {
834 let guitar = Guitar::default();
835 let input_pitches = vec![Playable(vec![Pitch::B9])];
836
837 let err = validate_fingerings(&guitar, &input_pitches).unwrap_err();
838 match err {
839 TabError::UnplayablePitches { pitches } => {
840 assert_eq!(pitches.len(), 1);
841 assert_eq!(pitches[0].value, "B9");
842 assert_eq!(pitches[0].line, 1);
843 }
844 other => panic!("expected UnplayablePitches, got {other:?}"),
845 }
846 }
847 #[test]
848 fn invalid_accidental_reports_plain_text_spelling() {
849 let guitar = Guitar::default();
852 let input_pitches = vec![Playable(vec![Pitch::CSharpDFlat9])];
853
854 let err = validate_fingerings(&guitar, &input_pitches).unwrap_err();
855 match err {
856 TabError::UnplayablePitches { pitches } => {
857 assert_eq!(pitches.len(), 1);
858 assert_eq!(pitches[0].value, "Db9");
859 assert_eq!(pitches[0].line, 1);
860 }
861 other => panic!("expected UnplayablePitches, got {other:?}"),
862 }
863 }
864 #[test]
865 fn invalid_complex() {
866 let guitar = Guitar::default();
867 let input_pitches = vec![
868 Playable(vec![Pitch::A1]),
869 Playable(vec![Pitch::G3]),
870 Playable(vec![Pitch::B3]),
871 Playable(vec![Pitch::A1, Pitch::B1]),
872 Playable(vec![Pitch::G3, Pitch::D2]),
873 Playable(vec![Pitch::D4, Pitch::G4]),
874 ];
875
876 let err = validate_fingerings(&guitar, &input_pitches).unwrap_err();
877 match err {
878 TabError::UnplayablePitches { pitches } => {
879 assert_eq!(pitches.len(), 4);
880 assert_eq!(pitches[0].value, "A1");
881 assert_eq!(pitches[0].line, 1);
882 assert_eq!(pitches[1].value, "A1");
883 assert_eq!(pitches[1].line, 4);
884 assert_eq!(pitches[2].value, "B1");
885 assert_eq!(pitches[2].line, 4);
886 assert_eq!(pitches[3].value, "D2");
887 assert_eq!(pitches[3].line, 5);
888 }
889 other => panic!("expected UnplayablePitches, got {other:?}"),
890 }
891 }
892}
893
894fn generate_beat_fingerings(
897 beat_fingerings_per_pitch: &[Vec<PitchFingering>],
898) -> Vec<BeatVec<PitchFingering>> {
899 if beat_fingerings_per_pitch.is_empty() {
904 return Vec::new();
905 }
906
907 beat_fingerings_per_pitch
908 .iter()
909 .multi_cartesian_product()
910 .map(|combo| combo.into_iter().copied().collect::<Vec<PitchFingering>>())
911 .filter(|x| no_duplicate_strings(x))
912 .collect()
913}
914#[cfg(test)]
915mod test_generate_beat_fingerings {
916 use super::*;
917 use crate::string_number::StringNumber;
918
919 #[test]
920 fn simple() {
921 let pitch_fingering = PitchFingering {
922 pitch: Pitch::B6,
923 string_number: StringNumber::new(2).unwrap(),
924 fret: 3,
925 };
926
927 let beat_fingerings_per_pitch = &[vec![pitch_fingering]];
928
929 assert_eq!(
930 generate_beat_fingerings(beat_fingerings_per_pitch),
931 beat_fingerings_per_pitch
932 );
933 }
934 #[test]
935 fn complex() {
936 let pitch_fingering_a_string_2 = PitchFingering {
937 pitch: Pitch::B6,
938 string_number: StringNumber::new(2).unwrap(),
939 fret: 3,
940 };
941 let pitch_fingering_a_string_3 = PitchFingering {
942 pitch: Pitch::B6,
943 string_number: StringNumber::new(3).unwrap(),
944 fret: 8,
945 };
946 let pitch_fingering_b_string_2 = PitchFingering {
947 pitch: Pitch::C7,
948 string_number: StringNumber::new(2).unwrap(),
949 fret: 4,
950 };
951 let pitch_fingering_b_string_3 = PitchFingering {
952 pitch: Pitch::C7,
953 string_number: StringNumber::new(3).unwrap(),
954 fret: 9,
955 };
956 let pitch_fingering_b_string_4 = PitchFingering {
957 pitch: Pitch::C7,
958 string_number: StringNumber::new(4).unwrap(),
959 fret: 14,
960 };
961
962 let beat_fingerings_per_pitch = vec![
963 vec![pitch_fingering_a_string_2, pitch_fingering_a_string_3],
964 vec![
965 pitch_fingering_b_string_2,
966 pitch_fingering_b_string_3,
967 pitch_fingering_b_string_4,
968 ],
969 ];
970 let expected_beat_fingerings = vec![
971 vec![pitch_fingering_a_string_2, pitch_fingering_b_string_3],
972 vec![pitch_fingering_a_string_2, pitch_fingering_b_string_4],
973 vec![pitch_fingering_a_string_3, pitch_fingering_b_string_2],
974 vec![pitch_fingering_a_string_3, pitch_fingering_b_string_4],
975 ];
976
977 assert_eq!(
978 generate_beat_fingerings(&beat_fingerings_per_pitch),
979 expected_beat_fingerings
980 );
981 }
982
983 #[test]
984 fn empty_input_returns_no_combinations() {
985 assert_eq!(
986 generate_beat_fingerings(&[]),
987 Vec::<BeatVec<PitchFingering>>::new()
988 );
989 }
990}
991
992fn no_duplicate_strings(beat_fingering_option: &[PitchFingering]) -> bool {
995 let mut seen_strings = HashSet::with_capacity(beat_fingering_option.len());
996 beat_fingering_option
997 .iter()
998 .all(|fingering| seen_strings.insert(fingering.string_number))
999}
1000#[cfg(test)]
1001mod test_no_duplicate_strings {
1002 use super::*;
1003 use crate::string_number::StringNumber;
1004
1005 #[test]
1006 fn valid_simple() {
1007 let fingering_1 = PitchFingering {
1008 pitch: Pitch::B6,
1009 string_number: StringNumber::new(2).unwrap(),
1010 fret: 3,
1011 };
1012
1013 assert!(no_duplicate_strings(&[fingering_1]));
1014 }
1015 #[test]
1016 fn valid_complex() {
1017 let fingering_1 = PitchFingering {
1018 pitch: Pitch::CSharpDFlat2,
1019 string_number: StringNumber::new(1).unwrap(),
1020 fret: 1,
1021 };
1022 let fingering_2 = PitchFingering {
1023 pitch: Pitch::F4,
1024 string_number: StringNumber::new(2).unwrap(),
1025 fret: 3,
1026 };
1027 let fingering_3 = PitchFingering {
1028 pitch: Pitch::A5,
1029 string_number: StringNumber::new(4).unwrap(),
1030 fret: 4,
1031 };
1032 let fingering_4 = PitchFingering {
1033 pitch: Pitch::DSharpEFlat6,
1034 string_number: StringNumber::new(11).unwrap(),
1035 fret: 0,
1036 };
1037
1038 assert!(no_duplicate_strings(&[
1039 fingering_1,
1040 fingering_2,
1041 fingering_3,
1042 fingering_4
1043 ]));
1044 }
1045 #[test]
1046 fn invalid_simple() {
1047 let fingering_1 = PitchFingering {
1048 pitch: Pitch::CSharpDFlat2,
1049 string_number: StringNumber::new(4).unwrap(),
1050 fret: 1,
1051 };
1052 let fingering_2 = PitchFingering {
1053 pitch: Pitch::F4,
1054 string_number: StringNumber::new(4).unwrap(),
1055 fret: 3,
1056 };
1057
1058 assert!(!no_duplicate_strings(&[fingering_1, fingering_2]));
1059 }
1060 #[test]
1061 fn invalid_complex() {
1062 let fingering_1 = PitchFingering {
1063 pitch: Pitch::CSharpDFlat2,
1064 string_number: StringNumber::new(1).unwrap(),
1065 fret: 1,
1066 };
1067 let fingering_2 = PitchFingering {
1068 pitch: Pitch::F4,
1069 string_number: StringNumber::new(3).unwrap(),
1070 fret: 3,
1071 };
1072 let fingering_3 = PitchFingering {
1073 pitch: Pitch::A5,
1074 string_number: StringNumber::new(6).unwrap(),
1075 fret: 4,
1076 };
1077 let fingering_4 = PitchFingering {
1078 pitch: Pitch::DSharpEFlat6,
1079 string_number: StringNumber::new(3).unwrap(),
1080 fret: 0,
1081 };
1082
1083 assert!(!no_duplicate_strings(&[
1084 fingering_1,
1085 fingering_2,
1086 fingering_3,
1087 fingering_4,
1088 ]));
1089 }
1090 #[test]
1091 fn empty_input() {
1092 assert!(no_duplicate_strings(&[]));
1093 }
1094}
1095
1096fn calc_fret_span(beat_fingering_candidate: &[PitchFingering]) -> Option<u8> {
1099 use itertools::MinMaxResult;
1100
1101 let non_zero_frets = beat_fingering_candidate
1102 .iter()
1103 .filter(|fingering| fingering.fret != 0)
1104 .map(|fingering| fingering.fret);
1105
1106 match non_zero_frets.minmax() {
1107 MinMaxResult::NoElements => None,
1108 MinMaxResult::OneElement(_) => Some(0),
1109 MinMaxResult::MinMax(min, max) => Some(max - min),
1111 }
1112}
1113#[cfg(test)]
1114mod test_calc_fret_span {
1115 use super::*;
1116 use crate::string_number::StringNumber;
1117
1118 #[test]
1119 fn simple() {
1120 let fingering_1 = PitchFingering {
1121 pitch: Pitch::B6,
1122 string_number: StringNumber::new(2).unwrap(),
1123 fret: 3,
1124 };
1125
1126 assert_eq!(calc_fret_span(&[fingering_1]).unwrap(), 0);
1127 }
1128 #[test]
1129 fn complex() {
1130 let fingering_1 = PitchFingering {
1131 pitch: Pitch::CSharpDFlat2,
1132 string_number: StringNumber::new(1).unwrap(),
1133 fret: 1,
1134 };
1135 let fingering_2 = PitchFingering {
1136 pitch: Pitch::F4,
1137 string_number: StringNumber::new(2).unwrap(),
1138 fret: 3,
1139 };
1140 let fingering_3 = PitchFingering {
1141 pitch: Pitch::A5,
1142 string_number: StringNumber::new(4).unwrap(),
1143 fret: 4,
1144 };
1145 let fingering_4 = PitchFingering {
1146 pitch: Pitch::DSharpEFlat6,
1147 string_number: StringNumber::new(11).unwrap(),
1148 fret: 0,
1149 };
1150 let beat_fingering_option = &[fingering_1, fingering_2, fingering_3, fingering_4];
1151
1152 assert_eq!(calc_fret_span(beat_fingering_option).unwrap(), 3);
1153 }
1154 #[test]
1155 fn empty_input() {
1156 assert!(calc_fret_span(&[]).is_none());
1157 }
1158}
1159
1160type NodeDifficulty = i32;
1161
1162fn calc_next_nodes(current_node: &Node, path_nodes: &[Node]) -> Vec<(Node, NodeDifficulty)> {
1168 let next_node_index = match current_node {
1169 Node::Start => 0,
1170 Node::Rest { line_index } | Node::Playable { line_index, .. } => line_index + 1,
1174 };
1175
1176 let next_nodes: Vec<(Node, NodeDifficulty)> = path_nodes
1177 .iter()
1178 .filter(|&node| {
1179 next_node_index
1180 == match node {
1181 Node::Start => unreachable!("Start should never be a future node."),
1182 Node::Rest { line_index } | Node::Playable { line_index, .. } => *line_index,
1183 }
1184 })
1185 .map(|next_node| {
1186 (
1187 next_node.clone(),
1188 calculate_node_difficulty(current_node, next_node),
1189 )
1190 })
1191 .collect_vec();
1192
1193 next_nodes
1194}
1195#[cfg(test)]
1196mod test_calc_next_nodes {
1197 use super::*;
1198
1199 fn create_test_path_nodes() -> [Node; 7] {
1200 [
1201 Node::Playable {
1202 line_index: 0,
1203 scored_beat_fingering: Rc::new(ScoredBeatFingering {
1204 beat_fingering: vec![],
1205 avg_non_zero_fret: Some(OrderedFloat(0.1)),
1206 non_zero_fret_span: 0,
1207 }),
1208 },
1209 Node::Playable {
1210 line_index: 0,
1211 scored_beat_fingering: Rc::new(ScoredBeatFingering {
1212 beat_fingering: vec![],
1213 avg_non_zero_fret: Some(OrderedFloat(0.2)),
1214 non_zero_fret_span: 0,
1215 }),
1216 },
1217 Node::Playable {
1218 line_index: 1,
1219 scored_beat_fingering: Rc::new(ScoredBeatFingering {
1220 beat_fingering: vec![],
1221 avg_non_zero_fret: Some(OrderedFloat(1.1)),
1222 non_zero_fret_span: 1,
1223 }),
1224 },
1225 Node::Rest { line_index: 2 },
1226 Node::Rest { line_index: 3 },
1227 Node::Playable {
1228 line_index: 4,
1229 scored_beat_fingering: Rc::new(ScoredBeatFingering {
1230 beat_fingering: vec![],
1231 avg_non_zero_fret: Some(OrderedFloat(4.1)),
1232 non_zero_fret_span: 4,
1233 }),
1234 },
1235 Node::Playable {
1236 line_index: 4,
1237 scored_beat_fingering: Rc::new(ScoredBeatFingering {
1238 beat_fingering: vec![],
1239 avg_non_zero_fret: Some(OrderedFloat(4.1)),
1240 non_zero_fret_span: 4,
1241 }),
1242 },
1243 ]
1244 }
1245
1246 #[test]
1247 fn from_start_to_note() {
1248 let current_node = Node::Start;
1249
1250 let expected_nodes_and_costs = [
1251 Node::Playable {
1252 line_index: 0,
1253 scored_beat_fingering: Rc::new(ScoredBeatFingering {
1254 beat_fingering: vec![],
1255 avg_non_zero_fret: Some(OrderedFloat(0.1)),
1256 non_zero_fret_span: 0,
1257 }),
1258 },
1259 Node::Playable {
1260 line_index: 0,
1261 scored_beat_fingering: Rc::new(ScoredBeatFingering {
1262 beat_fingering: vec![],
1263 avg_non_zero_fret: Some(OrderedFloat(0.2)),
1264 non_zero_fret_span: 0,
1265 }),
1266 },
1267 ]
1268 .iter()
1269 .map(|node| (node.clone(), calculate_node_difficulty(¤t_node, node)))
1270 .collect_vec();
1271
1272 assert_eq!(
1273 calc_next_nodes(¤t_node, &create_test_path_nodes()),
1274 expected_nodes_and_costs
1275 );
1276 }
1277 #[test]
1278 fn from_note_to_note() {
1279 let current_node = Node::Playable {
1280 line_index: 0,
1281 scored_beat_fingering: Rc::new(ScoredBeatFingering {
1282 beat_fingering: vec![],
1283 avg_non_zero_fret: Some(OrderedFloat(0.1)),
1284 non_zero_fret_span: 0,
1285 }),
1286 };
1287
1288 let expected_nodes_and_costs = [Node::Playable {
1289 line_index: 1,
1290 scored_beat_fingering: Rc::new(ScoredBeatFingering {
1291 beat_fingering: vec![],
1292 avg_non_zero_fret: Some(OrderedFloat(1.1)),
1293 non_zero_fret_span: 1,
1294 }),
1295 }]
1296 .iter()
1297 .map(|node| (node.clone(), calculate_node_difficulty(¤t_node, node)))
1298 .collect_vec();
1299
1300 assert_eq!(
1301 calc_next_nodes(¤t_node, &create_test_path_nodes()),
1302 expected_nodes_and_costs
1303 );
1304 }
1305 #[test]
1306 fn from_note_to_rest() {
1307 let current_node = Node::Playable {
1308 line_index: 1,
1309 scored_beat_fingering: Rc::new(ScoredBeatFingering {
1310 beat_fingering: vec![],
1311 avg_non_zero_fret: Some(OrderedFloat(1.1)),
1312 non_zero_fret_span: 1,
1313 }),
1314 };
1315
1316 let expected_nodes_and_costs = [Node::Rest { line_index: 2 }]
1317 .iter()
1318 .map(|node| (node.clone(), calculate_node_difficulty(¤t_node, node)))
1319 .collect_vec();
1320
1321 assert_eq!(
1322 calc_next_nodes(¤t_node, &create_test_path_nodes()),
1323 expected_nodes_and_costs
1324 );
1325 }
1326 #[test]
1327 fn from_rest_to_rest() {
1328 let current_node = Node::Rest { line_index: 2 };
1329
1330 let expected_nodes_and_costs = [Node::Rest { line_index: 3 }]
1331 .iter()
1332 .map(|node| (node.clone(), calculate_node_difficulty(¤t_node, node)))
1333 .collect_vec();
1334
1335 assert_eq!(
1336 calc_next_nodes(¤t_node, &create_test_path_nodes()),
1337 expected_nodes_and_costs
1338 );
1339 }
1340 #[test]
1341 fn from_rest_to_note() {
1342 let current_node = Node::Rest { line_index: 3 };
1343
1344 let expected_nodes_and_costs = [
1345 Node::Playable {
1346 line_index: 4,
1347 scored_beat_fingering: Rc::new(ScoredBeatFingering {
1348 beat_fingering: vec![],
1349 avg_non_zero_fret: Some(OrderedFloat(4.1)),
1350 non_zero_fret_span: 4,
1351 }),
1352 },
1353 Node::Playable {
1354 line_index: 4,
1355 scored_beat_fingering: Rc::new(ScoredBeatFingering {
1356 beat_fingering: vec![],
1357 avg_non_zero_fret: Some(OrderedFloat(4.1)),
1358 non_zero_fret_span: 4,
1359 }),
1360 },
1361 ]
1362 .iter()
1363 .map(|node| (node.clone(), calculate_node_difficulty(¤t_node, node)))
1364 .collect_vec();
1365
1366 assert_eq!(
1367 calc_next_nodes(¤t_node, &create_test_path_nodes()),
1368 expected_nodes_and_costs
1369 );
1370 }
1371
1372 #[test]
1373 #[should_panic]
1374 fn to_start() {
1375 calc_next_nodes(
1376 &Node::Rest { line_index: 3 },
1377 &[Node::Rest { line_index: 4 }, Node::Start],
1378 );
1379 }
1380}
1381
1382fn calculate_node_difficulty(current_node: &Node, next_node: &Node) -> NodeDifficulty {
1385 let current_avg_fret = match current_node {
1386 Node::Playable {
1387 scored_beat_fingering,
1388 ..
1389 } => scored_beat_fingering.avg_non_zero_fret,
1390 _ => None,
1391 };
1392
1393 let (next_avg_fret, next_fret_span) = match next_node {
1394 Node::Start => unreachable!("Start should never be a future node."),
1395 Node::Rest { .. } => (None, 0.0),
1396 Node::Playable {
1397 scored_beat_fingering,
1398 ..
1399 } => (
1400 scored_beat_fingering.avg_non_zero_fret,
1401 scored_beat_fingering.non_zero_fret_span as f64,
1402 ),
1403 };
1404
1405 let avg_fret_difference = match (current_avg_fret, next_avg_fret) {
1406 (Some(current_avg_fret_num), Some(next_avg_fret_num)) => {
1407 (next_avg_fret_num - current_avg_fret_num).abs()
1408 }
1409 _ => 0.0,
1410 };
1411
1412 ((avg_fret_difference * 100.0)
1416 + (next_fret_span * 10.0)
1417 + (next_avg_fret.unwrap_or(OrderedFloat(0.0))).into_inner()) as NodeDifficulty
1418}
1419#[cfg(test)]
1420mod test_calculate_node_difficulty {
1421 use super::*;
1422
1423 #[test]
1424 fn simple_no_diff() {
1425 let current_node = Node::Playable {
1426 line_index: 0,
1427 scored_beat_fingering: Rc::new(ScoredBeatFingering {
1428 beat_fingering: vec![],
1429 avg_non_zero_fret: Some(OrderedFloat(3.5)),
1430 non_zero_fret_span: 0,
1431 }),
1432 };
1433 let next_node = Node::Playable {
1434 line_index: 1,
1435 scored_beat_fingering: Rc::new(ScoredBeatFingering {
1436 beat_fingering: vec![],
1437 avg_non_zero_fret: Some(OrderedFloat(3.5)),
1438 non_zero_fret_span: 0,
1439 }),
1440 };
1441
1442 assert_eq!(calculate_node_difficulty(¤t_node, &next_node), 3);
1443 }
1444 #[test]
1445 fn simple_from_start() {
1446 let next_node = Node::Playable {
1447 line_index: 1,
1448 scored_beat_fingering: Rc::new(ScoredBeatFingering {
1449 beat_fingering: vec![],
1450 avg_non_zero_fret: Some(OrderedFloat(3.5)),
1451 non_zero_fret_span: 0,
1452 }),
1453 };
1454
1455 assert_eq!(calculate_node_difficulty(&Node::Start, &next_node), 3);
1456 }
1457 #[test]
1458 fn simple_from_rest() {
1459 let next_node = Node::Playable {
1460 line_index: 1,
1461 scored_beat_fingering: Rc::new(ScoredBeatFingering {
1462 beat_fingering: vec![],
1463 avg_non_zero_fret: Some(OrderedFloat(3.5)),
1464 non_zero_fret_span: 0,
1465 }),
1466 };
1467
1468 assert_eq!(
1469 calculate_node_difficulty(&Node::Rest { line_index: 0 }, &next_node),
1470 3
1471 );
1472 }
1473 #[test]
1474 fn simple_to_rest() {
1475 let current_node = Node::Playable {
1476 line_index: 0,
1477 scored_beat_fingering: Rc::new(ScoredBeatFingering {
1478 beat_fingering: vec![],
1479 avg_non_zero_fret: Some(OrderedFloat(3.5)),
1480 non_zero_fret_span: 0,
1481 }),
1482 };
1483
1484 assert_eq!(
1485 calculate_node_difficulty(¤t_node, &Node::Rest { line_index: 1 }),
1486 0
1487 );
1488 }
1489 #[test]
1490 fn simple_avg_fret_diff() {
1491 let current_node = Node::Playable {
1492 line_index: 0,
1493 scored_beat_fingering: Rc::new(ScoredBeatFingering {
1494 beat_fingering: vec![],
1495 avg_non_zero_fret: Some(OrderedFloat(3.0)),
1496 non_zero_fret_span: 0,
1497 }),
1498 };
1499 let next_node = Node::Playable {
1500 line_index: 1,
1501 scored_beat_fingering: Rc::new(ScoredBeatFingering {
1502 beat_fingering: vec![],
1503 avg_non_zero_fret: Some(OrderedFloat(1.6)),
1504 non_zero_fret_span: 0,
1505 }),
1506 };
1507
1508 assert_eq!(calculate_node_difficulty(¤t_node, &next_node), 141);
1509 }
1510 #[test]
1511 fn simple_fret_span() {
1512 let current_node = Node::Playable {
1513 line_index: 0,
1514 scored_beat_fingering: Rc::new(ScoredBeatFingering {
1515 beat_fingering: vec![],
1516 avg_non_zero_fret: Some(OrderedFloat(4.133333)),
1517 non_zero_fret_span: 0,
1518 }),
1519 };
1520 let next_node = Node::Playable {
1521 line_index: 1,
1522 scored_beat_fingering: Rc::new(ScoredBeatFingering {
1523 beat_fingering: vec![],
1524 avg_non_zero_fret: Some(OrderedFloat(4.133333)),
1525 non_zero_fret_span: 3,
1526 }),
1527 };
1528
1529 assert_eq!(calculate_node_difficulty(¤t_node, &next_node), 34);
1530 }
1531 #[test]
1532 fn compound() {
1533 let current_node = Node::Playable {
1534 line_index: 0,
1535 scored_beat_fingering: Rc::new(ScoredBeatFingering {
1536 beat_fingering: vec![],
1537 avg_non_zero_fret: Some(OrderedFloat(5.0)),
1538 non_zero_fret_span: 0,
1539 }),
1540 };
1541 let next_node = Node::Playable {
1542 line_index: 1,
1543 scored_beat_fingering: Rc::new(ScoredBeatFingering {
1544 beat_fingering: vec![],
1545 avg_non_zero_fret: Some(OrderedFloat(2.0)),
1546 non_zero_fret_span: 5,
1547 }),
1548 };
1549
1550 assert_eq!(calculate_node_difficulty(¤t_node, &next_node), 352);
1551 }
1552 #[test]
1553 fn complex() {
1554 let current_node = Node::Playable {
1555 line_index: 0,
1556 scored_beat_fingering: Rc::new(ScoredBeatFingering {
1557 beat_fingering: vec![],
1558 avg_non_zero_fret: Some(OrderedFloat(7.3333333)),
1559 non_zero_fret_span: 0,
1560 }),
1561 };
1562 let next_node = Node::Playable {
1563 line_index: 1,
1564 scored_beat_fingering: Rc::new(ScoredBeatFingering {
1565 beat_fingering: vec![],
1566 avg_non_zero_fret: Some(OrderedFloat(3.6666666)),
1567 non_zero_fret_span: 4,
1568 }),
1569 };
1570
1571 assert_eq!(calculate_node_difficulty(¤t_node, &next_node), 410);
1572 }
1573}
1574
1575fn process_path(
1576 path_nodes: Vec<Node>,
1577 path_difficulty: i32,
1578 measure_break_indices: &[usize],
1579) -> Arrangement {
1580 let mut lines: Vec<Line<BeatVec<PitchFingering>>> = path_nodes
1581 .iter()
1582 .filter(|node| node != &&Node::Start)
1583 .map(|node| match node {
1584 Node::Start => unreachable!("Start node should have been filtered out."),
1585 Node::Rest { .. } => Line::Rest,
1586 Node::Playable {
1587 scored_beat_fingering,
1588 ..
1589 } => Line::Playable(scored_beat_fingering.beat_fingering.clone()),
1590 })
1591 .collect_vec();
1592 for &measure_break_index in measure_break_indices {
1596 lines.insert(measure_break_index, Line::MeasureBreak);
1597 }
1598
1599 let max_fret_span: u8 = path_nodes
1600 .iter()
1601 .filter(|node| node != &&Node::Start)
1602 .filter_map(|node| match node {
1603 Node::Start => unreachable!("Start node should have been filtered out."),
1604 Node::Rest { .. } => None,
1605 Node::Playable {
1606 scored_beat_fingering,
1607 ..
1608 } => Some(scored_beat_fingering.non_zero_fret_span),
1609 })
1610 .max()
1611 .unwrap_or(0);
1612
1613 Arrangement {
1614 lines,
1615 difficulty: path_difficulty,
1616 max_fret_span,
1617 }
1618}
1619#[cfg(test)]
1620mod test_process_path {
1621 use super::*;
1622 use crate::string_number::StringNumber;
1623
1624 #[test]
1625 fn simple() {
1626 let placeholder_scored_beat_fingering = ScoredBeatFingering {
1627 beat_fingering: vec![PitchFingering {
1628 pitch: Pitch::C4,
1629 string_number: StringNumber::new(1).unwrap(),
1630 fret: 3,
1631 }],
1632 avg_non_zero_fret: Some(OrderedFloat(3.0)),
1633 non_zero_fret_span: 0,
1634 };
1635
1636 let path_nodes = vec![
1637 Node::Start,
1638 Node::Playable {
1639 line_index: 0,
1640 scored_beat_fingering: Rc::new(placeholder_scored_beat_fingering.clone()),
1641 },
1642 ];
1643
1644 let arrangement = process_path(path_nodes, 123, &[]);
1645
1646 let expected_arrangement = Arrangement {
1647 lines: vec![Playable(placeholder_scored_beat_fingering.beat_fingering)],
1648 difficulty: 123,
1649 max_fret_span: 0,
1650 };
1651
1652 assert_eq!(arrangement, expected_arrangement);
1653 }
1654 #[test]
1655 fn complex() {
1656 let placeholder_scored_beat_fingering = ScoredBeatFingering {
1657 beat_fingering: vec![PitchFingering {
1658 pitch: Pitch::C4,
1659 string_number: StringNumber::new(1).unwrap(),
1660 fret: 3,
1661 }],
1662 avg_non_zero_fret: Some(OrderedFloat(3.0)),
1663 non_zero_fret_span: 4,
1664 };
1665
1666 let path_nodes = vec![
1667 Node::Start,
1668 Node::Playable {
1669 line_index: 0,
1670 scored_beat_fingering: Rc::new(placeholder_scored_beat_fingering.clone()),
1671 },
1672 Node::Playable {
1673 line_index: 1,
1674 scored_beat_fingering: Rc::new(placeholder_scored_beat_fingering.clone()),
1675 },
1676 Node::Rest { line_index: 2 },
1677 Node::Playable {
1678 line_index: 3,
1679 scored_beat_fingering: Rc::new(placeholder_scored_beat_fingering.clone()),
1680 },
1681 Node::Playable {
1682 line_index: 4,
1683 scored_beat_fingering: Rc::new(placeholder_scored_beat_fingering.clone()),
1684 },
1685 ];
1686
1687 let arrangement = process_path(path_nodes, 321, &[0, 2, 5, 7]);
1688
1689 let expected_arrangement = Arrangement {
1690 lines: vec![
1691 MeasureBreak,
1692 Playable(placeholder_scored_beat_fingering.clone().beat_fingering),
1693 MeasureBreak,
1694 Playable(placeholder_scored_beat_fingering.clone().beat_fingering),
1695 Rest,
1696 MeasureBreak,
1697 Playable(placeholder_scored_beat_fingering.clone().beat_fingering),
1698 MeasureBreak,
1699 Playable(placeholder_scored_beat_fingering.beat_fingering),
1700 ],
1701 difficulty: 321,
1702 max_fret_span: 4,
1703 };
1704
1705 assert_eq!(arrangement, expected_arrangement);
1706 }
1707}
1708
1709#[cfg(all(test, not(target_arch = "wasm32")))]
1712mod proptest_invariants {
1713 use super::*;
1714 use crate::NumArrangements;
1715 use crate::guitar::{STD_6_STRING_TUNING_OPEN_PITCHES, create_string_tuning};
1716 use proptest::prelude::*;
1717 use std::collections::HashSet;
1718
1719 fn any_pitch() -> impl Strategy<Value = Pitch> {
1720 (28usize..=72usize).prop_map(|idx| Pitch::from_repr(idx).expect("BUG: index in range"))
1723 }
1724
1725 #[derive(Debug, Clone)]
1726 #[allow(dead_code)] struct ArrangementCase {
1728 input_lines: Vec<Line<BeatVec<Pitch>>>,
1729 num_arrangements: NumArrangements,
1730 measure_break_positions: Vec<usize>,
1731 rest_positions: Vec<usize>,
1732 playable_pitches_per_line: Vec<Vec<Pitch>>,
1733 }
1734
1735 fn arb_case() -> impl Strategy<Value = ArrangementCase> {
1736 (
1737 prop::collection::vec(
1738 (
1739 prop::collection::vec(any_pitch(), 1..=3),
1740 any::<u8>(), ),
1742 1..=6,
1743 ),
1744 1u8..=5u8,
1745 )
1746 .prop_map(|(line_specs, num_arrangements)| {
1747 let mut input_lines: Vec<Line<BeatVec<Pitch>>> =
1748 Vec::with_capacity(line_specs.len());
1749 let mut measure_break_positions = Vec::new();
1750 let mut rest_positions = Vec::new();
1751 let mut playable_pitches_per_line = Vec::new();
1752
1753 for (idx, (pitches, kind_byte)) in line_specs.into_iter().enumerate() {
1754 match kind_byte % 8 {
1755 0 => {
1756 input_lines.push(Line::Rest);
1757 rest_positions.push(idx);
1758 playable_pitches_per_line.push(Vec::new());
1759 }
1760 1 => {
1761 input_lines.push(Line::MeasureBreak);
1762 measure_break_positions.push(idx);
1763 playable_pitches_per_line.push(Vec::new());
1764 }
1765 _ => {
1766 playable_pitches_per_line.push(pitches.clone());
1767 input_lines.push(Line::Playable(pitches));
1768 }
1769 }
1770 }
1771
1772 if !input_lines.iter().any(|l| matches!(l, Line::Playable(_))) {
1774 input_lines[0] = Line::Playable(vec![Pitch::E4]);
1775 if let Some(pos) = rest_positions.iter().position(|&p| p == 0) {
1776 rest_positions.remove(pos);
1777 }
1778 if let Some(pos) = measure_break_positions.iter().position(|&p| p == 0) {
1779 measure_break_positions.remove(pos);
1780 }
1781 playable_pitches_per_line[0] = vec![Pitch::E4];
1782 }
1783
1784 ArrangementCase {
1785 input_lines,
1786 num_arrangements: NumArrangements::try_new(num_arrangements)
1787 .expect("BUG: strategy generates 1..=5"),
1788 measure_break_positions,
1789 rest_positions,
1790 playable_pitches_per_line,
1791 }
1792 })
1793 }
1794
1795 fn std_guitar() -> Guitar {
1796 let tuning = create_string_tuning(&STD_6_STRING_TUNING_OPEN_PITCHES)
1797 .expect("BUG: standard tuning is always valid");
1798 Guitar::new(tuning, 18, 0).expect("BUG: std guitar is always valid")
1799 }
1800
1801 proptest! {
1802 #![proptest_config(ProptestConfig::with_cases(32))]
1803
1804 #[test]
1807 fn invariant_input_pitches_represented(case in arb_case()) {
1808 let guitar = std_guitar();
1809 let arrangements = create_arrangements(
1810 guitar.clone(), case.input_lines.clone(), case.num_arrangements, None,
1811 ).map_err(|e| TestCaseError::reject(format!("create_arrangements rejected input: {e}")))?;
1812
1813 let first_playable = case.input_lines
1815 .iter()
1816 .position(|l| matches!(l, Line::Playable(_)))
1817 .unwrap_or(0);
1818 let effective_lines: Vec<&Line<BeatVec<Pitch>>> = case.input_lines
1819 .iter()
1820 .skip(first_playable)
1821 .collect();
1822
1823 for arrangement in &arrangements {
1824 prop_assert_eq!(arrangement.lines.len(), effective_lines.len());
1825 for (idx, (input_line, output_line)) in
1826 effective_lines.iter().zip(arrangement.lines.iter()).enumerate()
1827 {
1828 match (input_line, output_line) {
1829 (Line::Playable(input_pitches), Line::Playable(fingerings)) => {
1830 prop_assert_eq!(
1831 fingerings.len(), input_pitches.len(),
1832 "line {} fingering count mismatch", idx
1833 );
1834 let output_pitches: HashSet<Pitch> =
1835 fingerings.iter().map(|f| f.pitch).collect();
1836 let expected_pitches: HashSet<Pitch> =
1837 input_pitches.iter().copied().collect();
1838 prop_assert_eq!(
1839 output_pitches, expected_pitches,
1840 "line {} pitch mismatch", idx
1841 );
1842 }
1843 (Line::Rest, Line::Rest) | (Line::MeasureBreak, Line::MeasureBreak) => {}
1844 _ => prop_assert!(false, "line {} variant mismatch", idx),
1845 }
1846 }
1847 }
1848 }
1849
1850 #[test]
1852 fn invariant_no_duplicate_strings(case in arb_case()) {
1853 let guitar = std_guitar();
1854 let arrangements = create_arrangements(
1855 guitar, case.input_lines, case.num_arrangements, None,
1856 ).map_err(|e| TestCaseError::reject(format!("create_arrangements rejected input: {e}")))?;
1857
1858 for arrangement in &arrangements {
1859 for line in &arrangement.lines {
1860 if let Line::Playable(fingerings) = line {
1861 let mut seen = HashSet::new();
1862 for f in fingerings {
1863 prop_assert!(
1864 seen.insert(f.string_number),
1865 "duplicate string_number in beat"
1866 );
1867 }
1868 }
1869 }
1870 }
1871 }
1872
1873 #[test]
1875 fn invariant_fret_bounds(case in arb_case()) {
1876 let guitar = std_guitar();
1877 let playable_frets = guitar.playable_frets;
1878 let arrangements = create_arrangements(
1879 guitar, case.input_lines, case.num_arrangements, None,
1880 ).map_err(|e| TestCaseError::reject(format!("create_arrangements rejected input: {e}")))?;
1881
1882 for arrangement in &arrangements {
1883 for line in &arrangement.lines {
1884 if let Line::Playable(fingerings) = line {
1885 for f in fingerings {
1886 prop_assert!(f.fret <= playable_frets);
1887 }
1888 }
1889 }
1890 }
1891 }
1892
1893 #[test]
1895 fn invariant_sorted_by_difficulty(case in arb_case()) {
1896 let guitar = std_guitar();
1897 let arrangements = create_arrangements(
1898 guitar, case.input_lines, case.num_arrangements, None,
1899 ).map_err(|e| TestCaseError::reject(format!("create_arrangements rejected input: {e}")))?;
1900
1901 for pair in arrangements.windows(2) {
1902 prop_assert!(pair[0].difficulty <= pair[1].difficulty);
1903 }
1904 }
1905
1906 #[test]
1908 fn invariant_count_bounded(case in arb_case()) {
1909 let guitar = std_guitar();
1910 let arrangements = create_arrangements(
1911 guitar, case.input_lines, case.num_arrangements, None,
1912 ).map_err(|e| TestCaseError::reject(format!("create_arrangements rejected input: {e}")))?;
1913
1914 prop_assert!(arrangements.len() <= case.num_arrangements.get() as usize);
1915 }
1916
1917 #[test]
1922 fn invariant_deterministic(case in arb_case()) {
1923 let guitar1 = std_guitar();
1924 let guitar2 = std_guitar();
1925 let first = memoized_original_create_arrangements(
1926 guitar1, case.input_lines.clone(), case.num_arrangements, None,
1927 );
1928 let second = memoized_original_create_arrangements(
1929 guitar2, case.input_lines, case.num_arrangements, None,
1930 );
1931 match (first, second) {
1932 (Ok(a), Ok(b)) => prop_assert_eq!(a, b),
1933 (Err(a), Err(b)) => prop_assert_eq!(a, b),
1934 _ => prop_assert!(false, "determinism violated: outcomes differ"),
1935 }
1936 }
1937 }
1938}