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}