use crate::{platform::prelude::*, Segment, TimeSpan, TimingMethod};
use core::cmp::Ordering;
const WEIGHT: f64 = 0.75;
const TRIES: usize = 50;
#[derive(Default, Clone)]
pub struct SkillCurve {
all_weighted_segment_times: Vec<Vec<(f64, TimeSpan)>>,
}
impl SkillCurve {
pub fn new() -> Self {
Default::default()
}
pub fn len(&self) -> usize {
self.all_weighted_segment_times.len()
}
pub fn is_empty(&self) -> bool {
self.all_weighted_segment_times.is_empty()
}
pub fn truncate(&mut self, len: usize) {
self.all_weighted_segment_times.truncate(len);
}
pub fn for_segments(&mut self, segments: &[Segment], method: TimingMethod) {
let mut len = segments.len();
self.all_weighted_segment_times
.resize_with(len, Default::default);
for ((i, segment), weighted_segment_times) in segments
.iter()
.enumerate()
.zip(&mut self.all_weighted_segment_times)
{
weighted_segment_times.clear();
let mut current_weight = 1.0;
for &(id, time) in segment.segment_history().iter_actual_runs().rev() {
if let Some(time) = time[method] {
let skip = catch! {
segments[i.checked_sub(1)?].segment_history().get(id)?[method].is_none()
}
.unwrap_or(false);
if !skip {
weighted_segment_times.push((current_weight, time));
current_weight *= WEIGHT;
}
}
}
if weighted_segment_times.is_empty() {
len = i;
break;
}
weighted_segment_times.sort_unstable_by_key(|&(_, time)| time);
let mut sum = 0.0;
for (weight, _) in weighted_segment_times.iter_mut() {
sum += *weight;
*weight = sum;
}
let min = weighted_segment_times
.first()
.map(|&(w, _)| w)
.unwrap_or_default();
let max = weighted_segment_times
.last()
.map(|&(w, _)| w)
.unwrap_or_default();
let diff = max - min;
if diff != 0.0 {
for (weight, _) in weighted_segment_times.iter_mut() {
*weight = (*weight - min) / diff;
}
}
}
self.truncate(len);
}
pub fn iter_segment_times_at_percentile(
&self,
percentile: f64,
) -> impl Iterator<Item = TimeSpan> + '_ {
self.all_weighted_segment_times
.iter()
.map(move |weighted_segment_times| {
let found_index = weighted_segment_times.binary_search_by(|&(w, _)| {
if w < percentile {
Ordering::Less
} else if w > percentile {
Ordering::Greater
} else {
Ordering::Equal
}
});
match found_index {
Ok(index) => weighted_segment_times[index].1,
Err(right_index) => {
let left_index = right_index.saturating_sub(1);
let right_index = right_index.min(weighted_segment_times.len() - 1);
if left_index == right_index {
weighted_segment_times[left_index].1
} else {
let right = weighted_segment_times[right_index];
let left = weighted_segment_times[left_index];
interpolate(percentile, left, right)
}
}
}
})
}
pub fn iter_split_times_at_percentile(
&self,
percentile: f64,
offset: TimeSpan,
) -> impl Iterator<Item = TimeSpan> + '_ {
let mut sum = offset;
self.iter_segment_times_at_percentile(percentile)
.map(move |segment_time| {
sum += segment_time;
sum
})
}
pub fn find_percentile_for_time(&self, offset: TimeSpan, time_to_find: TimeSpan) -> f64 {
let (mut perc_min, mut perc_max) = (0.0, 1.0);
for _ in 0..TRIES {
let percentile = 0.5 * (perc_max + perc_min);
let sum = self
.iter_segment_times_at_percentile(percentile)
.fold(offset, |sum, segment_time| sum + segment_time);
match sum.cmp(&time_to_find) {
Ordering::Equal => return percentile,
Ordering::Less => perc_min = percentile,
Ordering::Greater => perc_max = percentile,
}
}
0.5 * (perc_max + perc_min)
}
}
fn interpolate(
perc: f64,
(weight_left, time_left): (f64, TimeSpan),
(weight_right, time_right): (f64, TimeSpan),
) -> TimeSpan {
let weight_diff_recip = (weight_right - weight_left).recip();
let perc_down = (weight_right - perc) * time_left.total_seconds() * weight_diff_recip;
let perc_up = (perc - weight_left) * time_right.total_seconds() * weight_diff_recip;
TimeSpan::from_seconds(perc_up + perc_down)
}