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
//! Provides functionality to calculate the chance to beat the Personal Best for
//! either a [`Run`](crate::Run) or a [`Timer`](crate::timing::Timer). For a
//! [`Run`](crate::Run) it calculates the general chance to beat the Personal Best.
//! For a [`Timer`](crate::timing::Timer) the chance is calculated in terms of the
//! current attempt. If there is no attempt in progress it yields the same
//! result as the PB chance for the run. The value is being reported as a
//! floating point number in the range from 0 (0%) to 1 (100%).
//!
//! The PB chance is currently calculated with the skill curve. The PB chance is
//! the percentile at which the PB is located on the skill curve. This is also
//! where the [`BalancedPB`](crate::comparison::balanced_pb::BalancedPB) would
//! source its split times.

use super::SkillCurve;
use crate::{comparison, timing::Snapshot, Run, Segment, TimeSpan, TimingMethod};

#[cfg(test)]
mod tests;

fn calculate(segments: &[Segment], method: TimingMethod, offset: TimeSpan) -> f64 {
    if segments
        .last()
        .and_then(|s| s.personal_best_split_time()[method])
        .is_none()
    {
        // If there is no PB time, then it's always a 100% chance.
        return 1.0;
    }

    comparison::goal::determine_percentile(offset, segments, method, None, &mut SkillCurve::new())
}

/// Calculates the PB chance for a [`Run`](crate::Run). No information about
/// an active attempt is used. Instead the general chance to beat the Personal
/// Best is calculated. The value is being reported as a floating point number
/// in the range from 0 (0%) to 1 (100%).
pub fn for_run(run: &Run, method: TimingMethod) -> f64 {
    calculate(run.segments(), method, TimeSpan::zero())
}

/// Calculates the PB chance for a [`Timer`](crate::timing::Timer). The chance
/// is calculated in terms of the current attempt. If there is no attempt in
/// progress it yields the same result as the PB chance for the run.
/// The value is being reported as a floating point number in the range
/// from 0 (0%) to 1 (100%). Additionally a boolean is returned that
/// indicates if the value is currently actively changing as time is being lost.
pub fn for_timer(timer: &Snapshot<'_>) -> (f64, bool) {
    let method = timer.current_timing_method();
    let all_segments = timer.run().segments();

    let is_live =
        super::check_live_delta(timer, false, comparison::personal_best::NAME, method).is_some();

    let (segments, current_time) = if is_live {
        // If there is a live delta, act as if we did just split.
        (
            &all_segments[timer.current_split_index().unwrap() + 1..],
            timer.current_time()[method].unwrap_or_default(),
        )
    } else if let Some((index, time)) = all_segments
        .iter()
        .enumerate()
        .rev()
        .find_map(|(i, s)| Some((i, s.split_time()[method]?)))
    {
        // Otherwise fall back to the the last split that we did split.
        (&all_segments[index + 1..], time)
    } else {
        // Otherwise fall back to all segments with a timer that didn't really
        // start.
        (all_segments, TimeSpan::zero())
    };

    // If there are no more segments, which can be because either there is a
    // live delta and we are on the final split, or if we actually did split the
    // final split, then we want to simply compare the current time to the PB
    // time and then either return 100% or 0% based on whether our new time is a
    // PB or not.
    let chance = if segments.is_empty() {
        let beat_pb = all_segments
            .last()
            .and_then(|s| s.personal_best_split_time()[method])
            .map_or(true, |pb| current_time < pb);
        if beat_pb {
            1.0
        } else {
            0.0
        }
    } else {
        calculate(segments, method, current_time)
    };

    (chance, is_live && timer.current_phase().is_running())
}