livesplit-core 0.13.0

livesplit-core is a library that provides a lot of functionality for creating a speedrun timer.
Documentation
//! The editor module provides an editor for [`Run`] objects. The editor ensures
//! that all the different invariants of the [`Run`] objects are upheld no matter
//! what kind of operations are being applied to the [`Run`]. It provides the
//! current state of the editor as state objects that can be visualized by any
//! kind of User Interface.

use super::{ComparisonError, ComparisonResult};
use crate::{
    comparison,
    platform::prelude::*,
    settings::{CachedImageId, Image},
    timing::ParseError as ParseTimeSpanError,
    util::PopulateString,
    Run, Segment, Time, TimeSpan, TimingMethod,
};
use core::{mem::swap, num::ParseIntError};
use snafu::{OptionExt, ResultExt};

pub mod cleaning;
mod fuzzy_list;
mod segment_row;
mod state;
#[cfg(test)]
mod tests;

pub use self::{
    cleaning::SumOfBestCleaner,
    fuzzy_list::FuzzyList,
    segment_row::SegmentRow,
    state::{Buttons as ButtonsState, Segment as SegmentState, SelectionState, State},
};

/// Describes an Error that occurred while parsing a time.
#[derive(Debug, snafu::Snafu)]
#[snafu(context(suffix(false)))]
pub enum ParseError {
    /// Couldn't parse the time.
    ParseTime {
        /// The underlying error.
        source: ParseTimeSpanError,
    },
    /// Negative times are not allowed here.
    NegativeTimeNotAllowed,
    /// Empty times are not allowed here.
    EmptyTimeNotAllowed,
}

/// Describes an Error that occurred while opening the Run Editor.
#[derive(Debug, snafu::Snafu)]
#[snafu(context(suffix(false)))]
pub enum OpenError {
    /// The Run Editor couldn't be opened because an empty run with no
    /// segments was provided.
    EmptyRun,
}

/// Error type for a failed Rename.
#[derive(PartialEq, Eq, Debug, snafu::Snafu)]
#[snafu(context(suffix(false)))]
pub enum RenameError {
    /// Old Comparison was not found during rename.
    OldNameNotFound,
    /// Name was invalid.
    InvalidName {
        /// The underlying error.
        source: ComparisonError,
    },
}

/// The Run Editor allows modifying Runs while ensuring that all the different
/// invariants of the Run objects are upheld no matter what kind of operations
/// are being applied to the Run. It provides the current state of the editor as
/// state objects that can be visualized by any kind of User Interface.
pub struct Editor {
    run: Run,
    selected_method: TimingMethod,
    selected_segments: Vec<usize>,
    previous_personal_best_time: Time,
    game_icon_id: CachedImageId,
    segment_icon_ids: Vec<CachedImageId>,
    segment_times: Vec<Option<TimeSpan>>,
}

impl Editor {
    /// Creates a new Run Editor that modifies the Run provided. Creation of the
    /// Run Editor fails when a Run with no segments is provided.
    pub fn new(mut run: Run) -> Result<Self, OpenError> {
        run.fix_splits();
        let len = run.len();
        if len == 0 {
            return Err(OpenError::EmptyRun);
        }

        let personal_best_time = run.segments().last().unwrap().personal_best_split_time();

        let mut editor = Self {
            run,
            selected_method: TimingMethod::RealTime,
            selected_segments: vec![0],
            previous_personal_best_time: personal_best_time,
            game_icon_id: CachedImageId::default(),
            segment_icon_ids: Vec::with_capacity(len),
            segment_times: Vec::with_capacity(len),
        };

        editor.update_segment_list();

        Ok(editor)
    }

    /// Closes the Run Editor and gives back access to the modified Run object.
    /// In case you want to implement a Cancel Button, just drop the Run object
    /// you get here.
    #[allow(clippy::missing_const_for_fn)] // FIXME: Drop is unsupported.
    pub fn close(self) -> Run {
        self.run
    }

    /// Accesses the Run being edited by the Run Editor.
    #[inline]
    pub const fn run(&self) -> &Run {
        &self.run
    }

    /// Accesses the timing method that is currently selected for being
    /// modified.
    pub const fn selected_timing_method(&self) -> TimingMethod {
        self.selected_method
    }

