1use crate::CalculusError;
2
3#[derive(Debug, Clone, Copy, PartialEq)]
5pub struct LimitApproximator {
6 step: f64,
7 tolerance: f64,
8}
9
10impl LimitApproximator {
11 #[must_use]
14 pub const fn new(step: f64, tolerance: f64) -> Self {
15 Self { step, tolerance }
16 }
17
18 pub fn try_new(step: f64, tolerance: f64) -> Result<Self, CalculusError> {
28 CalculusError::validate_step(step)?;
29 CalculusError::validate_tolerance(tolerance)?;
30
31 Ok(Self::new(step, tolerance))
32 }
33
34 pub fn validate(self) -> Result<Self, CalculusError> {
40 Self::try_new(self.step, self.tolerance)
41 }
42
43 #[must_use]
45 pub const fn step(&self) -> f64 {
46 self.step
47 }
48
49 #[must_use]
51 pub const fn tolerance(&self) -> f64 {
52 self.tolerance
53 }
54
55 pub fn at<F>(self, function: F, at: f64) -> Result<f64, CalculusError>
63 where
64 F: FnMut(f64) -> f64,
65 {
66 symmetric_limit(function, at, self.step, self.tolerance)
67 }
68}
69
70#[must_use = "limit estimates should be used or handled"]
100pub fn symmetric_limit<F>(
101 mut function: F,
102 at: f64,
103 step: f64,
104 tolerance: f64,
105) -> Result<f64, CalculusError>
106where
107 F: FnMut(f64) -> f64,
108{
109 let at = CalculusError::validate_point("at", at)?;
110 let step = CalculusError::validate_step(step)?;
111 let tolerance = CalculusError::validate_tolerance(tolerance)?;
112 let left = evaluate(&mut function, at - step)?;
113 let right = evaluate(&mut function, at + step)?;
114
115 if (left - right).abs() > tolerance {
116 return Err(CalculusError::LimitMismatch {
117 left,
118 right,
119 tolerance,
120 });
121 }
122
123 Ok(f64::midpoint(left, right))
124}
125
126fn evaluate<F>(function: &mut F, input: f64) -> Result<f64, CalculusError>
127where
128 F: FnMut(f64) -> f64,
129{
130 let input = CalculusError::validate_point("sample", input)?;
131 let value = function(input);
132
133 CalculusError::validate_evaluation(input, value)
134}
135
136#[cfg(test)]
137mod tests {
138 use super::{CalculusError, LimitApproximator, symmetric_limit};
139
140 fn assert_close(left: f64, right: f64, tolerance: f64) {
141 assert!(
142 (left - right).abs() <= tolerance,
143 "expected {left} to be within {tolerance} of {right}"
144 );
145 }
146
147 #[test]
148 fn validates_limit_configuration() {
149 assert!(matches!(
150 LimitApproximator::try_new(0.0, 1.0e-4),
151 Err(CalculusError::NonPositiveStep(0.0))
152 ));
153 assert!(matches!(
154 LimitApproximator::try_new(1.0e-4, -1.0),
155 Err(CalculusError::NegativeTolerance(-1.0))
156 ));
157 }
158
159 #[test]
160 fn approximates_two_sided_limits() -> Result<(), CalculusError> {
161 let limit = symmetric_limit(
162 |x| {
163 if x == 0.0 { 1.0 } else { x.sin() / x }
164 },
165 0.0,
166 1.0e-6,
167 1.0e-5,
168 )?;
169
170 assert_close(limit, 1.0, 1.0e-5);
171 Ok(())
172 }
173
174 #[test]
175 fn rejects_mismatched_limits() {
176 assert!(matches!(
177 symmetric_limit(|x| if x < 0.0 { -1.0 } else { 1.0 }, 0.0, 1.0e-6, 1.0e-3,),
178 Err(CalculusError::LimitMismatch { .. })
179 ));
180 }
181}