| 245 |
245 |
|
headline,
|
| 246 |
246 |
|
verdict,
|
| 247 |
247 |
|
crossover_note,
|
| 248 |
|
- |
sales_per_month: i.sales_per_month,
|
| 249 |
|
- |
crossover_sales: crossover,
|
|
248 |
+ |
crossover: crossover.map(|at| Crossover {
|
|
249 |
+ |
sales: i.sales_per_month.round().max(0.0) as u32,
|
|
250 |
+ |
at: at.ceil().max(1.0) as u32,
|
|
251 |
+ |
}),
|
| 250 |
252 |
|
}
|
| 251 |
253 |
|
}
|
| 252 |
254 |
|
}
|
| 282 |
284 |
|
pub verdict: Verdict,
|
| 283 |
285 |
|
/// Where the crossover sits, or why there isn't one.
|
| 284 |
286 |
|
pub crossover_note: Option<String>,
|
| 285 |
|
- |
/// The volume the dials were set to, so the panel can say where that sits
|
| 286 |
|
- |
/// against the crossover. Carried rather than recomputed: the reader typed
|
| 287 |
|
- |
/// it, `sanitize` rounded it, and a second rounding downstream is a second
|
| 288 |
|
- |
/// answer.
|
| 289 |
|
- |
pub sales_per_month: f64,
|
| 290 |
|
- |
/// Sales a month at which we become the cheaper place, exactly. `None` when
|
| 291 |
|
- |
/// our per-sale advantage is zero or negative, which is the case no volume
|
| 292 |
|
- |
/// closes.
|
| 293 |
|
- |
pub crossover_sales: Option<f64>,
|
|
287 |
+ |
/// Where the entered volume sits against the crossover. `None` when our
|
|
288 |
+ |
/// per-sale advantage is zero or negative, which is the case no volume
|
|
289 |
+ |
/// closes: there is then no set for the volume to be a proportion of, and
|
|
290 |
+ |
/// [`Outcome::crossover_note`] says so in words instead.
|
|
291 |
+ |
pub crossover: Option<Crossover>,
|
|
292 |
+ |
}
|
|
293 |
+ |
|
|
294 |
+ |
/// The entered volume against the crossover, as the two whole counts a meter
|
|
295 |
+ |
/// is drawn from.
|
|
296 |
+ |
///
|
|
297 |
+ |
/// Whole counts because a proportion is read by a person and a meter takes two
|
|
298 |
+ |
/// `u32`s. They are resolved here rather than where the panel is described,
|
|
299 |
+ |
/// because a description admits no arithmetic and this is a rounding and two
|
|
300 |
+ |
/// casts.
|
|
301 |
+ |
#[derive(Debug, Clone, Copy)]
|
|
302 |
+ |
pub struct Crossover {
|
|
303 |
+ |
/// The volume the dials were set to. Exact, not a second rounding:
|
|
304 |
+ |
/// `sanitize` already rounded it to a whole count and clamped it to
|
|
305 |
+ |
/// `MAX_SALES`.
|
|
306 |
+ |
pub sales: u32,
|
|
307 |
+ |
/// Sales a month at which we become the cheaper place, rounded up. At least
|
|
308 |
+ |
/// one, because a meter drawn against a zero total says nothing a reader
|
|
309 |
+ |
/// can act on.
|
|
310 |
+ |
pub at: u32,
|
| 294 |
311 |
|
}
|
| 295 |
312 |
|
|
| 296 |
313 |
|
/// Format dollars as `$1,234.56`, with the sign ahead of the `$`.
|
| 434 |
451 |
|
let note = out.crossover_note.expect("no-crossover note");
|
| 435 |
452 |
|
assert!(note.contains("no amount of volume"), "note was: {note}");
|
| 436 |
453 |
|
// No volume closes the gap, so there is no crossover to point at.
|
| 437 |
|
- |
assert!(out.crossover_sales.is_none());
|
|
454 |
+ |
assert!(out.crossover.is_none());
|
| 438 |
455 |
|
}
|
| 439 |
456 |
|
|
| 440 |
457 |
|
/// A free tier has nothing to repay, so MNW leads from the first sale and
|
| 450 |
467 |
|
});
|
| 451 |
468 |
|
assert_eq!(out.verdict, Verdict::MnwAhead);
|
| 452 |
469 |
|
assert!(out.crossover_note.is_none());
|
| 453 |
|
- |
assert!(out.crossover_sales.is_none());
|
|
470 |
+ |
assert!(out.crossover.is_none());
|
| 454 |
471 |
|
}
|
| 455 |
472 |
|
|
| 456 |
473 |
|
/// Zero sales is a real answer, not a placeholder: the subscription still
|
| 531 |
548 |
|
approx(inf.other_pct, c.defaults.other_pct, "NaN cut");
|
| 532 |
549 |
|
}
|
| 533 |
550 |
|
|
| 534 |
|
- |
/// A crossover is a count of sales, so it is positive and finite wherever
|
| 535 |
|
- |
/// there is one, at every dial position the inputs allow.
|
|
551 |
+ |
/// A crossover is a count of sales, so it is positive wherever there is
|
|
552 |
+ |
/// one, and the volume it is read against is the one the reader set.
|
| 536 |
553 |
|
///
|
| 537 |
554 |
|
/// This replaces `scale_segments_tile_the_axis`, which checked that two
|
| 538 |
555 |
|
/// hand-computed bar widths summed to 100. The widths went with the
|
| 539 |
556 |
|
/// template that positioned them; the property worth holding is the one the
|
| 540 |
|
- |
/// panel now states, since a meter drawn against a zero or negative total
|
| 541 |
|
- |
/// says nothing a reader can act on.
|
|
557 |
+ |
/// panel now states, since a meter drawn against a zero total says nothing
|
|
558 |
+ |
/// a reader can act on.
|
| 542 |
559 |
|
#[test]
|
| 543 |
560 |
|
fn a_crossover_is_a_positive_count_of_sales_wherever_there_is_one() {
|
| 544 |
561 |
|
let c = loaded();
|
| 552 |
569 |
|
other_pct: pct,
|
| 553 |
570 |
|
other_per_sale: 0.30,
|
| 554 |
571 |
|
});
|
| 555 |
|
- |
if let Some(crossover) = out.crossover_sales {
|
|
572 |
+ |
if let Some(crossover) = out.crossover {
|
| 556 |
573 |
|
assert!(
|
| 557 |
|
- |
crossover.is_finite() && crossover > 0.0,
|
| 558 |
|
- |
"crossover {crossover} at price {price}, {sales} sales, pct {pct}"
|
|
574 |
+ |
crossover.at >= 1,
|
|
575 |
+ |
"crossover at {} at price {price}, {sales} sales, pct {pct}",
|
|
576 |
+ |
crossover.at
|
|
577 |
+ |
);
|
|
578 |
+ |
approx(
|
|
579 |
+ |
f64::from(crossover.sales),
|
|
580 |
+ |
sales,
|
|
581 |
+ |
"the panel is told a volume the reader did not set",
|
| 559 |
582 |
|
);
|
| 560 |
583 |
|
}
|
| 561 |
|
- |
approx(
|
| 562 |
|
- |
out.sales_per_month,
|
| 563 |
|
- |
sales,
|
| 564 |
|
- |
"the panel is told a volume the reader did not set",
|
| 565 |
|
- |
);
|
| 566 |
584 |
|
}
|
| 567 |
585 |
|
}
|
| 568 |
586 |
|
}
|