use crate::domain::quantity::Quantity;
#[derive(Debug, Clone, PartialEq)]
pub struct HistoryEntry {
pub input: String,
pub value: Option<Quantity>,
pub error: Option<String>,
}
impl HistoryEntry {
pub fn evaluated(input: String, value: Quantity) -> Self {
HistoryEntry {
input,
value: Some(value),
error: None,
}
}
pub fn failed(input: String, error: String) -> Self {
HistoryEntry {
input,
value: None,
error: Some(error),
}
}
}
pub type LineResult = (Option<Quantity>, Option<String>);
#[derive(Debug, Clone, Default)]
pub struct History {
entries: Vec<HistoryEntry>,
max_len: usize,
}
impl History {
pub fn new(max_len: usize) -> Self {
History {
entries: Vec::new(),
max_len: max_len.max(1),
}
}
pub fn from_entries(entries: Vec<HistoryEntry>, max_len: usize) -> Self {
let mut history = History::new(max_len);
history.entries = entries;
history.trim_to_cap();
history
}
pub fn entries(&self) -> &[HistoryEntry] {
&self.entries
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn last_value(&self) -> Option<Quantity> {
self.entries
.iter()
.rev()
.find_map(|entry| entry.value.clone())
}
pub fn push(&mut self, entry: HistoryEntry) {
self.entries.push(entry);
self.trim_to_cap();
}
pub fn set_input(&mut self, index: usize, input: String) {
if let Some(entry) = self.entries.get_mut(index) {
entry.input = input;
}
}
pub fn remove(&mut self, index: usize) {
if index < self.entries.len() {
self.entries.remove(index);
}
}
pub fn swap(&mut self, a: usize, b: usize) {
let len = self.entries.len();
if a < len && b < len {
self.entries.swap(a, b);
}
}
pub fn insert(&mut self, index: usize, entry: HistoryEntry) {
let index = index.min(self.entries.len());
self.entries.insert(index, entry);
self.trim_to_cap();
}
pub fn clear(&mut self) {
self.entries.clear();
}
pub fn recompute_from<F>(&mut self, start: usize, mut evaluate: F)
where
F: FnMut(&str, Option<Quantity>) -> LineResult,
{
let mut running = self.entries[..start]
.iter()
.rev()
.find_map(|e| e.value.clone());
for index in start..self.entries.len() {
let input = self.entries[index].input.clone();
let (value, error) = evaluate(&input, running.clone());
if value.is_some() {
running = value.clone();
}
let entry = &mut self.entries[index];
entry.value = value;
entry.error = error;
}
}
fn trim_to_cap(&mut self) {
if self.entries.len() > self.max_len {
let excess = self.entries.len() - self.max_len;
self.entries.drain(0..excess);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn q(value: f64) -> Quantity {
Quantity::dimensionless(value)
}
fn values(history: &History) -> Vec<Option<f64>> {
history
.entries()
.iter()
.map(|e| e.value.as_ref().map(Quantity::display_value))
.collect()
}
fn increment(_input: &str, ans: Option<Quantity>) -> LineResult {
let previous = ans.map_or(0.0, |a| a.display_value());
(Some(q(previous + 1.0)), None)
}
fn history_of(inputs: &[&str]) -> History {
let entries = inputs
.iter()
.map(|input| HistoryEntry::evaluated(input.to_string(), q(0.0)))
.collect();
History::from_entries(entries, 100)
}
#[test]
fn recompute_threads_ans_through_the_chain() {
let mut history = history_of(&["a", "b", "c"]);
history.recompute_from(0, increment);
assert_eq!(values(&history), vec![Some(1.0), Some(2.0), Some(3.0)]);
assert_eq!(history.last_value().map(|a| a.display_value()), Some(3.0));
}
#[test]
fn recompute_skips_value_less_lines_when_threading_ans() {
fn maybe_increment(input: &str, ans: Option<Quantity>) -> LineResult {
if input == "note" {
(None, None)
} else {
let previous = ans.map_or(0.0, |a| a.display_value());
(Some(q(previous + 1.0)), None)
}
}
let mut history = history_of(&["a", "note", "b"]);
history.recompute_from(0, maybe_increment);
assert_eq!(values(&history), vec![Some(1.0), None, Some(2.0)]);
}
#[test]
fn recompute_from_the_middle_uses_the_prior_value() {
let mut history = history_of(&["a", "b", "c"]);
history.set_input(0, "a".to_string());
history.recompute_from(0, increment);
history.entries[0].value = Some(q(10.0));
history.recompute_from(1, increment);
assert_eq!(values(&history), vec![Some(10.0), Some(11.0), Some(12.0)]);
}
#[test]
fn push_drops_oldest_beyond_capacity() {
let mut history = History::new(2);
history.push(HistoryEntry::evaluated("1".to_string(), q(1.0)));
history.push(HistoryEntry::evaluated("2".to_string(), q(2.0)));
history.push(HistoryEntry::evaluated("3".to_string(), q(3.0)));
let inputs: Vec<&str> =
history.entries().iter().map(|e| e.input.as_str()).collect();
assert_eq!(inputs, vec!["2", "3"]);
}
#[test]
fn swap_and_insert_reorder_entries() {
let mut history = history_of(&["a", "b", "c"]);
history.swap(0, 2);
let inputs: Vec<&str> =
history.entries().iter().map(|e| e.input.as_str()).collect();
assert_eq!(inputs, vec!["c", "b", "a"]);
history.insert(1, HistoryEntry::evaluated("x".to_string(), q(0.0)));
let inputs: Vec<&str> =
history.entries().iter().map(|e| e.input.as_str()).collect();
assert_eq!(inputs, vec!["c", "x", "b", "a"]);
history.swap(0, 99);
history.insert(99, HistoryEntry::evaluated("end".to_string(), q(0.0)));
assert_eq!(history.entries().last().unwrap().input, "end");
}
#[test]
fn remove_is_bounds_checked() {
let mut history = history_of(&["a", "b"]);
history.remove(5);
assert_eq!(history.len(), 2);
history.remove(0);
assert_eq!(history.entries()[0].input, "b");
}
}