#[cfg(feature = "animation")]
use crate::animation::Spring;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PlaceValue {
Dot,
Power(f64),
}
impl PlaceValue {
#[inline]
pub fn is_dot(&self) -> bool {
matches!(self, PlaceValue::Dot)
}
#[inline]
pub fn power(&self) -> Option<f64> {
match self {
PlaceValue::Power(p) => Some(*p),
PlaceValue::Dot => None,
}
}
}
#[derive(Debug, Clone)]
pub struct DigitState {
pub digit_offsets: [f32; 10],
pub spring_value: f64,
}
#[derive(Debug, Clone)]
pub struct CounterState {
pub digits: Vec<(PlaceValue, Option<DigitState>)>,
}
impl CounterState {
#[inline]
pub fn len(&self) -> usize {
self.digits.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.digits.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct Counter {
pub value: f64,
pub places: Option<Vec<PlaceValue>>,
#[cfg(feature = "animation")]
pub spring: Spring,
pub time_offset: f64,
}
impl Counter {
pub fn new(value: f64) -> Self {
Self {
value,
places: None,
#[cfg(feature = "animation")]
spring: Spring::new().stiffness(100.0).damping(10.0),
time_offset: 0.0,
}
}
pub fn with_places(mut self, places: Vec<PlaceValue>) -> Self {
self.places = Some(places);
self
}
#[cfg(feature = "animation")]
pub fn with_spring(mut self, spring: Spring) -> Self {
self.spring = spring;
self
}
pub fn with_time_offset(mut self, offset: f64) -> Self {
self.time_offset = offset;
self
}
fn auto_detect_places(&self) -> Vec<PlaceValue> {
let value_str = self.value.to_string();
let chars: Vec<char> = value_str.chars().collect();
chars
.iter()
.enumerate()
.map(|(i, &ch)| {
if ch == '.' {
return PlaceValue::Dot;
}
let dot_index = chars.iter().position(|&c| c == '.');
let is_integer = dot_index.is_none();
let exponent = if is_integer {
(chars.len() - i - 1) as i32
} else {
let dot_idx = dot_index.unwrap();
if i < dot_idx {
(dot_idx - i - 1) as i32
} else {
-((i - dot_idx) as i32)
}
};
PlaceValue::Power(10_f64.powi(exponent))
})
.collect()
}
fn get_places(&self) -> Vec<PlaceValue> {
self.places.clone().unwrap_or_else(|| self.auto_detect_places())
}
#[cfg(feature = "animation")]
pub fn evaluate(&self, t: f64) -> CounterState {
let adjusted_t = t - self.time_offset;
let places = self.get_places();
let digits = places
.into_iter()
.map(|place| {
if place.is_dot() {
(place, None)
} else {
let power = place.power().unwrap();
let digit_state = self.evaluate_digit(power, adjusted_t);
(place, Some(digit_state))
}
})
.collect();
CounterState { digits }
}
#[cfg(not(feature = "animation"))]
pub fn evaluate(&self, _t: f64) -> CounterState {
let places = self.get_places();
let digits = places
.into_iter()
.map(|place| {
if place.is_dot() {
(place, None)
} else {
let power = place.power().unwrap();
let digit_state = self.evaluate_digit_static(power);
(place, Some(digit_state))
}
})
.collect();
CounterState { digits }
}
#[cfg(feature = "animation")]
fn evaluate_digit(&self, place: f64, t: f64) -> DigitState {
let value_rounded = (self.value / place).floor();
let (displacement, _velocity) = self.spring.evaluate(t);
let spring_value = value_rounded * (1.0 - displacement);
let place_value = spring_value % 10.0;
let mut digit_offsets = [0.0f32; 10];
for (number, slot) in digit_offsets.iter_mut().enumerate() {
let offset = (10.0 + number as f64 - place_value) % 10.0;
let mut memo = offset;
if offset > 5.0 {
memo -= 10.0;
}
*slot = memo as f32;
}
DigitState {
digit_offsets,
spring_value,
}
}
#[cfg(not(feature = "animation"))]
fn evaluate_digit_static(&self, place: f64) -> DigitState {
let value_rounded = (self.value / place).floor();
let place_value = value_rounded % 10.0;
let mut digit_offsets = [0.0f32; 10];
for number in 0..10 {
let offset = (10.0 + number as f64 - place_value) % 10.0;
let mut memo = offset;
if offset > 5.0 {
memo -= 10.0;
}
digit_offsets[number] = memo as f32;
}
DigitState {
digit_offsets,
spring_value: value_rounded,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_place_value() {
let dot = PlaceValue::Dot;
assert!(dot.is_dot());
assert_eq!(dot.power(), None);
let hundreds = PlaceValue::Power(100.0);
assert!(!hundreds.is_dot());
assert_eq!(hundreds.power(), Some(100.0));
}
#[test]
fn test_auto_detect_places_integer() {
let counter = Counter::new(1234.0);
let places = counter.auto_detect_places();
assert_eq!(places.len(), 4);
assert_eq!(places[0], PlaceValue::Power(1000.0));
assert_eq!(places[1], PlaceValue::Power(100.0));
assert_eq!(places[2], PlaceValue::Power(10.0));
assert_eq!(places[3], PlaceValue::Power(1.0));
}
#[test]
fn test_auto_detect_places_decimal() {
let counter = Counter::new(12.34);
let places = counter.auto_detect_places();
assert_eq!(places.len(), 5);
assert_eq!(places[0], PlaceValue::Power(10.0));
assert_eq!(places[1], PlaceValue::Power(1.0));
assert_eq!(places[2], PlaceValue::Dot);
assert_eq!(places[3], PlaceValue::Power(0.1));
assert_eq!(places[4], PlaceValue::Power(0.01));
}
#[test]
fn test_evaluate_at_start() {
let counter = Counter::new(123.0);
let state = counter.evaluate(0.0);
assert_eq!(state.len(), 3);
assert!(!state.is_empty());
}
#[test]
#[cfg(feature = "animation")]
fn test_digit_offsets() {
let counter = Counter::new(5.0);
let state = counter.evaluate(0.0);
assert_eq!(state.digits.len(), 1);
let (place, digit_state) = &state.digits[0];
assert_eq!(place, &PlaceValue::Power(1.0));
let ds = digit_state.as_ref().unwrap();
assert_eq!(ds.digit_offsets.len(), 10);
}
#[test]
fn test_custom_places() {
let counter = Counter::new(1234.0).with_places(vec![
PlaceValue::Power(100.0),
PlaceValue::Power(10.0),
]);
let state = counter.evaluate(0.0);
assert_eq!(state.len(), 2);
}
#[test]
fn test_decimal_point_in_places() {
let counter = Counter::new(12.3);
let state = counter.evaluate(0.0);
assert_eq!(state.len(), 4);
let (place, digit_state) = &state.digits[2];
assert_eq!(place, &PlaceValue::Dot);
assert!(digit_state.is_none());
}
#[test]
#[cfg(feature = "animation")]
fn test_spring_progression() {
let counter = Counter::new(5.0);
let state_start = counter.evaluate(0.0);
let state_mid = counter.evaluate(0.3);
let state_end = counter.evaluate(0.8);
let ds_start = state_start.digits[0].1.as_ref().unwrap();
let ds_mid = state_mid.digits[0].1.as_ref().unwrap();
let ds_end = state_end.digits[0].1.as_ref().unwrap();
assert!(ds_mid.spring_value > ds_start.spring_value);
assert!((ds_end.spring_value - 5.0).abs() < (ds_start.spring_value - 5.0).abs());
}
#[test]
fn test_time_offset() {
let counter = Counter::new(5.0).with_time_offset(1.0);
let state = counter.evaluate(1.0);
let ds = state.digits[0].1.as_ref().unwrap();
assert!(ds.spring_value.abs() < 1.0);
}
}