    /// Selects a different timing method for being modified.
    pub fn select_timing_method(&mut self, method: TimingMethod) {
        self.selected_method = method;
        self.update_segment_list();
    }

    fn active_segment_index(&self) -> usize {
        *self.selected_segments.last().unwrap()
    }

    /// Grants mutable access to the actively selected segment row. You can
    /// select multiple segment rows at the same time, but only the one that is
    /// most recently selected is the active segment.
    pub fn active_segment(&mut self) -> SegmentRow<'_> {
        SegmentRow::new(self.active_segment_index(), self)
    }

    /// Unselects the segment with the given index. If it's not selected or the
    /// index is out of bounds, nothing happens. The segment is not unselected,
    /// when it is the only segment that is selected. If the active segment is
    /// unselected, the most recently selected segment remaining becomes the
    /// active segment.
    pub fn unselect(&mut self, index: usize) {
        self.selected_segments.retain(|&i| i != index);
        if self.selected_segments.is_empty() {
            self.selected_segments.push(index);
        }
    }

    /// In addition to the segments that are already selected, the segment with
    /// the given index is being selected. The segment chosen also becomes the
    /// active segment.
    ///
    /// # Panics
    ///
    /// This panics if the index of the segment provided is out of bounds.
    pub fn select_additionally(&mut self, index: usize) {
        if index >= self.run.len() {
            panic!("Index out of bounds for segment selection.");
        }
        self.selected_segments.retain(|&i| i != index);
        self.selected_segments.push(index);
    }

    /// Select all segments from the currently active segment to the segment at
    /// the index provided. The segment at the index provided becomes the new
    /// active segment.
    ///
    /// # Panics
    ///
    /// This panics if the index of the segment provided is out of bounds.
    pub fn select_range(&mut self, index: usize) {
        let active = self.active_segment_index();
        let range = if index < active {
            index + 1..active
        } else {
            active + 1..index
        };
        for i in range {
            if !self.selected_segments.contains(&i) {
                self.selected_segments.push(i);
            }
        }
        self.select_additionally(index);
    }

    /// Selects the segment with the given index. All other segments are
    /// unselected. The segment chosen also becomes the active segment.
    ///
    /// # Panics
    ///
    /// This panics if the index of the segment provided is out of bounds.
    pub fn select_only(&mut self, index: usize) {
        if index >= self.run.len() {
            panic!("Index out of bounds for segment selection.");
        }
        self.selected_segments.clear();
        self.selected_segments.push(index);
    }

    fn raise_run_edited(&mut self) {
        self.run.mark_as_modified();
    }

    /// Accesses the name of the game.
    pub fn game_name(&self) -> &str {
        self.run.game_name()
    }

    /// Sets the name of the game.
    pub fn set_game_name<S>(&mut self, name: S)
    where
        S: PopulateString,
    {
        self.run.set_game_name(name);
        self.raise_run_edited();
        self.run.clear_run_id();
    }

    /// Accesses the name of the category.
    pub fn category_name(&self) -> &str {
        self.run.category_name()
    }

    /// Sets the name of the category.
    pub fn set_category_name<S>(&mut self, name: S)
    where
        S: PopulateString,
    {
        self.run.set_category_name(name);
        self.raise_run_edited();
        self.run.clear_run_id();
    }

    /// Accesses the timer offset. The timer offset specifies the time, the
    /// timer starts at when starting a new attempt.
    pub const fn offset(&self) -> TimeSpan {
        self.run.offset()
    }

    /// Sets the timer offset. The timer offset specifies the time, the timer
    /// starts at when starting a new attempt.
    pub fn set_offset(&mut self, offset: TimeSpan) {
        self.run.set_offset(offset);
        self.raise_run_edited();
    }

    /// Parses and sets the timer offset from the string provided. The timer
    /// offset specifies the time, the timer starts at when starting a new
    /// attempt.
    pub fn parse_and_set_offset(&mut self, offset: &str) -> Result<(), ParseError> {
        self.set_offset(offset.parse().context(ParseTime)?);
        Ok(())
    }

    /// Accesses the attempt count.
    pub const fn attempt_count(&self) -> u32 {
        self.run.attempt_count()
    }

    /// Sets the attempt count. Changing this has no affect on the attempt
    /// history or the segment history. This number is mostly just a visual
    /// number for the runner.
    pub fn set_attempt_count(&mut self, attempts: u32) {
        self.run.set_attempt_count(attempts);
        self.raise_run_edited();
    }

    /// Parses and sets the attempt count from the string provided. Changing
    /// this has no affect on the attempt history or the segment history. This
    /// number is mostly just a visual number for the runner.
    pub fn parse_and_set_attempt_count(&mut self, attempts: &str) -> Result<(), ParseIntError> {
        self.set_attempt_count(attempts.parse()?);
        Ok(())
    }

    /// Accesses the game's icon.
    pub const fn game_icon(&self) -> &Image {
        self.run.game_icon()
    }

    /// Sets the game's icon.
    pub fn set_game_icon<D: Into<Image>>(&mut self, image: D) {
        self.run.set_game_icon(image);
        self.raise_run_edited();
    }

    /// Removes the game's icon.
    pub fn remove_game_icon(&mut self) {
        self.run.set_game_icon([]);
        self.raise_run_edited();
    }

    /// Accesses all the custom comparisons that exist on the Run.
    pub fn custom_comparisons(&self) -> &[String] {
        self.run.custom_comparisons()
    }

    /// Sets the speedrun.com Run ID of the run. You need to ensure that the
    /// record on speedrun.com matches up with the Personal Best of this run.
    /// This may be empty if there's no association.
    pub fn set_run_id<S>(&mut self, id: S)
    where
        S: PopulateString,
    {
        self.run.metadata_mut().set_run_id(id);
        self.raise_run_edited();
    }

    /// Sets the name of the region this game is from. This may be empty if it's
    /// not specified.
    pub fn set_region_name<S>(&mut self, name: S)
    where
        S: PopulateString,
    {
        self.run.metadata_mut().set_region_name(name);
        self.metadata_modified();
    }

    /// Sets the name of the platform this game is run on. This may be empty if
    /// it's not specified.
    pub fn set_platform_name<S>(&mut self, name: S)
    where
        S: PopulateString,
    {
        self.run.metadata_mut().set_platform_name(name);
        self.metadata_modified();
    }

    /// Specifies whether this speedrun is done on an emulator. Keep in mind
    /// that `false` may also mean that this information is simply not known.
    pub fn set_emulator_usage(&mut self, uses_emulator: bool) {
        self.run.metadata_mut().set_emulator_usage(uses_emulator);
        self.metadata_modified();
    }

    /// Sets the speedrun.com variable with the name specified to the value
    /// specified. A variable is an arbitrary key value pair storing additional
    /// information about the category. An example of this may be whether
    /// Amiibos are used in this category. If the variable doesn't exist yet, it
    /// is being inserted.
    pub fn set_speedrun_com_variable<N, V>(&mut self, name: N, value: V)
    where
        N: PopulateString,
        V: PopulateString,
    {
        self.run
            .metadata_mut()
            .set_speedrun_com_variable(name, value);
        self.metadata_modified();
    }

    /// Removes the speedrun.com variable with the name specified.
    pub fn remove_speedrun_com_variable(&mut self, name: &str) {
        self.run.metadata_mut().remove_speedrun_com_variable(name);
        self.metadata_modified();
    }

    /// Adds a new permanent custom variable. If there's a temporary variable
    /// with the same name, it gets turned into a permanent variable and its
    /// value stays. If a permanent variable with the name already exists,
    /// nothing happens.
    pub fn add_custom_variable<N>(&mut self, name: N)
    where
        N: PopulateString,
    {
        self.run
            .metadata_mut()
            .custom_variable_mut(name)
            .permanent();
        self.raise_run_edited();
    }

    /// Sets the value of a custom variable with the name specified. If the
    /// custom variable does not exist, or is not a permanent variable, nothing
    /// happens.
    pub fn set_custom_variable<N, V>(&mut self, name: N, value: V)
    where
        N: PopulateString,
        V: PopulateString,
    {
        if self.run.metadata().custom_variable(name.as_str()).is_none() {
            return;
        }
        let variable = self.run.metadata_mut().custom_variable_mut(name);
        if variable.is_permanent {
            value.populate(&mut variable.value);
            self.raise_run_edited();
        }
    }

    /// Removes the custom variable with the name specified. If the custom
    /// variable does not exist, or is not a permanent variable, nothing
    /// happens.
    pub fn remove_custom_variable(&mut self, name: &str) {
        if let Some(variable) = self.run.metadata().custom_variable(name) {
            if variable.is_permanent {
                self.run.metadata_mut().remove_custom_variable(name);
                self.raise_run_edited();
            }
        }
    }

    /// Resets all the Metadata Information.
    pub fn clear_metadata(&mut self) {
        self.run.metadata_mut().clear();
        self.raise_run_edited();
    }

    fn metadata_modified(&mut self) {
        self.run.clear_run_id();
        self.raise_run_edited();
    }

    fn times_modified(&mut self) {
        let pb_split_time = self
            .run
            .segments()
            .last()
            .unwrap()
            .personal_best_split_time();

        if pb_split_time != self.previous_personal_best_time {
            self.run.clear_run_id();
            self.previous_personal_best_time = pb_split_time;
        }

        self.raise_run_edited();
    }

    fn fix(&mut self) {
        self.run.fix_splits();
        self.update_segment_list();
        self.raise_run_edited();
    }

    fn update_segment_list(&mut self) {
        let method = self.selected_method;
        let mut previous_time = Some(TimeSpan::zero());
        self.segment_times.clear();
        for segment in self.run.segments() {
            let split_time = segment.personal_best_split_time()[method];
            self.segment_times
                .push(catch! { split_time? - previous_time? });
            if split_time.is_some() {
                previous_time = split_time;
            }
        }
    }

    fn fix_splits_from_segments(&mut self) {
        let method = self.selected_method;
        let mut previous_time = Some(TimeSpan::zero());
        for (segment_time, segment) in self
            .segment_times
            .iter_mut()
            .zip(self.run.segments_mut().iter_mut())
        {
            {
                let time = segment.personal_best_split_time_mut();
                time[method] = catch! { previous_time? + (*segment_time)? };
            }
            if segment_time.is_some() {
                previous_time = segment.personal_best_split_time()[method];
            }
        }
    }

    /// Inserts a new empty segment above the active segment and adjusts the
    /// Run's history information accordingly. The newly created segment is then
    /// the only selected segment and also the active segment.
    pub fn insert_segment_above(&mut self) {
        let active_segment = self.active_segment_index();

        let mut segment = Segment::new("");
        self.run.import_best_segment(active_segment);

        let max_index = self.run.max_attempt_history_index().unwrap_or(0);
        let min_index = self.run.min_segment_history_index().unwrap();
        for x in min_index..=max_index {
            segment.segment_history_mut().insert(x, Default::default());
        }
        self.run.segments_mut().insert(active_segment, segment);

        self.select_only(active_segment);

        self.times_modified();
        self.fix();
    }

    /// Inserts a new empty segment below the active segment and adjusts the
    /// Run's history information accordingly. The newly created segment is then
    /// the only selected segment and also the active segment.
    pub fn insert_segment_below(&mut self) {
        let active_segment = self.active_segment_index();
        let next_segment = active_segment + 1;

        let mut segment = Segment::new("");
        if next_segment < self.run.len() {
            self.run.import_best_segment(next_segment);
        }

        let max_index = self.run.max_attempt_history_index().unwrap_or(0);
        let min_index = self.run.min_segment_history_index().unwrap();
        for x in min_index..=max_index {
            segment.segment_history_mut().insert(x, Default::default());
        }
        self.run.segments_mut().insert(next_segment, segment);

        self.select_only(next_segment);

        self.times_modified();
        self.fix();
    }

    fn fix_after_deletion(&mut self, index: usize) {
        self.fix_with_timing_method(index, TimingMethod::RealTime);
        self.fix_with_timing_method(index, TimingMethod::GameTime);
    }

    fn fix_with_timing_method(&mut self, index: usize, method: TimingMethod) {
        let current_index = index + 1;

        if current_index >= self.run.len() {
            return;
        }

        let max_index = self.run.max_attempt_history_index().unwrap_or(0);
        let min_index = self.run.min_segment_history_index().unwrap();
        for run_index in min_index..=max_index {
            // If a history element isn't there in the segment that's deleted
            // remove it from the next segment's history as well
            if let Some(segment_history_element) =
                self.run.segment(index).segment_history().get(run_index)
            {
                let current_segment = segment_history_element[method];
                if let Some(current_segment) = current_segment {
                    for current_index in current_index..self.run.len() {
                        // Add the removed segment's history times to the next
                        // non None times
                        if let Some(Some(segment)) = self
                            .run
                            .segment_mut(current_index)
                            .segment_history_mut()
                            .get_mut(run_index)
                            .map(|t| &mut t[method])
                        {
                            *segment += current_segment;
                            break;
                        }
                    }
                }
            } else {
                self.run
                    .segment_mut(current_index)
                    .segment_history_mut()
                    .remove(run_index);
            }
        }

        // Set the new Best Segment time to be the sum of the two Best Segments
        let min_best_segment = catch! {
            self.run.segment(index).best_segment_time()[method]?
                + self.run.segment(current_index).best_segment_time()[method]?
        };

        if let Some(mut min_best_segment) = min_best_segment {
            // Use any element in the history that has a lower time than this
            // sum
            for time in self
                .run
                .segment(current_index)
                .segment_history()
                .iter()
                .filter_map(|&(_, t)| t[method])
            {
                if time < min_best_segment {
                    min_best_segment = time;
                }
            }
            self.run.segment_mut(current_index).best_segment_time_mut()[method] =
                Some(min_best_segment);
        }
    }

    /// Checks if the currently selected segments can be removed. If all
    /// segments are selected, they can't be removed.
    pub fn can_remove_segments(&self) -> bool {
        self.run.len() > self.selected_segments.len()
    }

    /// Removes all the selected segments, unless all of them are selected. The
    /// run's information is automatically adjusted properly. The next
    /// not-to-be-removed segment after the active segment becomes the new
    /// active segment. If there's none, then the next not-to-be-removed segment
    /// before the active segment, becomes the new active segment.
    pub fn remove_segments(&mut self) {
        if !self.can_remove_segments() {
            return;
        }

        let mut removed = 0;
        for i in 0..self.run.len() {
            if self.selected_segments.contains(&i) {
                let segment_index = i - removed;
                self.fix_after_deletion(segment_index);
                self.run.segments_mut().remove(segment_index);
                removed += 1;
            }
        }

        let selected_segment = self.active_segment_index();
        let above_count = self
            .selected_segments
            .iter()
            .filter(|&&i| i < selected_segment)
            .count();
        let mut new_index = selected_segment - above_count;
        if new_index >= self.run.len() {
            new_index = self.run.len() - 1;
        }
        self.selected_segments.clear();
        self.selected_segments.push(new_index);

        self.times_modified();
        self.fix();
    }

    fn switch_segments(&mut self, index: usize) {
        let max_index = self.run.max_attempt_history_index().unwrap_or(0);
        let min_index = self.run.min_segment_history_index().unwrap();

        // Use split_at to prove that the 3 segments are distinct
        let (a, b) = self.run.segments_mut().split_at_mut(index);
        let previous = a.last();
        let (a, b) = b.split_at_mut(1);
        let first = &mut a[0];
        let second = &mut b[0];

        for run_index in min_index..=max_index {
            // Remove both segment history elements if one of them has a None
            // time and the other has has a non None time
            let first_history = first.segment_history().get(run_index);
            let second_history = second.segment_history().get(run_index);
            if let (Some(first_history), Some(second_history)) = (first_history, second_history) {
                if first_history.real_time.is_some() != second_history.real_time.is_some()
                    || first_history.game_time.is_some() != second_history.game_time.is_some()
                {
                    first.segment_history_mut().remove(run_index);
                    second.segment_history_mut().remove(run_index);
                }
            }
        }

        for (comparison, first_time) in first.comparisons_mut().iter_mut() {
            // Fix the comparison times based on the new positions of the two
            // segments
            let previous_time = previous
                .map(|p| p.comparison(comparison))
                .unwrap_or_else(Time::zero);

            let second_time = second.comparison_mut(comparison);
            let first_segment_time = *first_time - previous_time;
            let second_segment_time = *second_time - *first_time;
            *second_time = previous_time + second_segment_time;
            *first_time = *second_time + first_segment_time;
        }

        swap(first, second);
    }

    /// Checks if the currently selected segments can be moved up. If any one of
    /// the selected segments is the first segment, then they can't be moved.
    pub fn can_move_segments_up(&self) -> bool {
        !self.selected_segments.iter().any(|&s| s == 0)
    }

    /// Moves all the selected segments up, unless the first segment is
    /// selected. The run's information is automatically adjusted properly. The
    /// active segment stays the active segment.
    pub fn move_segments_up(&mut self) {
        if !self.can_move_segments_up() {
            return;
        }

        for i in 0..self.run.len() - 1 {
            if self.selected_segments.contains(&(i + 1)) {
                self.switch_segments(i);
            }
        }

        for segment in &mut self.selected_segments {
            *segment = segment.saturating_sub(1);
        }

        self.times_modified();
        self.fix();
    }

    /// Checks if the currently selected segments can be moved down. If any one
    /// of the selected segments is the last segment, then they can't be moved.
    pub fn can_move_segments_down(&self) -> bool {
        let last_index = self.run.len() - 1;
        !self.selected_segments.iter().any(|&s| s == last_index)
    }

    /// Moves all the selected segments down, unless the last segment is
    /// selected. The run's information is automatically adjusted properly. The
    /// active segment stays the active segment.
    pub fn move_segments_down(&mut self) {
        if !self.can_move_segments_down() {
            return;
        }

        for i in (0..self.run.len() - 1).rev() {
            if self.selected_segments.contains(&i) {
                self.switch_segments(i);
            }
        }

        for segment in &mut self.selected_segments {
            if *segment < self.run.len() - 1 {
                *segment += 1;
            }
        }

        self.times_modified();
        self.fix();
    }

    /// Adds a new custom comparison. It can't be added if it starts with
    /// `[Race]` or already exists.
    pub fn add_comparison<S: PopulateString>(&mut self, comparison: S) -> ComparisonResult<()> {
        self.run.add_custom_comparison(comparison)?;
        self.fix();
        Ok(())
    }

    /// Imports the Personal Best from the provided run as a comparison. The
    /// comparison can't be added if its name starts with `[Race]` or it already
    /// exists.
    pub fn import_comparison(&mut self, run: &Run, comparison: &str) -> ComparisonResult<()> {
        self.run.add_custom_comparison(comparison)?;

        let mut remaining_segments = self.run.segments_mut().as_mut_slice();

        for segment in run.segments().iter().take(run.len().saturating_sub(1)) {
            if let Some((segment_index, my_segment)) = remaining_segments
                .iter_mut()
                .enumerate()
                .find(|(_, s)| unicase::eq(segment.name(), s.name()))
            {
                *my_segment.comparison_mut(comparison) = segment.personal_best_split_time();
                remaining_segments = &mut remaining_segments[segment_index + 1..];
            }
        }

        if let (Some(my_segment), Some(segment)) =
            (self.run.segments_mut().last_mut(), run.segments().last())
        {
            *my_segment.comparison_mut(comparison) = segment.personal_best_split_time();
        }

        self.fix();
        Ok(())
    }

    /// Removes the chosen custom comparison. You can't remove a Comparison
    /// Generator's Comparison or the Personal Best.
    pub fn remove_comparison(&mut self, comparison: &str) {
        if comparison == comparison::personal_best::NAME {
            return;
        }

        self.run
            .custom_comparisons_mut()
            .retain(|c| c != comparison);

        if self.run.comparisons().any(|c| c == comparison) {
            return;
        }

        for segment in self.run.segments_mut() {
            segment.comparisons_mut().remove(comparison);
        }

        self.fix();
    }

    /// Renames a comparison. The comparison can't be renamed if the new name of
    /// the comparison starts with `[Race]` or it already exists.
    pub fn rename_comparison(&mut self, old: &str, new: &str) -> Result<(), RenameError> {
        if old == new {
            return Ok(());
        }

        self.run
            .validate_comparison_name(new)
            .context(InvalidName)?;

        {
            let comparison_name = self
                .run
                .custom_comparisons_mut()
                .iter_mut()
                .find(|c| *c == old)
                .context(OldNameNotFound)?;

            comparison_name.clear();
            comparison_name.push_str(new);
        }

        for segment in self.run.segments_mut() {
            if let Some(time) = segment.comparisons_mut().remove(old) {
                *segment.comparison_mut(new) = time;
            }
        }

        self.fix();

        Ok(())
    }

    /// Reorders the custom comparisons by moving the comparison with the
    /// `src_index` specified to the `dst_index` specified. Returns `Err(())` if
    /// one of the indices is invalid. The indices are based on the
    /// `comparison_names` field of the Run Editor's `State`.
    pub fn move_comparison(&mut self, src_index: usize, dst_index: usize) -> Result<(), ()> {
        let comparisons = self.run.custom_comparisons_mut();
        let (src_index, dst_index) = (src_index + 1, dst_index + 1);
        if src_index >= comparisons.len() || dst_index >= comparisons.len() {
            return Err(());
        }
        if src_index == dst_index {
            return Ok(());
        }

        if src_index > dst_index {
            comparisons[dst_index..=src_index].rotate_left(src_index - dst_index);
        } else {
            comparisons[src_index..=dst_index].rotate_left(1);
        }

        self.raise_run_edited();
        Ok(())
    }

    /// Generates a custom goal comparison based on the goal time provided. The
    /// comparison's times are automatically balanced based on the runner's
    /// history such that it roughly represents what split times for the goal
    /// time would roughly look like. Since it is populated by the runner's
    /// history, only goal times within the sum of the best segments and the sum
    /// of the worst segments are supported. Everything else is automatically
    /// capped by that range. The comparison is only populated for the selected
    /// timing method. The other timing method's comparison times are not
    /// modified by this, so you can call this again with the other timing
    /// method to generate the comparison times for both timing methods.
    pub fn generate_goal_comparison(&mut self, time: TimeSpan) {
        if !self
            .run
            .custom_comparisons()
            .iter()
            .any(|c| c == comparison::goal::NAME)
        {
            self.run
                .custom_comparisons_mut()
                .push(String::from(comparison::goal::NAME));
        }

        comparison::goal::generate_for_timing_method(
            self.run.segments_mut(),
            self.selected_method,
            time,
            comparison::goal::NAME,
        );

        self.raise_run_edited();
    }

    /// Parses a goal time and generates a custom goal comparison based on the
    /// parsed value. The comparison's times are automatically balanced based on
    /// the runner's history such that it roughly represents what split times
    /// for the goal time would roughly look like. Since it is populated by the
    /// runner's history, only goal times within the sum of the best segments
    /// and the sum of the worst segments are supported. Everything else is
    /// automatically capped by that range. The comparison is only populated for
    /// the selected timing method. The other timing method's comparison times
    /// are not modified by this, so you can call this again with the other
    /// timing method to generate the comparison times for both timing methods.
    pub fn parse_and_generate_goal_comparison(&mut self, time: &str) -> Result<(), ParseError> {
        self.generate_goal_comparison(
            parse_positive(time)?.ok_or(ParseError::EmptyTimeNotAllowed)?,
        );
        Ok(())
    }

    /// Clears out the Attempt History and the Segment Histories of all the
    /// segments.
    pub fn clear_history(&mut self) {
        self.run.clear_history();
        self.fix();
    }

    /// Clears out the Attempt History, the Segment Histories, all the times,
    /// sets the Attempt Count to 0 and clears the speedrun.com run id
    /// association. All Custom Comparisons other than `Personal Best` are
    /// deleted as well.
    pub fn clear_times(&mut self) {
        self.run.clear_times();
        self.fix();
    }

    /// Creates a Sum of Best Cleaner which allows you to interactively remove
    /// potential issues in the segment history that lead to an inaccurate Sum
    /// of Best. If you skip a split, whenever you will do the next split, the
    /// combined segment time might be faster than the sum of the individual
    /// best segments. The Sum of Best Cleaner will point out all of these and
    /// allows you to delete them individually if any of them seem wrong.
    pub fn clean_sum_of_best(&mut self) -> SumOfBestCleaner<'_> {
        SumOfBestCleaner::new(&mut self.run)
    }
}

fn parse_positive(time: &str) -> Result<Option<TimeSpan>, ParseError> {
    let time = TimeSpan::parse_opt(time).context(ParseTime)?;
    if time.map_or(false, |t| t < TimeSpan::zero()) {
        Err(ParseError::NegativeTimeNotAllowed)
    } else {
        Ok(time)
    }
}