Skip to main content

wellformed_validate/
batch.rs

1//! Batch validation with Structure of Arrays (SoA) layout.
2//!
3//! This module provides high-throughput batch validation by:
4//! - Using cache-friendly SoA memory layout
5//! - Processing multiple fields with SIMD
6//! - Minimizing allocations through buffer reuse
7//!
8//! ## Usage
9//!
10//! ```rust
11//! use wellformed_validate::batch::{FormBatch, ValidationConfig};
12//!
13//! let mut batch = FormBatch::with_capacity(1000);
14//!
15//! // Add forms to the batch
16//! batch.push_tins("123-45-6789", "987-65-4321");
17//! batch.push_income(10000); // $100.00 in cents
18//! batch.push_tax_year(2024);
19//!
20//! // Validate all at once
21//! let results = batch.validate();
22//! assert!(results.all_valid());
23//! ```
24
25use crate::error::BatchResult;
26use crate::tin::{
27    validate_atin_digits, validate_ein_digits, validate_itin_digits, validate_ssn_digits,
28};
29
30/// Configuration for batch validation.
31#[derive(Debug, Clone)]
32pub struct ValidationConfig {
33    /// Allow formatted TINs (with dashes)
34    pub allow_formatted_tins: bool,
35    /// Require both payer and recipient TINs
36    pub require_both_tins: bool,
37    /// Validate amounts are non-negative
38    pub validate_amounts: bool,
39    /// Maximum batch size before automatic flush
40    pub max_batch_size: usize,
41}
42
43impl Default for ValidationConfig {
44    fn default() -> Self {
45        Self {
46            allow_formatted_tins: true,
47            require_both_tins: true,
48            validate_amounts: true,
49            max_batch_size: 10_000,
50        }
51    }
52}
53
54/// Pre-allocated buffer for TIN digits.
55/// Each TIN is stored as 9 bytes (digits 0-9).
56#[derive(Clone)]
57pub struct TinBuffer {
58    /// Packed TIN digits (9 bytes per TIN)
59    data: Vec<u8>,
60    /// Valid flags (1 bit per TIN, packed into u64)
61    valid: Vec<u64>,
62    /// Number of TINs stored
63    count: usize,
64}
65
66impl TinBuffer {
67    /// Create a new buffer with given capacity.
68    pub fn with_capacity(capacity: usize) -> Self {
69        Self {
70            data: Vec::with_capacity(capacity * 9),
71            valid: Vec::with_capacity(capacity.div_ceil(64)),
72            count: 0,
73        }
74    }
75
76    /// Clear the buffer for reuse.
77    pub fn clear(&mut self) {
78        self.data.clear();
79        self.valid.clear();
80        self.count = 0;
81    }
82
83    /// Push a TIN string to the buffer.
84    pub fn push(&mut self, tin: &str) -> bool {
85        let mut digits = [0u8; 9];
86        let mut digit_count = 0;
87
88        for b in tin.bytes() {
89            let d = b.wrapping_sub(b'0');
90            if d <= 9 {
91                if digit_count >= 9 {
92                    // Too many digits - mark invalid and store zeros
93                    self.data.extend_from_slice(&[0; 9]);
94                    self.set_valid(self.count, false);
95                    self.count += 1;
96                    return false;
97                }
98                digits[digit_count] = d;
99                digit_count += 1;
100            } else if b != b'-' && b != b' ' {
101                // Invalid character
102                self.data.extend_from_slice(&[0; 9]);
103                self.set_valid(self.count, false);
104                self.count += 1;
105                return false;
106            }
107        }
108
109        if digit_count == 9 {
110            self.data.extend_from_slice(&digits);
111            self.set_valid(self.count, true);
112            self.count += 1;
113            true
114        } else {
115            self.data.extend_from_slice(&[0; 9]);
116            self.set_valid(self.count, false);
117            self.count += 1;
118            false
119        }
120    }
121
122    /// Set validity flag for an index.
123    fn set_valid(&mut self, index: usize, valid: bool) {
124        let word = index / 64;
125        let bit = index % 64;
126
127        while self.valid.len() <= word {
128            self.valid.push(0);
129        }
130
131        if valid {
132            self.valid[word] |= 1u64 << bit;
133        } else {
134            self.valid[word] &= !(1u64 << bit);
135        }
136    }
137
138    /// Check if a TIN at index has valid format.
139    #[inline]
140    pub fn is_format_valid(&self, index: usize) -> bool {
141        let word = index / 64;
142        let bit = index % 64;
143        word < self.valid.len() && (self.valid[word] >> bit) & 1 == 1
144    }
145
146    /// Get the digits for a TIN at index.
147    #[inline]
148    pub fn get_digits(&self, index: usize) -> Option<&[u8; 9]> {
149        if index < self.count {
150            let start = index * 9;
151            Some(self.data[start..start + 9].try_into().unwrap())
152        } else {
153            None
154        }
155    }
156
157    /// Get the number of TINs in the buffer.
158    #[inline]
159    pub fn len(&self) -> usize {
160        self.count
161    }
162
163    /// Check if the buffer is empty.
164    #[inline]
165    pub fn is_empty(&self) -> bool {
166        self.count == 0
167    }
168}
169
170/// Amount buffer for monetary values.
171/// Amounts are stored as i64 cents for exact arithmetic.
172#[derive(Clone)]
173pub struct AmountBuffer {
174    /// Amounts in cents
175    data: Vec<i64>,
176}
177
178impl AmountBuffer {
179    /// Create a new buffer with given capacity.
180    pub fn with_capacity(capacity: usize) -> Self {
181        Self {
182            data: Vec::with_capacity(capacity),
183        }
184    }
185
186    /// Clear the buffer for reuse.
187    pub fn clear(&mut self) {
188        self.data.clear();
189    }
190
191    /// Push an amount in cents.
192    #[inline]
193    pub fn push(&mut self, cents: i64) {
194        self.data.push(cents);
195    }
196
197    /// Push an amount from dollars (f64).
198    #[inline]
199    pub fn push_dollars(&mut self, dollars: f64) {
200        self.data.push((dollars * 100.0).round() as i64);
201    }
202
203    /// Get amount at index.
204    #[inline]
205    pub fn get(&self, index: usize) -> Option<i64> {
206        self.data.get(index).copied()
207    }
208
209    /// Get the number of amounts.
210    #[inline]
211    pub fn len(&self) -> usize {
212        self.data.len()
213    }
214
215    /// Check if empty.
216    #[inline]
217    pub fn is_empty(&self) -> bool {
218        self.data.is_empty()
219    }
220
221    /// Validate all amounts are non-negative.
222    pub fn validate_non_negative(&self) -> BatchResult {
223        let mut result = BatchResult::with_capacity(self.data.len());
224
225        // Process in chunks for SIMD
226        for (i, &amount) in self.data.iter().enumerate() {
227            result.set(i, amount >= 0);
228        }
229
230        result
231    }
232}
233
234/// Batch of tax forms for validation.
235///
236/// Uses Structure of Arrays (SoA) layout for cache efficiency.
237pub struct FormBatch {
238    /// Payer TINs
239    pub payer_tins: TinBuffer,
240    /// Recipient TINs
241    pub recipient_tins: TinBuffer,
242    /// Interest income amounts (cents)
243    pub interest_income: AmountBuffer,
244    /// Tax year (stored as u16)
245    pub tax_years: Vec<u16>,
246    /// Configuration
247    config: ValidationConfig,
248}
249
250impl FormBatch {
251    /// Create a new batch with given capacity.
252    pub fn with_capacity(capacity: usize) -> Self {
253        Self {
254            payer_tins: TinBuffer::with_capacity(capacity),
255            recipient_tins: TinBuffer::with_capacity(capacity),
256            interest_income: AmountBuffer::with_capacity(capacity),
257            tax_years: Vec::with_capacity(capacity),
258            config: ValidationConfig::default(),
259        }
260    }
261
262    /// Create a batch with custom configuration.
263    pub fn with_config(capacity: usize, config: ValidationConfig) -> Self {
264        Self {
265            payer_tins: TinBuffer::with_capacity(capacity),
266            recipient_tins: TinBuffer::with_capacity(capacity),
267            interest_income: AmountBuffer::with_capacity(capacity),
268            tax_years: Vec::with_capacity(capacity),
269            config,
270        }
271    }
272
273    /// Clear the batch for reuse.
274    pub fn clear(&mut self) {
275        self.payer_tins.clear();
276        self.recipient_tins.clear();
277        self.interest_income.clear();
278        self.tax_years.clear();
279    }
280
281    /// Get the number of forms in the batch.
282    #[inline]
283    pub fn len(&self) -> usize {
284        self.payer_tins.len()
285    }
286
287    /// Check if the batch is empty.
288    #[inline]
289    pub fn is_empty(&self) -> bool {
290        self.payer_tins.is_empty()
291    }
292
293    /// Push TINs for a form.
294    pub fn push_tins(&mut self, payer_tin: &str, recipient_tin: &str) {
295        self.payer_tins.push(payer_tin);
296        self.recipient_tins.push(recipient_tin);
297    }
298
299    /// Push an interest income amount.
300    pub fn push_income(&mut self, cents: i64) {
301        self.interest_income.push(cents);
302    }
303
304    /// Push a tax year.
305    pub fn push_tax_year(&mut self, year: u16) {
306        self.tax_years.push(year);
307    }
308
309    /// Validate all forms in the batch.
310    ///
311    /// Returns a BatchResult with validation flags for each form.
312    pub fn validate(&self) -> BatchResult {
313        let count = self.len();
314        let mut result = BatchResult::with_capacity(count);
315
316        for i in 0..count {
317            let mut valid = true;
318
319            // Validate payer TIN
320            if self.payer_tins.is_format_valid(i) {
321                if let Some(digits) = self.payer_tins.get_digits(i) {
322                    valid &= is_valid_tin_digits(digits);
323                }
324            } else {
325                valid = false;
326            }
327
328            // Validate recipient TIN
329            if self.config.require_both_tins {
330                if self.recipient_tins.is_format_valid(i) {
331                    if let Some(digits) = self.recipient_tins.get_digits(i) {
332                        valid &= is_valid_tin_digits(digits);
333                    }
334                } else {
335                    valid = false;
336                }
337            }
338
339            // Validate amount
340            if self.config.validate_amounts {
341                if let Some(amount) = self.interest_income.get(i) {
342                    valid &= amount >= 0;
343                }
344            }
345
346            // Validate tax year
347            if let Some(&year) = self.tax_years.get(i) {
348                valid &= (2020..=2100).contains(&year);
349            }
350
351            result.set(i, valid);
352        }
353
354        result
355    }
356
357    /// Validate TINs only (faster if you only need TIN validation).
358    pub fn validate_tins_only(&self) -> BatchResult {
359        let count = self.len();
360        let mut result = BatchResult::with_capacity(count);
361
362        for i in 0..count {
363            let payer_valid = self.payer_tins.is_format_valid(i)
364                && self
365                    .payer_tins
366                    .get_digits(i)
367                    .map(is_valid_tin_digits)
368                    .unwrap_or(false);
369
370            let recipient_valid = !self.config.require_both_tins
371                || (self.recipient_tins.is_format_valid(i)
372                    && self
373                        .recipient_tins
374                        .get_digits(i)
375                        .map(is_valid_tin_digits)
376                        .unwrap_or(false));
377
378            result.set(i, payer_valid && recipient_valid);
379        }
380
381        result
382    }
383}
384
385/// Check if TIN digits are valid (any type).
386#[inline]
387fn is_valid_tin_digits(digits: &[u8; 9]) -> bool {
388    // Check for all zeros
389    if digits.iter().all(|&d| d == 0) {
390        return false;
391    }
392
393    // Try each TIN type
394    validate_ssn_digits(digits).is_ok()
395        || validate_ein_digits(digits).is_ok()
396        || validate_itin_digits(digits)
397        || validate_atin_digits(digits)
398}
399
400// ============================================================================
401// Parallel Batch Validation
402// ============================================================================
403
404/// Validate multiple batches in parallel.
405#[cfg(feature = "rayon")]
406pub fn validate_parallel(batches: &[FormBatch]) -> Vec<BatchResult> {
407    use rayon::prelude::*;
408    batches.par_iter().map(|b| b.validate()).collect()
409}
410
411// ============================================================================
412// Tests
413// ============================================================================
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418
419    #[test]
420    fn test_tin_buffer() {
421        let mut buf = TinBuffer::with_capacity(10);
422
423        assert!(buf.push("123456789"));
424        assert!(buf.push("123-45-6789"));
425        assert!(!buf.push("12345678")); // Too few digits
426        assert!(!buf.push("1234567890")); // Too many digits
427        assert!(!buf.push("12345678a")); // Invalid char
428
429        assert_eq!(buf.len(), 5);
430        assert!(buf.is_format_valid(0));
431        assert!(buf.is_format_valid(1));
432        assert!(!buf.is_format_valid(2));
433        assert!(!buf.is_format_valid(3));
434        assert!(!buf.is_format_valid(4));
435
436        assert_eq!(buf.get_digits(0), Some(&[1, 2, 3, 4, 5, 6, 7, 8, 9]));
437        assert_eq!(buf.get_digits(1), Some(&[1, 2, 3, 4, 5, 6, 7, 8, 9]));
438    }
439
440    #[test]
441    fn test_amount_buffer() {
442        let mut buf = AmountBuffer::with_capacity(10);
443
444        buf.push(10000); // $100.00
445        buf.push(-5000); // -$50.00
446        buf.push(0);
447        buf.push_dollars(123.45);
448
449        assert_eq!(buf.get(0), Some(10000));
450        assert_eq!(buf.get(1), Some(-5000));
451        assert_eq!(buf.get(2), Some(0));
452        assert_eq!(buf.get(3), Some(12345));
453
454        let result = buf.validate_non_negative();
455        assert!(result.is_valid(0));
456        assert!(!result.is_valid(1));
457        assert!(result.is_valid(2));
458        assert!(result.is_valid(3));
459    }
460
461    #[test]
462    fn test_form_batch() {
463        let mut batch = FormBatch::with_capacity(10);
464
465        // Valid form
466        batch.push_tins("123-45-6789", "987-65-4321");
467        batch.push_income(10000);
468        batch.push_tax_year(2024);
469
470        // Invalid payer TIN
471        batch.push_tins("000-00-0000", "123-45-6789");
472        batch.push_income(5000);
473        batch.push_tax_year(2024);
474
475        // Invalid amount
476        batch.push_tins("123-45-6789", "987-65-4321");
477        batch.push_income(-1000);
478        batch.push_tax_year(2024);
479
480        let result = batch.validate();
481
482        assert!(result.is_valid(0));
483        assert!(!result.is_valid(1)); // Invalid payer TIN
484        assert!(!result.is_valid(2)); // Negative amount
485
486        assert_eq!(result.valid_count(), 1);
487        assert_eq!(result.invalid_count(), 2);
488    }
489
490    #[test]
491    fn test_batch_reuse() {
492        let mut batch = FormBatch::with_capacity(10);
493
494        batch.push_tins("123-45-6789", "987-65-4321");
495        batch.push_income(10000);
496        batch.push_tax_year(2024);
497
498        assert_eq!(batch.len(), 1);
499
500        batch.clear();
501
502        assert_eq!(batch.len(), 0);
503        assert!(batch.is_empty());
504
505        // Reuse
506        batch.push_tins("111-22-3333", "444-55-6666");
507        assert_eq!(batch.len(), 1);
508    }
509}