mod methods;
pub use self::methods::*;
use std::ops::{Range, RangeInclusive};
use bellframe::{
method::LABEL_LEAD_END,
music::{Elem, Pattern},
Bell, Mask, PlaceNot, RowBuf, Stage, Stroke,
};
use itertools::Itertools;
use ordered_float::OrderedFloat;
use crate::{
group::PartHeadGroup,
query::{self, CallVec, MethodVec, MusicTypeIdx, MusicTypeVec, Query, StrokeSet},
search::{Config, Search, Update},
utils::{
lengths::{PerPartLength, TotalLength},
Score,
},
Composition,
};
#[allow(unused_imports)] use bellframe::Row;
pub struct SearchBuilder {
length_range: RangeInclusive<TotalLength>,
pub num_comps: usize,
pub require_truth: bool,
stage: Stage,
methods: MethodVec<(bellframe::Method, self::Method)>,
pub default_method_count: OptionalRangeInclusive,
pub default_start_indices: Vec<isize>,
pub default_end_indices: EndIndices,
pub splice_style: SpliceStyle,
pub splice_weight: f32,
pub base_calls: Option<BaseCalls>,
pub custom_calls: Vec<Call>,
pub call_display_style: CallDisplayStyle,
music_types: MusicTypeVec<query::MusicType>,
pub start_stroke: Stroke,
pub start_row: RowBuf,
pub end_row: RowBuf,
pub part_head: RowBuf,
pub courses: Option<Vec<Mask>>,
pub course_weights: Vec<(Mask, f32)>,
pub non_duffer_courses: Option<Vec<CourseSet>>,
pub max_contiguous_duffer: Option<usize>,
pub max_total_duffer: Option<usize>,
}
impl SearchBuilder {
pub fn with_methods(
methods: impl IntoIterator<Item = Method>,
length: Length,
) -> crate::Result<Self> {
let method_set = MethodSet {
vec: methods.into_iter().collect(),
};
Self::with_method_set(method_set, length)
}
pub fn with_method_set(method_set: MethodSet, length: Length) -> crate::Result<Self> {
let method_builders = method_set.vec;
if method_builders.is_empty() {
return Err(crate::Error::NoMethods);
}
let cc_lib = bellframe::MethodLib::cc_lib().expect("Can't load Central Council library.");
let mut methods = MethodVec::new();
let mut stage = Stage::ONE;
for method_builder in method_builders {
let bellframe_method = method_builder.bellframe_method(&cc_lib)?;
stage = stage.max(bellframe_method.stage());
methods.push((bellframe_method, method_builder));
}
let (min_length, max_length) = match length {
Length::Practice => (0, 300),
Length::QuarterPeal => (1250, 1350),
Length::HalfPeal => (2500, 2600),
Length::Peal => (5000, 5200),
Length::Range(range) => (*range.start(), *range.end()),
};
let length_range = TotalLength::new(min_length)..=TotalLength::new(max_length);
Ok(SearchBuilder {
stage,
length_range,
num_comps: 100,
require_truth: true,
methods,
default_method_count: OptionalRangeInclusive::OPEN,
default_start_indices: vec![0], default_end_indices: EndIndices::Any,
splice_style: SpliceStyle::LeadLabels,
splice_weight: 0.0,
base_calls: Some(BaseCalls::default()),
custom_calls: vec![],
call_display_style: CallDisplayStyle::CallingPositions(stage.tenor()),
music_types: MusicTypeVec::new(),
start_stroke: Stroke::Hand,
start_row: RowBuf::rounds(stage),
end_row: RowBuf::rounds(stage),
part_head: RowBuf::rounds(stage),
courses: None, course_weights: vec![],
non_duffer_courses: None, max_contiguous_duffer: None,
max_total_duffer: None,
})
}
pub fn get_stage(&self) -> Stage {
self.stage
}
pub fn music_types(mut self, music_types: impl IntoIterator<Item = MusicType>) -> Self {
self.music_types
.extend(music_types.into_iter().map(|ty| ty.music_type));
self
}
pub fn handbell_coursing_weight(mut self, weight: f32) -> Self {
if weight == 0.0 {
return self; }
for right_bell in self.stage.bells().step_by(2) {
let left_bell = right_bell + 1;
for (b1, b2) in [(left_bell, right_bell), (right_bell, left_bell)] {
let pattern =
Pattern::from_elems([Elem::Star, Elem::Bell(b1), Elem::Bell(b2)], self.stage)
.expect("Handbell patterns should always be valid regexes");
let mask = Mask::from_pattern(&pattern)
.expect("Handbell patterns should only have one `*`");
self.course_weights.push((mask, weight));
}
}
self
}
pub fn build(self, config: Config) -> crate::Result<Search> {
Search::new(self.into_query()?, config)
}
pub fn run(self) -> crate::Result<Vec<Composition>> {
self.run_with_config(Config::default())
}
pub fn run_with_config(self, config: Config) -> crate::Result<Vec<Composition>> {
let mut comps = Vec::<Composition>::new();
let update_fn = |update| {
if let Update::Comp(comp) = update {
comps.push(comp);
}
};
Search::new(self.into_query()?, config)?.run(update_fn);
Ok(comps)
}
fn into_query(self) -> crate::Result<Query> {
let Self {
stage,
length_range,
num_comps,
require_truth,
methods,
default_method_count,
default_start_indices,
default_end_indices,
splice_style,
splice_weight,
base_calls,
custom_calls,
call_display_style,
music_types,
start_stroke,
start_row,
end_row,
part_head,
courses,
course_weights,
non_duffer_courses,
max_contiguous_duffer,
max_total_duffer,
} = self;
let courses = courses.unwrap_or_else(|| {
let tenors_unaffected_by_part_head =
stage.bells().skip(6).filter(|&b| part_head.is_fixed(b));
vec![Mask::with_fixed_bells(
stage,
tenors_unaffected_by_part_head,
)]
});
let mut calls = CallVec::new();
if let Some(base_calls) = base_calls {
calls.extend(base_calls.into_calls(stage));
}
calls.extend(custom_calls.into_iter().map(Call::build));
let fixed_bells = self::methods::fixed_bells(&methods, &calls, &start_row, stage);
let mut built_methods = MethodVec::new();
for (bellframe_method, method_builder) in methods {
built_methods.push(method_builder.build(
bellframe_method,
&fixed_bells,
default_method_count,
&default_start_indices,
&default_end_indices,
&courses,
non_duffer_courses.as_deref(),
&part_head,
stage,
)?);
}
let max_contiguous_duffer = match (max_contiguous_duffer, max_total_duffer) {
(Some(contiguous), Some(total)) => Some(usize::min(contiguous, total)),
(None, Some(one_val)) | (Some(one_val), None) => Some(one_val),
(None, None) => None,
};
let part_head_group = PartHeadGroup::new(&part_head);
Ok(Query {
length_range,
stage,
num_comps,
require_truth,
methods: built_methods,
splice_style,
splice_weight: OrderedFloat(splice_weight),
calls,
call_display_style,
fixed_bells,
start_row,
end_row,
course_weights: course_weights
.into_iter()
.map(|(mask, weight)| (mask, OrderedFloat(weight)))
.collect(),
music_types: music_types.into_iter().collect(),
start_stroke,
max_contiguous_duffer: max_contiguous_duffer.map(PerPartLength::new),
max_total_duffer: max_total_duffer.map(TotalLength::new), part_head_group,
})
}
}
pub const DEFAULT_BOB_WEIGHT: f32 = -1.8;
pub const DEFAULT_SINGLE_WEIGHT: f32 = -2.3;
pub const DEFAULT_MISC_CALL_WEIGHT: f32 = -3.0;
pub struct Call {
symbol: String,
calling_positions: Option<Vec<String>>,
label_from: String,
label_to: String,
place_notation: PlaceNot,
weight: f32,
}
impl Call {
pub fn new(symbol: impl Into<String>, place_notation: PlaceNot) -> Self {
Self {
symbol: symbol.into(),
calling_positions: None, label_from: LABEL_LEAD_END.to_owned(),
label_to: LABEL_LEAD_END.to_owned(),
place_notation,
weight: DEFAULT_MISC_CALL_WEIGHT,
}
}
pub fn calling_positions(mut self, positions: Vec<String>) -> Self {
self.calling_positions = Some(positions);
self
}
pub fn maybe_calling_positions(mut self, positions: Option<Vec<String>>) -> Self {
self.calling_positions = positions;
self
}
pub fn label(mut self, label: impl Into<String>) -> Self {
let label = label.into();
self.label_from = label.clone();
self.label_to = label;
self
}
pub fn label_from_to(mut self, from: impl Into<String>, to: impl Into<String>) -> Self {
self.label_from = from.into();
self.label_to = to.into();
self
}
pub fn weight(mut self, weight: f32) -> Self {
self.weight = weight;
self
}
fn build(self) -> query::Call {
query::Call {
symbol: self.symbol,
calling_positions: self
.calling_positions
.unwrap_or_else(|| default_calling_positions(&self.place_notation)),
label_from: self.label_from,
label_to: self.label_to,
place_notation: self.place_notation,
weight: Score::from(self.weight),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CallDisplayStyle {
Positional,
CallingPositions(Bell),
}
#[derive(Debug, Clone)]
pub struct BaseCalls {
pub ty: BaseCallType,
pub bob_weight: Option<f32>,
pub single_weight: Option<f32>,
}
#[derive(Debug, Clone, Copy)]
pub enum BaseCallType {
Near,
Far,
}
impl BaseCalls {
fn into_calls(self, stage: Stage) -> Vec<query::Call> {
let n = stage.num_bells_u8();
let mut calls = Vec::new();
if let Some(bob_weight) = self.bob_weight {
let bob_pn = match self.ty {
BaseCallType::Near => PlaceNot::parse("14", stage).unwrap(),
BaseCallType::Far => PlaceNot::from_slice(&mut [0, n - 3], stage).unwrap(),
};
calls.push(lead_end_call(bob_pn, "-", bob_weight)); }
if let Some(single_weight) = self.single_weight {
let single_pn = match self.ty {
BaseCallType::Near => PlaceNot::parse("1234", stage).unwrap(),
BaseCallType::Far => {
PlaceNot::from_slice(&mut [0, n - 3, n - 2, n - 1], stage).unwrap()
}
};
calls.push(lead_end_call(single_pn, "s", single_weight));
}
calls
}
}
impl Default for BaseCalls {
fn default() -> Self {
Self {
ty: BaseCallType::Near,
bob_weight: Some(DEFAULT_BOB_WEIGHT),
single_weight: Some(DEFAULT_SINGLE_WEIGHT),
}
}
}
fn lead_end_call(place_not: PlaceNot, symbol: &str, weight: f32) -> query::Call {
query::Call {
symbol: symbol.to_owned(),
calling_positions: default_calling_positions(&place_not),
label_from: LABEL_LEAD_END.to_owned(),
label_to: LABEL_LEAD_END.to_owned(),
place_notation: place_not,
weight: Score::from(weight),
}
}
#[allow(clippy::branches_sharing_code)]
fn default_calling_positions(place_not: &PlaceNot) -> Vec<String> {
let named_positions = "LIBFVXSEN";
let mut positions =
named_positions
.chars()
.map(|c| c.to_string())
.chain((named_positions.len()..).map(|i| format!("{}ths", i + 1)))
.take(place_not.stage().num_bells())
.collect_vec();
macro_rules! replace_pos {
($idx: expr, $new_val: expr) => {
if let Some(v) = positions.get_mut($idx) {
v.clear();
v.push($new_val);
}
};
}
if place_not.contains(1) {
replace_pos!(1, 'B');
replace_pos!(2, 'T');
}
macro_rules! replace_mwh {
($ind: expr, $new_val: expr) => {
if let Some(place) = place_not.stage().num_bells().checked_sub(1 + $ind) {
if place >= 4 {
if let Some(v) = positions.get_mut(place) {
v.clear();
v.push($new_val);
}
}
}
};
}
if place_not.stage().is_even() {
replace_mwh!(2, 'M');
replace_mwh!(1, 'W');
replace_mwh!(0, 'H');
} else {
replace_mwh!(2, 'W');
replace_mwh!(1, 'M');
replace_mwh!(0, 'H');
}
positions
}
#[cfg(test)]
mod tests {
use bellframe::{PlaceNot, Stage};
use itertools::Itertools;
fn char_vec(string: &str) -> Vec<String> {
string.chars().map(|c| c.to_string()).collect_vec()
}
#[test]
fn default_calling_positions() {
#[rustfmt::skip]
let cases = &[
("145", Stage::DOUBLES, char_vec("LIBFH")),
("125", Stage::DOUBLES, char_vec("LBTFH")),
("1", Stage::DOUBLES, char_vec("LIBFH")),
("14", Stage::MINOR, char_vec("LIBFWH")),
("1234", Stage::MINOR, char_vec("LBTFWH")),
("1456", Stage::MINOR, char_vec("LIBFWH")),
("147", Stage::TRIPLES, char_vec("LIBFWMH")),
("12347", Stage::TRIPLES, char_vec("LBTFWMH")),
("14", Stage::MAJOR, char_vec("LIBFVMWH")),
("1234", Stage::MAJOR, char_vec("LBTFVMWH")),
("16", Stage::MAJOR, char_vec("LIBFVMWH")),
("1678", Stage::MAJOR, char_vec("LIBFVMWH")),
("1256", Stage::MAJOR, char_vec("LBTFVMWH")),
("123456", Stage::MAJOR, char_vec("LBTFVMWH")),
("14", Stage::ROYAL, char_vec("LIBFVXSMWH")),
("16", Stage::ROYAL, char_vec("LIBFVXSMWH")),
("18", Stage::ROYAL, char_vec("LIBFVXSMWH")),
("1890", Stage::ROYAL, char_vec("LIBFVXSMWH")),
("14", Stage::MAXIMUS, char_vec("LIBFVXSENMWH")),
("1234", Stage::MAXIMUS, char_vec("LBTFVXSENMWH")),
];
for (pn_str, stage, exp_positions) in cases {
let positions =
super::default_calling_positions(&PlaceNot::parse(pn_str, *stage).unwrap());
assert_eq!(positions, *exp_positions);
}
}
}
pub struct MusicType {
music_type: query::MusicType,
}
impl MusicType {
pub fn new(patterns: impl IntoIterator<Item = Pattern>) -> Self {
Self {
music_type: query::MusicType {
patterns: patterns.into_iter().collect_vec(),
strokes: StrokeSet::Both,
weight: Score::from(1.0),
count_range: OptionalRangeInclusive::OPEN,
},
}
}
pub fn weight(mut self, weight: f32) -> Self {
self.music_type.weight = Score::from(weight);
self
}
pub fn at_backstroke(mut self) -> Self {
self.music_type.strokes = StrokeSet::Back;
self
}
pub fn at_handstroke(mut self) -> Self {
self.music_type.strokes = StrokeSet::Hand;
self
}
pub fn count_range(mut self, range: OptionalRangeInclusive) -> Self {
self.music_type.count_range = range;
self
}
#[allow(clippy::should_implement_trait)]
pub fn add(self, search: &mut SearchBuilder) -> MusicTypeId {
let index = search.music_types.push(self.music_type);
MusicTypeId { index }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct MusicTypeId {
pub(crate) index: MusicTypeIdx,
}
pub enum Length {
Practice,
QuarterPeal,
HalfPeal,
Peal,
Range(RangeInclusive<usize>),
}
#[derive(Debug, Clone, Copy, Default)]
pub struct OptionalRangeInclusive {
pub min: Option<usize>,
pub max: Option<usize>,
}
impl OptionalRangeInclusive {
pub const OPEN: Self = Self {
min: None,
max: None,
};
pub fn is_set(self) -> bool {
self.min.is_some() || self.max.is_some()
}
pub fn or(self, other: Self) -> Self {
Self {
min: self.min.or(other.min),
max: self.max.or(other.max),
}
}
pub fn or_range(self, other: &Range<usize>) -> Range<usize> {
let min = self.min.unwrap_or(other.start);
let max = self
.max
.map(|x| x + 1) .unwrap_or(other.end);
min..max
}
}
pub enum EndIndices {
Any,
Specific(Vec<isize>),
}