1use crate::round::div_nearest_ties_away;
14
15#[derive(Copy, Clone, Debug, Eq, PartialEq)]
50pub struct AffineTransform {
51 gain: i32,
52 offset: i32,
53 scale: i32,
54}
55
56#[derive(Copy, Clone, Debug, Eq, PartialEq)]
58pub enum AffineTransformError {
59 ZeroScale,
61 ZeroGain,
63}
64
65#[derive(Copy, Clone, Debug, Eq, PartialEq)]
70pub enum AffineOverflow {
71 Overflow,
73}
74
75impl AffineTransform {
76 pub const fn new(gain: i32, offset: i32, scale: i32) -> Result<Self, AffineTransformError> {
84 if scale == 0 {
85 return Err(AffineTransformError::ZeroScale);
86 }
87 if gain == 0 {
88 return Err(AffineTransformError::ZeroGain);
89 }
90 Ok(Self {
91 gain,
92 offset,
93 scale,
94 })
95 }
96
97 pub const fn gain(&self) -> i32 {
99 self.gain
100 }
101
102 pub const fn offset(&self) -> i32 {
104 self.offset
105 }
106
107 pub const fn scale(&self) -> i32 {
109 self.scale
110 }
111
112 pub fn apply(&self, value: i32) -> Result<i32, AffineOverflow> {
121 debug_assert!(self.scale != 0);
123
124 let product = i64::from(value)
125 .checked_mul(i64::from(self.gain))
126 .ok_or(AffineOverflow::Overflow)?;
127 let numerator = product
128 .checked_add(i64::from(self.offset))
129 .ok_or(AffineOverflow::Overflow)?;
130 let scaled = div_nearest_ties_away(numerator, i64::from(self.scale));
131 i32::try_from(scaled).map_err(|_| AffineOverflow::Overflow)
132 }
133
134 pub fn unapply(&self, value: i32) -> Result<i32, AffineOverflow> {
149 debug_assert!(self.gain != 0 && self.scale != 0);
150
151 let product = i64::from(value)
154 .checked_mul(i64::from(self.scale))
155 .ok_or(AffineOverflow::Overflow)?;
156 let numerator = product
157 .checked_sub(i64::from(self.offset))
158 .ok_or(AffineOverflow::Overflow)?;
159 let uncalibrated = div_nearest_ties_away(numerator, i64::from(self.gain));
160 i32::try_from(uncalibrated).map_err(|_| AffineOverflow::Overflow)
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 fn transform(gain: i32, offset: i32, scale: i32) -> AffineTransform {
169 AffineTransform::new(gain, offset, scale).unwrap()
170 }
171
172 fn reconstruction_bound(gain: i32, scale: i32) -> i64 {
178 let gain_abs = i64::from(gain.unsigned_abs());
179 let scale_abs = i64::from(scale.unsigned_abs());
180 (scale_abs + gain_abs - 1) / gain_abs
181 }
182
183 #[test]
184 fn constructor_rejects_zero_scale_and_zero_gain() {
185 assert_eq!(
186 AffineTransform::new(1, 0, 0),
187 Err(AffineTransformError::ZeroScale)
188 );
189 assert_eq!(
190 AffineTransform::new(0, 5, 1),
191 Err(AffineTransformError::ZeroGain)
192 );
193 assert_eq!(
194 AffineTransform::new(0, 0, 0),
195 Err(AffineTransformError::ZeroScale)
196 );
197 }
198
199 #[test]
200 fn accessors_echo_the_coefficients() {
201 let t = transform(1_005, -120, 1_000);
202 assert_eq!(t.gain(), 1_005);
203 assert_eq!(t.offset(), -120);
204 assert_eq!(t.scale(), 1_000);
205 }
206
207 #[test]
208 fn identity_is_passthrough() {
209 let t = transform(1, 0, 1);
210 assert_eq!(t.apply(0), Ok(0));
211 assert_eq!(t.apply(42), Ok(42));
212 assert_eq!(t.apply(-7), Ok(-7));
213 assert_eq!(t.apply(i32::MAX), Ok(i32::MAX));
214 assert_eq!(t.apply(i32::MIN), Ok(i32::MIN));
215 assert_eq!(t.unapply(i32::MAX), Ok(i32::MAX));
216 assert_eq!(t.unapply(i32::MIN), Ok(i32::MIN));
217 }
218
219 #[test]
220 fn positive_and_negative_gain_and_scale() {
221 assert_eq!(transform(2, 0, 1).apply(10), Ok(20));
222 assert_eq!(transform(-2, 0, 1).apply(10), Ok(-20));
223 assert_eq!(transform(2, 0, -1).apply(10), Ok(-20));
224 assert_eq!(transform(-2, 0, -1).apply(10), Ok(20));
225
226 let trim = transform(1_005, -120, 1_000);
228 assert_eq!(trim.apply(-500), Ok(-503));
230 assert_eq!(trim.apply(0), Ok(0));
232 assert_eq!(trim.apply(2_000), Ok(2_010));
234 }
235
236 #[test]
237 fn ties_round_away_from_zero_in_every_sign_combination() {
238 assert_eq!(transform(1, 0, 2).apply(1), Ok(1));
240 assert_eq!(transform(1, 0, 2).apply(-1), Ok(-1));
241 assert_eq!(transform(1, 0, -2).apply(1), Ok(-1));
242 assert_eq!(transform(1, 0, -2).apply(-1), Ok(1));
243 assert_eq!(transform(1, 0, 2).apply(3), Ok(2));
244 assert_eq!(transform(1, 0, 2).apply(-3), Ok(-2));
245
246 assert_eq!(transform(2, 0, 1).unapply(1), Ok(1));
248 assert_eq!(transform(2, 0, 1).unapply(-1), Ok(-1));
249 assert_eq!(transform(-2, 0, 1).unapply(1), Ok(-1));
250 assert_eq!(transform(-2, 0, 1).unapply(-1), Ok(1));
251 }
252
253 #[test]
254 fn i32_extremes_are_representable_when_the_result_fits() {
255 let identity = transform(1, 0, 1);
256 assert_eq!(identity.apply(i32::MAX), Ok(i32::MAX));
257 assert_eq!(identity.apply(i32::MIN), Ok(i32::MIN));
258 assert_eq!(identity.unapply(i32::MAX), Ok(i32::MAX));
259 assert_eq!(identity.unapply(i32::MIN), Ok(i32::MIN));
260
261 let shifted = transform(1, i32::MIN, 1);
263 assert_eq!(shifted.apply(0), Ok(i32::MIN));
264 assert_eq!(shifted.unapply(i32::MIN), Ok(0));
265 assert_eq!(shifted.apply(1), Ok(i32::MIN + 1));
266 assert_eq!(shifted.unapply(i32::MIN + 1), Ok(1));
267
268 let high_offset = transform(1, i32::MAX, 1);
269 assert_eq!(high_offset.apply(0), Ok(i32::MAX));
270 assert_eq!(high_offset.unapply(i32::MAX), Ok(0));
271 }
272
273 #[test]
274 fn representable_intermediates_still_overflow_i32() {
275 assert_eq!(
277 transform(2, 0, 1).apply(i32::MAX),
278 Err(AffineOverflow::Overflow)
279 );
280 assert_eq!(
281 transform(i32::MAX, 0, 1).apply(2),
282 Err(AffineOverflow::Overflow)
283 );
284 assert_eq!(
286 transform(-1, 0, 1).apply(i32::MIN),
287 Err(AffineOverflow::Overflow)
288 );
289 assert_eq!(
291 transform(1, 0, i32::MAX).unapply(i32::MAX),
292 Err(AffineOverflow::Overflow)
293 );
294 }
295
296 #[test]
297 fn identity_round_trips_are_exact() {
298 let t = transform(1, 0, 1);
299 for y in [i32::MIN, i32::MIN + 1, -1, 0, 1, i32::MAX - 1, i32::MAX] {
300 let applied = t.apply(y).unwrap();
301 assert_eq!(t.unapply(applied), Ok(y));
302 let undone = t.unapply(y).unwrap();
303 assert_eq!(t.apply(undone), Ok(y));
304 }
305 }
306
307 #[test]
308 fn compressing_round_trips_are_bounded_not_exact() {
309 let t = transform(2, 0, 3);
310 assert_eq!(t.apply(1), Ok(1));
311 assert_eq!(t.unapply(1), Ok(2));
312 assert_ne!(t.unapply(t.apply(1).unwrap()), Ok(1));
313
314 let bound = reconstruction_bound(2, 3);
315 for y in -2_000..=2_000 {
316 let applied = t.apply(y).unwrap();
317 let recovered = t.unapply(applied).unwrap();
318 let error = i64::from(recovered) - i64::from(y);
319 assert!(
320 error.abs() <= bound,
321 "y={y}: apply -> {applied} -> unapply -> {recovered} (Δ={error}, bound={bound})"
322 );
323 }
324 }
325
326 #[test]
327 fn interior_apply_results_are_unapplyable() {
328 let cases = [
329 transform(1, 0, 1),
330 transform(1_005, -120, 1_000),
331 transform(2, 0, 3),
332 transform(-2, 0, 3),
333 transform(3, 0, 2),
334 transform(1, i32::MIN, 1),
335 transform(-1, 0, 2),
336 ];
337 let samples = [-10_000, -1, 0, 1, 10_000];
338
339 for t in cases {
340 for y in samples {
341 let Ok(applied) = t.apply(y) else {
342 continue;
343 };
344 t.unapply(applied).unwrap_or_else(|_| {
345 panic!(
346 "apply({y}) -> {applied} was not unapplyable for gain={} offset={} scale={}",
347 t.gain(),
348 t.offset(),
349 t.scale()
350 )
351 });
352 }
353 }
354 }
355
356 #[test]
357 fn inverse_rounding_can_overflow_at_i32_max() {
358 let t = transform(2, 0, 3);
362 let applied_max = t.apply(i32::MAX).unwrap();
363 assert_eq!(t.unapply(applied_max), Err(AffineOverflow::Overflow));
364 let applied_min = t.apply(i32::MIN).unwrap();
365 assert_eq!(t.unapply(applied_min), Ok(i32::MIN));
366 }
367
368 #[test]
369 fn expanding_round_trips_stay_within_the_bound() {
370 let t = transform(3, 0, 2);
371 let bound = reconstruction_bound(3, 2);
372 for y in -2_000..=2_000 {
373 let Ok(applied) = t.apply(y) else {
374 continue;
375 };
376 let recovered = t.unapply(applied).unwrap();
377 let error = i64::from(recovered) - i64::from(y);
378 assert!(
379 error.abs() <= bound,
380 "y={y}: apply -> {applied} -> unapply -> {recovered} (Δ={error}, bound={bound})"
381 );
382 }
383 }
384}