use std::{
collections::HashMap,
fmt::Write,
path::{Path, PathBuf},
};
use anyhow::anyhow;
use bellframe::{
method::LABEL_LEAD_END, method_lib::SearchError, place_not::PnBlockParseError, Bell, Mask,
MethodLib, RowBuf, Stage, Stroke,
};
use colored::Colorize;
use index_vec::index_vec;
use itertools::Itertools;
use monument::{
parameters::{
BaseCallType, CallDisplayStyle, CallId, CallVec, IdGenerator, MethodId, MethodVec,
MusicType, MusicTypeVec, OptionalRangeInclusive, Parameters, DEFAULT_BOB_WEIGHT,
DEFAULT_SINGLE_WEIGHT,
},
Config, PartHeadGroup,
};
use serde::Deserialize;
use crate::{
calls::{BaseCalls, CustomCall},
music::{BaseMusic, MusicDisplay, TomlMusic},
utils::OptRangeInclusive,
};
use self::length::Length;
#[allow(unused_imports)] use bellframe::Row;
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TomlFile {
length: Length,
#[serde(default = "default_num_comps")]
num_comps: usize,
#[serde(default = "crate::utils::get_true")]
require_truth: bool,
allow_false: Option<bool>,
graph_size_limit: Option<usize>,
method: Option<TomlMethod>,
#[serde(default)]
methods: Vec<TomlMethod>,
#[serde(default)]
splice_style: SpliceStyle,
#[serde(default)]
splice_weight: f32,
#[serde(default)]
method_count: OptRangeInclusive,
#[serde(default)]
snap_start: bool,
start_indices: Option<Vec<isize>>,
end_indices: Option<Vec<isize>>,
atw_weight: Option<f32>,
#[serde(default)]
require_atw: bool,
calling_bell: Option<u8>,
#[serde(default)] base_calls: BaseCalls,
#[serde(default)]
bobs_only: bool,
#[serde(default)]
singles_only: bool,
bob_weight: Option<f32>,
single_weight: Option<f32>,
#[serde(default)]
calls: Vec<CustomCall>,
#[serde(default)]
base_music: BaseMusic,
music_file: Option<PathBuf>,
#[serde(default)]
music: Vec<TomlMusic>,
#[serde(default = "crate::utils::handstroke")]
start_stroke: Stroke,
#[serde(default)] start_row: String,
#[serde(default)] end_row: String,
#[serde(default)]
part_head: String,
#[serde(default)]
split_tenors: bool,
courses: Option<Vec<String>>,
course_heads: Option<Vec<String>>, #[serde(default)]
course_weights: Vec<CourseWeightPattern>,
ch_weights: Option<Vec<CourseWeightPattern>>, #[serde(default)]
handbell_coursing_weight: f32,
}
impl TomlFile {
pub fn new(toml_path: &Path) -> anyhow::Result<Self> {
let toml_buf = crate::utils::read_file_to_string(toml_path)?;
crate::utils::parse_toml(&toml_buf)
}
pub fn to_params(&self, toml_path: &Path) -> anyhow::Result<(Parameters, Vec<MusicDisplay>)> {
log::debug!("Generating params");
if self.allow_false.is_some() {
anyhow::bail!("`allow_false` has been replaced with `require_truth`");
}
if self.course_heads.is_some() {
anyhow::bail!("`course_heads` has been renamed to `courses`");
}
let cc_lib =
bellframe::MethodLib::cc_lib().expect("Couldn't load Central Council method library");
let all_methods = self.methods.iter().chain(self.method.as_ref());
let mut parsed_methods = Vec::new();
for m in all_methods {
parsed_methods.push((m.as_bellframe_method(&cc_lib)?, m.common()));
}
let stage = parsed_methods
.iter()
.map(|(m, _)| m.stage())
.max()
.ok_or_else(|| {
anyhow!("No methods specified. Try something like `method = \"Bristol Surprise Major\"`.")
})?;
let (music_types, music_displays) = self.music(toml_path, stage)?;
let part_head = parse_row("part head", &self.part_head, stage)?;
let calling_bell = match self.calling_bell {
Some(v) => Bell::from_number(v).ok_or_else(|| {
anyhow::Error::msg("Invalid calling bell: bell number 0 doesn't exist.")
})?,
None => stage.tenor(),
};
let call_display_style = if part_head.is_fixed(calling_bell) {
CallDisplayStyle::CallingPositions(calling_bell)
} else {
CallDisplayStyle::Positional
};
let params = monument::parameters::Parameters {
length: self.length.as_total_length_range(),
stage,
num_comps: self.num_comps,
require_truth: self.require_truth,
methods: self.build_methods(parsed_methods, &part_head, stage)?,
splice_style: self.splice_style.into(),
splice_weight: self.splice_weight,
calls: self.calls(stage)?,
call_display_style,
atw_weight: self.atw_weight,
require_atw: self.require_atw,
start_row: parse_row("start row", &self.start_row, stage)?,
end_row: parse_row("end row", &self.end_row, stage)?,
part_head_group: PartHeadGroup::new(&part_head),
course_weights: self.course_weights(stage)?,
music_types,
start_stroke: self.start_stroke,
};
Ok((params, music_displays))
}
pub fn should_print_atw(&self) -> bool {
self.atw_weight.is_some() && !self.require_atw
}
pub fn config(&self, opts: &crate::args::Options, leak_search_memory: bool) -> Config {
let mut config = Config {
thread_limit: opts.num_threads,
mem_limit: opts.mem_limit,
leak_search_memory,
..Default::default()
};
if let Some(limit) = opts.graph_size_limit.or(self.graph_size_limit) {
config.graph_size_limit = limit;
}
config
}
fn calls(&self, stage: Stage) -> anyhow::Result<CallVec<monument::parameters::Call>> {
let mut call_id_generator = IdGenerator::<CallId>::starting_at_zero();
let mut calls = self.base_calls(&mut call_id_generator, stage)?;
for custom_call in &self.calls {
calls.push(custom_call.as_monument_call(call_id_generator.next(), stage)?);
}
Ok(calls)
}
fn base_calls(
&self,
id_gen: &mut IdGenerator<CallId>,
stage: Stage,
) -> anyhow::Result<CallVec<monument::parameters::Call>> {
let base_call_type = match self.base_calls {
BaseCalls::Near => BaseCallType::Near,
BaseCalls::Far => BaseCallType::Far,
BaseCalls::None => return Ok(index_vec![]), };
const BIG_NEGATIVE_WEIGHT: f32 = -100.0;
if let Some(w) = self.bob_weight {
if w <= BIG_NEGATIVE_WEIGHT {
log::warn!("It looks like you're trying to make a singles only composition; consider using `singles_only = true` explicitly.");
}
}
if let Some(w) = self.single_weight {
if w <= BIG_NEGATIVE_WEIGHT {
log::warn!("It looks like you're trying to make a bobs only composition; consider using `bobs_only = true` explicitly.");
}
}
if self.bobs_only && self.singles_only {
return Err(anyhow::Error::msg(
"Composition can't be both `bobs_only` and `singles_only`",
));
}
Ok(monument::parameters::base_calls(
id_gen,
base_call_type,
(!self.singles_only).then_some(self.bob_weight.unwrap_or(DEFAULT_BOB_WEIGHT)),
(!self.bobs_only).then_some(self.single_weight.unwrap_or(DEFAULT_SINGLE_WEIGHT)),
stage,
))
}
fn music(
&self,
toml_path: &Path,
stage: Stage,
) -> anyhow::Result<(MusicTypeVec<MusicType>, Vec<MusicDisplay>)> {
let music_file_str = match &self.music_file {
Some(relative_music_path) => {
let mut music_path = toml_path
.parent()
.expect("files should always have a parent")
.to_owned();
music_path.push(relative_music_path);
Some(crate::utils::read_file_to_string(&music_path)?)
}
None => None,
};
crate::music::generate_music(
&self.music,
self.base_music,
music_file_str.as_deref(),
stage,
)
}
fn course_weights(&self, stage: Stage) -> anyhow::Result<Vec<(Mask, f32)>> {
let mut course_weights = self.parse_ch_weights(stage)?;
if self.handbell_coursing_weight != 0.0 {
for right_bell in stage.bells().step_by(2) {
let left_bell = right_bell + 1;
for (b1, b2) in [(left_bell, right_bell), (right_bell, left_bell)] {
let mask_string = format!("*{b1}{b2}");
let mask = Mask::parse_with_stage(&mask_string, stage).unwrap();
course_weights.push((mask, self.handbell_coursing_weight));
}
}
}
Ok(course_weights)
}
fn parse_ch_weights(&self, stage: Stage) -> anyhow::Result<Vec<(Mask, f32)>> {
let mut weights = Vec::new();
if self.ch_weights.is_some() {
anyhow::bail!("`ch_weights` has been renamed to `course_weights`");
}
for pattern in &self.course_weights {
use CourseWeightPattern::*;
let (ch_masks, weight) = match pattern {
Pattern { pattern, weight } => (std::slice::from_ref(pattern), weight),
Patterns { patterns, weight } => (patterns.as_slice(), weight),
};
for mask_str in ch_masks {
weights.push((parse_mask("course head weight", mask_str, stage)?, *weight));
}
}
Ok(weights)
}
fn build_methods(
&self,
parsed_methods: Vec<(bellframe::Method, MethodCommon)>,
part_head: &Row,
stage: Stage,
) -> anyhow::Result<MethodVec<monument::parameters::Method>> {
for (method, _) in &parsed_methods {
if self.base_calls != BaseCalls::None {
match method.name.as_str() {
"Grandsire" | "Stedman" => log::warn!(
"It looks like you're using Plain Bob calls in {}. Try `base_calls = \"none\"`?",
method.name
),
_ => {}
}
}
}
let default_method_count = OptionalRangeInclusive::from(self.method_count);
let default_start_indices = match &self.start_indices {
Some(indices) => indices.clone(),
None if self.snap_start => vec![2],
None => vec![0],
};
let default_allowed_courses = match &self.courses {
Some(ch_strings) => parse_masks("course mask", ch_strings, stage)?,
None if self.split_tenors => vec![Mask::any(stage)],
None => {
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 id_gen = IdGenerator::<MethodId>::starting_at_zero();
let mut methods = MethodVec::new();
for (mut method, common) in parsed_methods {
let lead_len_isize = method.lead_len() as isize;
let wrap_idxs = |idxs: Vec<isize>| -> Vec<usize> {
let mut wrapped_idxs = Vec::new();
for idx in idxs {
let wrapped_idx = ((idx % lead_len_isize) + lead_len_isize) % lead_len_isize;
wrapped_idxs.push(wrapped_idx as usize);
}
wrapped_idxs
};
if common.lead_locations.is_some() {
anyhow::bail!("`methods.lead_locations` has been renamed to `labels`");
}
if common.course_heads.is_some() {
anyhow::bail!("`methods.course_heads` has been renamed to `courses`");
}
let labels = common.labels.unwrap_or_else(|| {
hmap::hmap! {
LABEL_LEAD_END.to_owned() => LeadLabels::JustOne(0)
}
});
for (label, indices) in labels {
for idx in wrap_idxs(indices.into_indices()) {
method.add_label(idx, label.clone());
}
}
let allowed_courses = match common.courses {
Some(ch_strings) => parse_masks("course mask", &ch_strings, stage)?,
None => default_allowed_courses.clone(),
};
let start_indices = common
.start_indices
.unwrap_or_else(|| default_start_indices.clone());
let end_indices = common
.end_indices
.unwrap_or_else(|| match &self.end_indices {
Some(idxs) => idxs.clone(),
None => (0..method.lead_len() as isize).collect_vec(),
});
methods.push(monument::parameters::Method {
id: id_gen.next(),
inner: method,
custom_shorthand: common.shorthand.unwrap_or_default(),
count_range: OptionalRangeInclusive::from(common.count_range)
.or(default_method_count),
start_indices,
end_indices,
allowed_courses: vec![monument::parameters::CourseSet::from(allowed_courses)],
});
}
Ok(methods)
}
}
fn parse_row(name: &str, s: &str, stage: Stage) -> Result<RowBuf, anyhow::Error> {
RowBuf::parse_with_stage(s, stage)
.map_err(|e| anyhow::Error::msg(format!("Can't parse {} {:?}: {}", name, s, e)))
}
fn parse_masks(mask_kind: &str, strings: &[String], stage: Stage) -> anyhow::Result<Vec<Mask>> {
let mut masks = Vec::with_capacity(strings.len());
for s in strings {
masks
.push(Mask::parse_with_stage(s, stage).map_err(|e| mask_parse_error(mask_kind, s, e))?);
}
Ok(masks)
}
fn parse_mask(mask_kind: &str, string: &str, stage: Stage) -> anyhow::Result<Mask> {
Mask::parse_with_stage(string, stage).map_err(|e| mask_parse_error(mask_kind, string, e))
}
#[derive(Debug, Deserialize)]
#[serde(untagged, deny_unknown_fields)]
pub enum CourseWeightPattern {
Pattern { pattern: String, weight: f32 },
Patterns { patterns: Vec<String>, weight: f32 },
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged, deny_unknown_fields)]
pub enum TomlMethod {
JustTitle(String),
FromCcLib {
title: String,
#[serde(flatten)]
common: MethodCommon,
},
Custom {
name: String,
place_notation: String,
stage: Stage,
#[serde(flatten)]
common: MethodCommon,
},
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MethodCommon {
shorthand: Option<String>,
#[serde(default, rename = "count")]
count_range: OptRangeInclusive,
labels: Option<HashMap<String, LeadLabels>>,
lead_locations: Option<HashMap<String, LeadLabels>>,
courses: Option<Vec<String>>,
course_heads: Option<Vec<String>>, start_indices: Option<Vec<isize>>,
end_indices: Option<Vec<isize>>,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Deserialize)]
pub enum SpliceStyle {
#[serde(rename = "leads")]
LeadLabels,
#[serde(rename = "calls")]
Calls,
}
impl From<self::SpliceStyle> for monument::parameters::SpliceStyle {
fn from(style: self::SpliceStyle) -> Self {
match style {
self::SpliceStyle::LeadLabels => monument::parameters::SpliceStyle::LeadLabels,
self::SpliceStyle::Calls => monument::parameters::SpliceStyle::Calls,
}
}
}
impl Default for SpliceStyle {
fn default() -> Self {
Self::LeadLabels
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum LeadLabels {
JustOne(isize),
Many(Vec<isize>),
}
impl LeadLabels {
fn into_indices(self) -> Vec<isize> {
match self {
Self::JustOne(idx) => vec![idx],
Self::Many(indices) => indices,
}
}
}
const NUM_METHOD_SUGGESTIONS: usize = 10;
impl TomlMethod {
fn as_bellframe_method(&self, cc_lib: &MethodLib) -> anyhow::Result<bellframe::Method> {
match self {
TomlMethod::JustTitle(title) | TomlMethod::FromCcLib { title, .. } => cc_lib
.get_by_title_with_suggestions(title, NUM_METHOD_SUGGESTIONS)
.map_err(|error| match error {
SearchError::PnParseErr { pn, error } => {
panic!("Error parsing {pn} in CCCBR library: {error}")
}
SearchError::NotFound(suggestions) => {
anyhow::Error::msg(method_suggestion_message(title, suggestions))
}
}),
TomlMethod::Custom {
name,
place_notation,
stage,
common: _,
} => bellframe::Method::from_place_not_string(name.to_owned(), *stage, place_notation)
.map_err(|error| anyhow::Error::msg(pn_parse_err_msg(name, place_notation, error))),
}
}
fn common(&self) -> MethodCommon {
match self {
TomlMethod::JustTitle(_) => MethodCommon::default(),
TomlMethod::FromCcLib { common, .. } => common.clone(),
TomlMethod::Custom { common, .. } => common.clone(),
}
}
}
#[derive(Debug, Deserialize)]
#[serde(untagged, deny_unknown_fields)]
#[allow(dead_code)] enum CourseSet {
OneMask(String),
WithOptions {
courses: Vec<String>,
#[serde(default)]
any_stroke: bool,
#[serde(default)]
any_bells: bool,
},
}
impl CourseSet {
#[allow(dead_code)] fn as_monument_course_set(
&self,
mask_kind: &str,
stage: Stage,
) -> anyhow::Result<monument::parameters::CourseSet> {
Ok(match self {
CourseSet::OneMask(mask_str) => {
monument::parameters::CourseSet::from(parse_mask(mask_kind, mask_str, stage)?)
}
CourseSet::WithOptions {
courses: masks,
any_stroke,
any_bells,
} => monument::parameters::CourseSet {
masks: parse_masks(mask_kind, masks, stage)?,
any_stroke: *any_stroke,
any_bells: *any_bells,
},
})
}
}
mod length {
use std::{
fmt,
ops::{Range, RangeInclusive},
};
use monument::utils::TotalLength;
use serde::{
de::{Error, MapAccess, Visitor},
Deserialize, Deserializer,
};
pub const PRACTICE: RangeInclusive<usize> = 0..=300;
pub const QP: RangeInclusive<usize> = 1250..=1350;
pub const HALF_PEAL: RangeInclusive<usize> = 2500..=2600;
pub const PEAL: RangeInclusive<usize> = 5000..=5200;
#[derive(Debug, Clone)]
#[repr(transparent)]
pub(super) struct Length {
pub(super) range: RangeInclusive<usize>,
}
impl Length {
pub(super) fn as_total_length_range(&self) -> RangeInclusive<TotalLength> {
let start = TotalLength::new(*self.range.start());
let end = TotalLength::new(*self.range.end());
start..=end
}
}
impl From<usize> for Length {
#[inline(always)]
fn from(v: usize) -> Length {
Length { range: v..=v }
}
}
impl From<Range<usize>> for Length {
#[inline(always)]
fn from(r: Range<usize>) -> Length {
Length {
range: r.start..=r.end - 1,
}
}
}
impl From<RangeInclusive<usize>> for Length {
#[inline(always)]
fn from(range: RangeInclusive<usize>) -> Length {
Length { range }
}
}
struct LengthVisitor;
impl<'de> Visitor<'de> for LengthVisitor {
type Value = Length;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str(
r#"a positive integer, a 'min/max' range, "practice", "qp", "half peal" or "peal""#,
)
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
where
E: Error,
{
de_signed_num(v)
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: Error,
{
Ok(Length::from(v as usize))
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
#[derive(Deserialize)]
#[serde(field_identifier, rename_all = "lowercase", deny_unknown_fields)]
enum Fields {
Min,
Max,
}
let mut min: Option<usize> = None;
let mut max: Option<usize> = None;
while let Some(key) = map.next_key()? {
match key {
Fields::Min => {
if min.is_some() {
return Err(Error::duplicate_field("min"));
}
min = Some(map.next_value()?);
}
Fields::Max => {
if max.is_some() {
return Err(Error::duplicate_field("max"));
}
max = Some(map.next_value()?);
}
}
}
let min = min.ok_or_else(|| Error::missing_field("min"))?;
let max = max.ok_or_else(|| Error::missing_field("max"))?;
Ok(Length::from(min..=max))
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: Error,
{
let lower_str = v.to_lowercase();
Ok(Length::from(match lower_str.as_str() {
"practice" => PRACTICE,
"qp" => QP,
"quarter peal" => QP,
"peal" => PEAL,
"half peal" => HALF_PEAL,
_ => return Err(E::custom(format!("unknown length name '{}'", v))),
}))
}
}
#[inline(always)]
fn de_signed_num<E: Error>(v: i64) -> Result<Length, E> {
if v >= 0 {
Ok(Length::from(v as usize))
} else {
Err(E::custom(format!("negative length: {}", v)))
}
}
impl<'de> Deserialize<'de> for Length {
fn deserialize<D>(deserializer: D) -> Result<Length, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(LengthVisitor)
}
}
}
fn method_suggestion_message(title: &str, mut suggestions: Vec<(String, usize)>) -> String {
suggestions.sort_by_key(|(name, edit_dist)| (*edit_dist, name.clone()));
let mut message = format!(
"Can't find {:?} in the Central Council method library.",
title
);
message.push_str(" Did you mean:");
let best_edit_dist = suggestions[0].1;
let mut is_first_suggestion = true;
for (suggested_title, edit_dist) in suggestions {
if edit_dist > best_edit_dist {
break;
}
message.push('\n');
message.push_str(if is_first_suggestion {
" "
} else {
" or "
});
write!(
message,
"{:?} ({})",
suggested_title,
difference::Changeset::new(title, &suggested_title, "")
)
.unwrap();
is_first_suggestion = false;
}
message
}
fn pn_parse_err_msg(name: &str, pn_str: &str, error: PnBlockParseError) -> String {
let (region, message) = match error {
PnBlockParseError::EmptyBlock => {
return format!(
"Can't have empty place notation block for method {:?}",
name
);
}
PnBlockParseError::PlusNotAtBlockStart(plus_idx) => (
plus_idx..plus_idx + 1,
"`+` must only go at the start of a block (i.e. at the start or directly after a `,`)"
.to_owned(),
),
PnBlockParseError::PnError(range, err) => (range, err.to_string()),
};
let pn_before_error = &pn_str[..region.start];
let pn_error = &pn_str[region.clone()];
let pn_after_error = &pn_str[region.end..];
let mut msg = format!("Can't parse place notation for method {:?}:\n", name);
writeln!(
msg,
" \"{}{}{}\"",
pn_before_error,
pn_error.bright_red().bold(),
pn_after_error
)
.unwrap();
let chars_before_plus = pn_before_error.chars().count();
let caret_string = std::iter::repeat('^')
.take(pn_error.chars().count())
.join("")
.bright_red()
.bold();
msg.extend(std::iter::repeat(' ').take(6 + chars_before_plus)); write!(msg, "{} {}", caret_string, message.bright_red().bold()).unwrap();
msg
}
fn mask_parse_error(
mask_kind: &str,
string: &str,
e: bellframe::mask::ParseError,
) -> anyhow::Error {
anyhow::Error::msg(format!("Can't parse {mask_kind} {string:?}: {e}"))
}
fn default_num_comps() -> usize {
100
}