1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
//! Code for handling the logging of compositions or updates provided by Monument
use std::{fmt::Write, io::Write as IoWrite, sync::Arc};
use bellframe::row::ShortRow;
use colored::Colorize;
use itertools::Itertools;
use log::log_enabled;
use monument::{Composition, Progress, Search, Update};
use ringing_utils::BigNumInt;
use crate::music::MusicDisplay;
/// Struct which handles logging updates, keeping the updates to a single line which updates as the
/// search progresses.
pub struct SingleLineProgressLogger {
// Set to `None` if the `--only-update-line` option is set
comp_printer: Option<CompositionPrinter>,
last_progress: Progress,
/// The number of characters in the last line we printed. `UpdateLogger` will use this add
/// enough spaces to the end of the next message to completely overwrite the last one
last_line_length: usize,
}
impl SingleLineProgressLogger {
pub fn new(comp_printer: Option<CompositionPrinter>) -> Self {
Self {
comp_printer,
last_progress: Progress::START,
last_line_length: 0,
}
}
pub fn log(&mut self, update: Update, comps_generated_so_far: usize) -> Option<Composition> {
// Early return if we can't log anything, making sure to still keep the composition
if !log_enabled!(log::Level::Info) {
return match update {
Update::Comp(c) => Some(c),
_ => None,
};
}
let comp = self.update_progress(update);
// Decide what string we're going to print. This may have multiple lines (if a comp was
// generated).
let mut update_string = String::new();
if let (Some(printer), Some(c)) = (&mut self.comp_printer, &comp) {
update_string
.push_str(&printer.comp_string_with_possible_headers(c, comps_generated_so_far));
update_string.push('\n');
}
self.append_progress_string(&mut update_string);
let update_string = self.extend_string(&update_string);
let std_err = std::io::stderr();
let mut std_err = std_err.lock();
// We precede with a carriage return to make sure that we overwrite anything the user
// types (e.g. `^C`). We don't do anything in the case that the user's input is longer
// than what we're writing - we'll just assume that no-one would be able to type that much
// in between updates (which happen many many times per second).
write!(std_err, "\r{}\r", update_string).unwrap();
std_err.flush().unwrap(); // `std_out` won't flush by default without a newline
comp
}
/// Given a new update, update `self` and return the [`Composition`] (if one has just been
/// generated)
fn update_progress(&mut self, update: Update) -> Option<Composition> {
match update {
Update::Comp(comp) => return Some(comp),
Update::Progress(progress) => self.last_progress = progress,
Update::Complete => {} // Nothing to do, we're just about to return anyway
}
None
}
/// Append a progress summary to some [`String`] buffer
fn append_progress_string(&self, buf: &mut String) {
let p = self.last_progress;
write!(
buf,
" {} iters, {} comps :: {} items in queue, avg/max len {:.0}/{}",
BigNumInt(p.iter_count),
BigNumInt(p.num_comps),
BigNumInt(p.queue_len),
p.avg_length,
p.max_length
)
.unwrap();
buf.push_str(match (p.aborting, p.truncating_queue) {
(false, false) => "",
(true, false) => ". Aborting...",
(false, true) => ". Truncating queue...",
(true, true) => unreachable!("Must either be aborting or truncating queue"),
});
}
/// Add whitespace to the end of a string to make sure it will cover the last thing we printed.
/// This updates `self.last_update_length` for the next update.
fn extend_string(&mut self, s: &str) -> String {
let (first_line, other_lines) = match s.split_once('\n') {
Some((f, o)) => (f, Some(o)),
None => (s, None),
};
let num_spaces = self.last_line_length.saturating_sub(first_line.len());
let mut output = String::new();
output.push_str(first_line);
output.extend(std::iter::repeat(' ').take(num_spaces)); // Add spaces to the first line
if let Some(o) = other_lines {
output.push('\n');
output.push_str(o);
}
// Set `self.last_update_length` for next time. `self.last_update_length` is the length of
// the last line of `output` (the `+ 1` gives us the index **after** the '\n')
self.last_line_length = output.len() - output.rfind('\n').map_or(0, |n| n + 1);
output
}
}
#[derive(Debug, Clone)]
pub struct CompositionPrinter {
search: Arc<Search>,
music_displays: Vec<MusicDisplay>,
/// Counter which records how many compositions have been printed so far
comps_printed: usize,
/* COLUMN WIDTH INFORMATION */
/// The maximum width of a composition's number (i.e. the width of the total number of
/// compositions). If `None`, no composition numbers are printed.
comp_count_width: Option<usize>,
/// The maximum width of a composition's (total) length
length_width: usize,
/// For each method in the composition:
/// ```text
/// (
/// maximum width of row count,
/// shorthand
/// )
/// ```
method_count_widths: Vec<(usize, String)>,
/// `true` if the user gave some weight to atw
print_atw: bool,
/// If a part head should be displayed, then what's its width
part_head_width: Option<usize>,
/// The column widths of every `MusicDisplay` in the output
music_widths: Vec<usize>,
}
impl CompositionPrinter {
pub fn new(
music_displays: Vec<MusicDisplay>,
search: Arc<Search>,
print_atw: bool,
print_comp_widths: bool,
) -> Self {
Self {
comp_count_width: print_comp_widths
.then_some(search.parameters().num_comps.to_string().len()),
length_width: search.parameters().max_length().to_string().len().max(3),
method_count_widths: search
.methods()
.map(|(method, shorthand)| {
let max_count_width =
search.method_count_range(method.id).end().to_string().len();
let max_width = max_count_width.max(shorthand.len());
(max_width, shorthand)
})
.collect_vec(),
comps_printed: 0,
print_atw,
part_head_width: (search.num_parts() > 2)
.then(|| search.effective_part_head_stage().num_bells()),
music_widths: music_displays
.iter()
.map(|d| d.col_width(search.parameters()))
.collect_vec(),
search,
music_displays,
}
}
/// Create some lines which summarise the given [`Composition`]. This may include additional
/// lines for headers or ruleoffs, depending on how many compositions have been printed so far.
pub fn comp_string_with_possible_headers(
&mut self,
comp: &Composition,
generation_index: usize,
) -> String {
let mut update_string = String::new();
// Add a header every 50 lines
if self.comps_printed % 50 == 0 {
if self.comps_printed > 0 {
update_string.push_str(&self.ruleoff());
update_string.push('\n');
}
update_string.push_str(&self.header());
update_string.push('\n');
update_string.push_str(&self.ruleoff());
update_string.push('\n');
}
// Add the composition
update_string.push_str(&self.comp_string(comp, generation_index));
self.comps_printed += 1;
update_string
}
/// Return some lines which end a composition list. This includes one line for a ruleoff and
/// one for the column headers, like:
/// ```text
/// --------------|---------|-----------|-----------
/// len Y C | music | avg score | calling
/// ```
pub fn footer_lines(&self) -> String {
let mut s = String::new();
s.push_str(&self.ruleoff());
s.push('\n');
s.push_str(&self.header());
s
}
fn ruleoff(&self) -> String {
// Ruleoff is the same as header, but with every non-'|' char replaced with '-'
let mut ruleoff = self
.header()
.chars()
.map(|c| if c == '|' { '|' } else { '-' })
.collect::<String>();
ruleoff.push_str("---"); // Add a couple of extra `-`s to make the ruleoff a bit longer
ruleoff
}
fn header(&self) -> String {
let mut s = String::new();
// Comp index
if let Some(c) = self.comp_count_width {
write_centered_text(&mut s, "#", c);
s.push_str(" | ");
}
// Length
write_centered_text(&mut s, "len", self.length_width);
s.push(' ');
// Method shorthands (for counts)
if self.method_count_widths.len() > 1 {
s.push_str(" ");
for (width, shorthand) in &self.method_count_widths {
write_centered_text(&mut s, shorthand, *width);
s.push(' ');
}
}
s.push('|');
// Atw
if self.print_atw {
s.push_str(" atw |");
}
// Part head
if let Some(w) = self.part_head_width {
// Add 2 to the width to get one char of extra padding on either side
write_centered_text(&mut s, "PH", w + 2);
s.push('|');
}
// Music
s.push_str(" music ");
if !self.music_displays.is_empty() {
s.push(' ');
}
for (music_display, col_width) in self.music_displays.iter().zip_eq(&self.music_widths) {
s.push_str(" ");
write_centered_text(&mut s, &music_display.name, *col_width);
s.push(' ');
}
// Everything else
s.push_str("| avg score | calling");
s
}
fn comp_string(&self, comp: &Composition, generation_index: usize) -> String {
let params = self.search.parameters();
let mut s = String::new();
// Comp index
if let Some(c) = self.comp_count_width {
write!(s, "{:>width$} | ", generation_index + 1, width = c).unwrap();
}
// Length
write!(s, "{:>width$} ", comp.length(), width = self.length_width).unwrap();
// Method counts (for spliced)
if self.method_count_widths.len() > 1 {
s.push_str(": ");
for ((width, _), count) in self
.method_count_widths
.iter()
.zip_eq(comp.method_counts(params))
{
write!(s, "{:>width$} ", count, width = *width).unwrap();
}
}
s.push('|');
// Atw
if self.print_atw {
let factor = comp.atw_factor(params);
if factor > 0.999999 {
s.push_str(&format!(" {} |", "atw".color(colored::Color::BrightGreen)));
} else {
write!(s, " {:>2}% |", (factor * 100.0).floor() as usize).unwrap();
}
}
// Part head (if >2 parts; up to 2-parts must always have the same part head)
if self.part_head_width.is_some() {
write!(s, " {} |", ShortRow(comp.part_head())).unwrap();
}
// Music
let (music_counts, music_score) = comp.music_counts_and_score(params); // Only call `music_counts` once!
write!(s, " {:>7.2} ", music_score).unwrap();
if !self.music_displays.is_empty() {
s.push(':');
}
for (music_display, col_width) in self.music_displays.iter().zip_eq(&self.music_widths) {
s.push_str(" ");
write_left_centered_text(
&mut s,
&music_display.display_counts(&music_counts, self.search.parameters()),
*col_width,
);
s.push(' ');
}
// avg score, call string
write!(
s,
"| {:>9.6} | {}",
comp.average_score(),
comp.call_string(params)
)
.unwrap();
s
}
}
/// Write some `string` to `out`, centering it among `width` spaces (rounding to the right).
fn write_centered_text(out: &mut String, text: &str, width: usize) {
let w = width.saturating_sub(text.len());
push_multiple(' ', w - (w / 2), out);
out.push_str(text);
push_multiple(' ', w / 2, out);
}
/// Write some `string` to `out`, centering it among `width` spaces (rounding to the left).
fn write_left_centered_text(out: &mut String, text: &str, width: usize) {
let w = width.saturating_sub(text.len());
push_multiple(' ', w / 2, out);
out.push_str(text);
push_multiple(' ', w - (w / 2), out);
}
/// Push `n` copies of `c` to the end of `out`
fn push_multiple(c: char, n: usize, out: &mut String) {
out.extend(std::iter::repeat(c).take(n));
}