bellframe/method/
mod.rs

1use itertools::Itertools;
2
3use crate::{place_not::PnBlockParseError, Block, PnBlock, Row, RowBuf, Stage};
4
5use self::class::FullClass;
6
7pub mod class;
8
9/// A standard label name used for denoting the 'lead end' of a method
10pub const LABEL_LEAD_END: &str = "LE";
11
12/// The definition of a 'method' within Change Ringing.  Essentially, a `Method` consists of a
13/// [`Block`] which is intended to be rung as a repeating unit (usually a 'lead'), along with
14/// 'labels' for specific locations within this [`Block`].  Calls can then be attached to these
15/// locations (by their label), and thus the single lead can be modified to determine the effect of
16/// calls in a general way.  This follows how [CompLib](https://complib.org)'s composition input
17/// works.
18#[derive(Debug, Clone)]
19pub struct Method {
20    pub name: String,
21    omit_class: bool, // Set to `true` for methods like Grandsire, who's title omits the class
22    class: FullClass,
23    /// The first lead of this [`Method`], where each row can be given any number of arbitrary
24    /// labels.
25    // TODO: Use a `HashMap<(usize, String)>` to store lead labels?
26    first_lead: Block<Vec<String>>,
27}
28
29impl Method {
30    //////////////////
31    // CONSTRUCTORS //
32    //////////////////
33
34    /// Creates a new `Method` from its raw parts
35    pub fn new(
36        name: String,
37        class: FullClass,
38        omit_class: bool,
39        first_lead: Block<Vec<String>>,
40    ) -> Self {
41        Self {
42            name,
43            omit_class,
44            class,
45            first_lead,
46        }
47    }
48
49    /// Create and classify a new `Method`, given its name and first lead
50    pub fn with_name(name: String, first_lead: Block<Vec<String>>) -> Self {
51        let class = FullClass::classify(&first_lead);
52        Self {
53            name,
54            omit_class: false,
55            class,
56            first_lead,
57        }
58    }
59
60    /// Parses a place notation string and creates a `Method` with that place notation and no
61    /// labels.
62    pub fn from_place_not_string(
63        name: String,
64        stage: Stage,
65        place_notation: &str,
66    ) -> Result<Self, PnBlockParseError> {
67        Ok(Self::with_name(
68            name,
69            PnBlock::parse(place_notation, stage)?.to_block_from_rounds(),
70        ))
71    }
72
73    /// Creates a new `Method` from some place notation, adding a [`LABEL_LEAD_END`] on the first
74    /// row.
75    pub fn with_lead_end(name: String, block: &PnBlock) -> Self {
76        let mut first_lead: Block<Vec<String>> = block.to_block_from_rounds();
77        first_lead
78            .get_annot_mut(0)
79            .unwrap()
80            .push(LABEL_LEAD_END.to_owned());
81        Self::with_name(name, first_lead)
82    }
83
84    /////////////
85    // GETTERS //
86    /////////////
87
88    /// Returns an [`Block`] of the first lead of this [`Method`], along with the labels applied to
89    /// each [`Row`].
90    #[inline]
91    pub fn first_lead(&self) -> &Block<Vec<String>> {
92        &self.first_lead
93    }
94
95    /// The last [`Row`] of the [first lead](Self::first_lead).  Don't confuse this with the
96    /// **[lead head](Self::lead_head)**: 'lead **end**' refers to the treble's _handstroke_ lead,
97    /// whereas 'lead **head**' refers to the treble's _backstroke_ lead.
98    ///
99    /// # Panics
100    ///
101    /// Panics if this method has a 0-length lead.
102    #[inline]
103    #[track_caller]
104    pub fn lead_end(&self) -> &Row {
105        self.first_lead
106            .rows()
107            .last()
108            .expect("`Method::lead_end` called on a method with a 0-length lead")
109    }
110
111    /// The overall transposing effect of one lead of this `Method`.
112    #[inline]
113    pub fn lead_head(&self) -> &Row {
114        self.first_lead.leftover_row()
115    }
116
117    /// How many [`Row`]s are in a single lead of this `Method`?
118    #[inline]
119    pub fn lead_len(&self) -> usize {
120        self.first_lead.len()
121    }
122
123    /// How many [`Row`]s are in one course of this `Method`?
124    pub fn course_len(&self) -> usize {
125        self.lead_len() * self.lead_head().order()
126    }
127
128    /// Gets the [`Stage`] of this `Method`
129    #[inline]
130    pub fn stage(&self) -> Stage {
131        self.first_lead.stage()
132    }
133
134    #[inline]
135    pub fn class(&self) -> FullClass {
136        self.class
137    }
138
139    /// Gets the **title** of this `Method` - i.e. including the classification or [`Stage`].
140    /// Take Bristol Major as an example: its name is `"Bristol"` but its title is `"Bristol
141    /// Surprise Major"`.
142    pub fn title(&self) -> String {
143        generate_title(&self.name, self.class, self.omit_class, self.stage())
144    }
145
146    //////////////////////////////
147    // BLOCK-RELATED OPERATIONS //
148    //////////////////////////////
149
150    /// Returns the [`Row`] at some index in the plain lead of this `Method`.
151    ///
152    /// # Panics
153    ///
154    /// Panics if the sub-lead index is larger than the first lead
155    pub fn row_in_plain_lead(&self, idx: usize) -> &Row {
156        &self.first_lead.row_vec()[idx]
157    }
158
159    /// Returns the [`Row`] at some index in the infinite plain course of this `Method`.
160    pub fn row_in_plain_course(&self, idx: usize) -> RowBuf {
161        let num_leads = idx / self.lead_len();
162        let sub_lead_idx = idx % self.lead_len();
163
164        let lead_head = self.lead_head().pow_u(num_leads);
165        let row_within_lead = self.row_in_plain_lead(sub_lead_idx);
166        lead_head.as_row() * row_within_lead
167    }
168
169    /// Returns an [`Block`] representing the plain course of this method
170    pub fn plain_course(&self) -> Block<RowAnnot> {
171        // Create a copy of `self.first_lead` where each row is also annotated by its index within
172        // this lead
173        let first_lead_with_indices = self
174            .first_lead
175            .clone_map_annots_with_index(|i, labels| RowAnnot::new(i, labels));
176
177        // Start with the first lead, and repeatedly add leads until we get back to rounds
178        let mut plain_course = first_lead_with_indices;
179        while !plain_course.leftover_row().is_rounds() {
180            plain_course.extend_from_within(0..self.lead_len());
181        }
182        plain_course
183    }
184
185    //////////////////////
186    // LABEL OPERATIONS //
187    //////////////////////
188
189    /// Sets or clears the label at a given index, panicking if the index is out of range
190    pub fn add_label(&mut self, index: usize, label: String) {
191        let labels = self.first_lead.get_annot_mut(index).unwrap();
192        if !labels.contains(&label) {
193            labels.push(label);
194        }
195    }
196
197    /// Same as `self.set_label(0, Some(LABEL_LEAD_END.to_owned()))`
198    pub fn set_lead_end_label(&mut self) {
199        self.add_label(0, LABEL_LEAD_END.to_owned())
200    }
201
202    /// Returns the label at a given index, panicking if the index is out of range
203    // TODO: Make this not panic
204    pub fn get_labels(&self, index: usize) -> &[String] {
205        self.first_lead.get_annot(index).unwrap()
206    }
207
208    /// An [`Iterator`] over the sub-lead indices of a particular lead label.
209    pub fn label_indices<'s>(&'s self, label: &'s str) -> impl Iterator<Item = usize> + 's {
210        self.first_lead
211            .annots()
212            .positions(move |labels| labels.iter().any(|l| l == label))
213    }
214
215    /// An [`Iterator`] over the sub-lead indices of a particular lead label.
216    pub fn all_label_indices(&self) -> Vec<(&str, usize)> {
217        let mut label_indices = Vec::new();
218        for (sub_lead_idx, labels) in self.first_lead.annots().enumerate() {
219            for label in labels {
220                label_indices.push((label.as_str(), sub_lead_idx));
221            }
222        }
223        label_indices
224    }
225
226    /// Gets the closest labels **after** `sub_lead_idx`, along with their distance from that index.
227    ///
228    /// This will wrap around the end of the lead if needed.  Note that labels at `sub_lead_idx`
229    /// are treated as a whole lead away (so the returned distance can never be 0).
230    pub fn next_labels(&self, sub_lead_idx: usize) -> (&[String], usize) {
231        for distance in 1..=self.lead_len() {
232            let labels = self.get_labels((sub_lead_idx + distance) % self.lead_len());
233            if !labels.is_empty() {
234                return (labels, distance);
235            }
236        }
237        panic!("`next_labels` called on a method with no labels")
238    }
239
240    /// Gets the closest labels **before** `sub_lead_idx`, along with their distance from that
241    /// index.
242    ///
243    /// Note that, unlike `next_labels`, a distance of `0` is possible if there are labels exactly
244    /// on the given `sub_lead_idx`.
245    pub fn prev_labels(&self, sub_lead_idx: usize) -> (&[String], usize) {
246        for distance in 0..self.lead_len() {
247            let labels =
248                self.get_labels((sub_lead_idx + self.lead_len() - distance) % self.lead_len());
249            if !labels.is_empty() {
250                return (labels, distance);
251            }
252        }
253        panic!("`prev_labels` called on a method with no labels")
254    }
255}
256
257/// Generate the (standard) title of a [`Method`] from its parts, according to the Framework's
258/// rules.  Some methods (e.g. Grandsire and Union) do not follow this convention, and therefore
259/// their titles must be stored separately.
260pub fn generate_title(name: &str, class: FullClass, omit_class: bool, stage: Stage) -> String {
261    let mut s = String::new();
262
263    // Push the name, followed by a space (if the name is non-empty)
264    s.push_str(name);
265    if !name.is_empty() {
266        s.push(' ');
267    }
268    // Push the classification, and add another space if the classification wasn't the empty string
269    // (which we check indirectly by measuring the length of `s` before and after adding the
270    // classification string)
271    if !omit_class {
272        let len_before_class = s.len();
273        class.fmt_name(&mut s, true).unwrap();
274        if s.len() > len_before_class {
275            // If the class made the string longer, we need another space before the stage
276            s.push(' ');
277        }
278    }
279    // Always push the stage
280    s.push_str(stage.name().expect("Stage was too big to generate a name"));
281
282    s
283}
284
285/// The source of a [`Row`] within a [`Method`]
286#[derive(Debug, Clone)]
287pub struct RowAnnot<'meth> {
288    pub sub_lead_idx: usize,
289    pub labels: &'meth [String],
290}
291
292impl<'meth> RowAnnot<'meth> {
293    fn new(sub_lead_idx: usize, labels: &'meth [String]) -> Self {
294        Self {
295            sub_lead_idx,
296            labels,
297        }
298    }
299}