brazilian_utils 0.1.0

Brazilian utilities for Rust - validation and formatting for CPF, CNPJ, RENAVAM, voter ID, and more
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
use rand::Rng;

const SIZE: usize = 11;

const BLACKLIST: [&str; 12] = [
    "000",
    "00000000000",
    "11111111111",
    "22222222222",
    "33333333333",
    "44444444444",
    "55555555555",
    "66666666666",
    "77777777777",
    "88888888888",
    "99999999999",
    "999999999999",
];

// FORMATTING
// ==========

/// Removes specific symbols from a CPF (Brazilian Individual Taxpayer Number) string.
///
/// This function takes a CPF string as input and removes all occurrences of
/// the '.', '-' characters from it.
///
/// # Arguments
///
/// * `dirty` - The CPF string containing symbols to be removed.
///
/// # Returns
///
/// A new string with the specified symbols removed.
///
/// # Examples
///
/// ```
/// use brazilian_utils::cpf::remove_symbols;
///
/// assert_eq!(remove_symbols("123.456.789-01"), "12345678901");
/// assert_eq!(remove_symbols("987-654-321.01"), "98765432101");
/// ```
pub fn remove_symbols(dirty: &str) -> String {
    dirty.chars().filter(|c| *c != '.' && *c != '-').collect()
}

/// Format a CPF (Brazilian Individual Taxpayer Number) for display with visual aid symbols.
///
/// This function takes a numbers-only CPF string as input and adds standard
/// formatting visual aid symbols for display.
///
/// # Arguments
///
/// * `cpf` - A numbers-only CPF string.
///
/// # Returns
///
/// A formatted CPF string with standard visual aid symbols or None if the input is invalid.
///
/// # Examples
///
/// ```
/// use brazilian_utils::cpf::format_cpf;
///
/// assert_eq!(format_cpf("82178537464"), Some("821.785.374-64".to_string()));
/// assert_eq!(format_cpf("55550207753"), Some("555.502.077-53".to_string()));
/// assert_eq!(format_cpf("12345678901"), None);
/// ```
pub fn format_cpf(cpf: &str) -> Option<String> {
    if !is_valid(cpf) {
        return None;
    }

    Some(format!(
        "{}.{}.{}-{}",
        &cpf[0..3],
        &cpf[3..6],
        &cpf[6..9],
        &cpf[9..11]
    ))
}

// OPERATIONS
// ==========

/// Validate the checksum digits of a CPF.
///
/// This function checks whether the verifying checksum digits of the given CPF
/// match its base number. The input should be a digit string of the proper length.
///
/// # Arguments
///
/// * `cpf` - A numbers-only CPF string.
///
/// # Returns
///
/// `true` if the checksum digits are valid, `false` otherwise.
///
/// # Examples
///
/// ```
/// use brazilian_utils::cpf::validate;
///
/// assert_eq!(validate("82178537464"), true);
/// assert_eq!(validate("55550207753"), true);
/// assert_eq!(validate("00011122233"), false);
/// ```
pub fn validate(cpf: &str) -> bool {
    if !cpf.chars().all(|c| c.is_ascii_digit()) || cpf.len() != SIZE {
        return false;
    }

    if is_blacklisted(cpf) {
        return false;
    }

    is_valid_checksum(cpf)
}

/// Returns whether or not the verifying checksum digits of the given CPF
/// match its base number.
///
/// This function does not verify the existence of the CPF; it only
/// validates the format of the string.
///
/// # Arguments
///
/// * `cpf` - The CPF to be validated, an 11-digit string.
///
/// # Returns
///
/// `true` if the checksum digits match the base number, `false` otherwise.
///
/// # Examples
///
/// ```
/// use brazilian_utils::cpf::is_valid;
///
/// assert_eq!(is_valid("82178537464"), true);
/// assert_eq!(is_valid("55550207753"), true);
/// assert_eq!(is_valid("00011122233"), false);
/// ```
pub fn is_valid(cpf: &str) -> bool {
    validate(cpf)
}

/// Generate a random valid CPF (Brazilian Individual Taxpayer Number) digit string.
///
/// # Returns
///
/// A random valid CPF string.
///
/// # Examples
///
/// ```
/// use brazilian_utils::cpf::{generate, is_valid};
///
/// let cpf = generate();
/// assert_eq!(cpf.len(), 11);
/// assert!(is_valid(&cpf));
/// ```
pub fn generate() -> String {
    let mut rng = rand::thread_rng();
    let base = format!("{:09}", rng.gen_range(1..=999999999));
    let checksum = compute_checksum(&base);
    format!("{}{}", base, checksum)
}

/// Compute the given position checksum digit for a CPF.
///
/// This function computes the specified position checksum digit for the CPF input.
/// The input needs to contain all elements previous to the position, or the
/// computation will yield the wrong result.
///
/// # Arguments
///
/// * `cpf` - A CPF string.
/// * `position` - The position to calculate the checksum digit for (10 or 11).
///
/// # Returns
///
/// The calculated checksum digit.
///
/// # Examples
///
/// ```
/// use brazilian_utils::cpf::hashdigit;
///
/// assert_eq!(hashdigit("52599927765", 11), 5);
/// assert_eq!(hashdigit("52599927765", 10), 6);
/// ```
pub fn hashdigit(cpf: &str, position: usize) -> usize {
    let sum: usize = cpf
        .chars()
        .take(position - 1)
        .enumerate()
        .map(|(i, c)| {
            let digit = c.to_digit(10).unwrap() as usize;
            let weight = position - i;
            digit * weight
        })
        .sum();

    let val = sum % 11;
    if val < 2 {
        0
    } else {
        11 - val
    }
}

/// Compute the checksum digits for a given CPF base number.
///
/// This function calculates the checksum digits for a given CPF base number.
/// The base number should be a digit string of adequate length (9 digits).
///
/// # Arguments
///
/// * `basenum` - A digit string of adequate length.
///
/// # Returns
///
/// The calculated checksum digits as a string.
///
/// # Examples
///
/// ```
/// use brazilian_utils::cpf::compute_checksum;
///
/// assert_eq!(compute_checksum("335451269"), "51");
/// assert_eq!(compute_checksum("382916331"), "26");
/// ```
pub fn compute_checksum(basenum: &str) -> String {
    let digit1 = hashdigit(basenum, 10);
    let with_digit1 = format!("{}{}", basenum, digit1);
    let digit2 = hashdigit(&with_digit1, 11);

    format!("{}{}", digit1, digit2)
}

fn is_blacklisted(input: &str) -> bool {
    BLACKLIST.contains(&input)
}

fn is_valid_checksum(input: &str) -> bool {
    let digit1 = hashdigit(input, 10);
    let digit2 = hashdigit(input, 11);

    input.chars().nth(9).unwrap().to_digit(10).unwrap() == digit1 as u32
        && input.chars().nth(10).unwrap().to_digit(10).unwrap() == digit2 as u32
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_remove_symbols() {
        assert_eq!(remove_symbols("00000000000"), "00000000000");
        assert_eq!(remove_symbols("123.456.789-10"), "12345678910");
        assert_eq!(remove_symbols("134..2435.-1892.-"), "13424351892");
        assert_eq!(remove_symbols("abc1230916*!*&#"), "abc1230916*!*&#");
        assert_eq!(
            remove_symbols("ab.c1.--.2-309.-1-.6-.*.-!*&#"),
            "abc1230916*!*&#"
        );
        assert_eq!(remove_symbols("...---..."), "");
    }

    #[test]
    fn test_format_cpf() {
        // Valid CPFs should be formatted
        assert_eq!(
            format_cpf("82178537464"),
            Some("821.785.374-64".to_string())
        );
        assert_eq!(
            format_cpf("55550207753"),
            Some("555.502.077-53".to_string())
        );
        assert_eq!(
            format_cpf("11144477735"),
            Some("111.444.777-35".to_string())
        );

        // Invalid CPFs should return None
        assert_eq!(format_cpf("00000000000"), None);
        assert_eq!(format_cpf("12345678901"), None);
        assert_eq!(format_cpf("1234567890"), None);
        assert_eq!(format_cpf("123456789012"), None);
    }

    #[test]
    fn test_validate() {
        // Valid CPFs
        assert!(validate("52513127765"));
        assert!(validate("52599927765"));
        assert!(validate("82178537464"));
        assert!(validate("55550207753"));

        // Invalid CPFs
        assert!(!validate("00000000000"));
        assert!(!validate("11111111111"));
        assert!(!validate("12345678901"));
        assert!(!validate("123456789"));
        assert!(!validate("12345678901a"));
    }

    #[test]
    fn test_is_valid() {
        // Valid CPFs
        assert!(is_valid("96271845860"));
        assert!(is_valid("40364478829"));
        assert!(is_valid("11144477735"));
        assert!(is_valid("82178537464"));
        assert!(is_valid("55550207753"));

        // Invalid CPFs - wrong length
        assert!(!is_valid("1"));
        assert!(!is_valid("123456789"));
        assert!(!is_valid("123456789012"));

        // Invalid CPFs - non-digits
        assert!(!is_valid("1112223334-"));
        assert!(!is_valid("111.444.777-35"));

        // Invalid CPFs - blacklisted sequences
        for input in BLACKLIST.iter() {
            assert!(!is_valid(input));
        }

        // Invalid CPFs - wrong checksum
        assert!(!is_valid("11144477705"));
        assert!(!is_valid("11144477732"));
        assert!(!is_valid("11111111215"));
    }

    #[test]
    fn test_generate() {
        // Test that generate creates valid CPFs
        for _ in 0..1000 {
            let cpf = generate();
            assert_eq!(cpf.len(), 11);
            assert!(is_valid(&cpf));
            assert!(cpf.chars().all(|c| c.is_ascii_digit()));
        }
    }

    #[test]
    fn test_hashdigit() {
        assert_eq!(hashdigit("000000000", 10), 0);
        assert_eq!(hashdigit("0000000000", 11), 0);
        assert_eq!(hashdigit("52513127765", 10), 6);
        assert_eq!(hashdigit("52513127765", 11), 5);
        assert_eq!(hashdigit("52599927765", 10), 6);
        assert_eq!(hashdigit("52599927765", 11), 5);
    }

    #[test]
    fn test_compute_checksum() {
        assert_eq!(compute_checksum("000000000"), "00");
        assert_eq!(compute_checksum("525131277"), "65");
        assert_eq!(compute_checksum("335451269"), "51");
        assert_eq!(compute_checksum("382916331"), "26");
    }

    #[test]
    fn test_is_blacklisted() {
        assert!(is_blacklisted("00000000000"));
        assert!(is_blacklisted("11111111111"));
        assert!(is_blacklisted("99999999999"));
        assert!(!is_blacklisted("12345678901"));
    }

    #[test]
    fn test_is_valid_checksum() {
        // Valid checksums
        assert!(is_valid_checksum("11144477735"));
        assert!(is_valid_checksum("96271845860"));

        // Invalid checksums
        assert!(!is_valid_checksum("11144477705"));
        assert!(!is_valid_checksum("11144477732"));
    }

    #[test]
    fn test_edge_cases() {
        // Empty string
        assert!(!is_valid(""));

        // Special characters
        assert!(!is_valid("!@#$%^&*()_"));

        // Mixed valid and invalid
        assert!(!is_valid("111444777a5"));
    }

    #[test]
    fn test_format_with_symbols() {
        // Test that formatting after removing symbols works
        let cpf_with_symbols = "821.785.374-64";
        let cpf_clean = remove_symbols(cpf_with_symbols);
        assert_eq!(format_cpf(&cpf_clean), Some("821.785.374-64".to_string()));
    }

    #[test]
    fn test_generate_uniqueness() {
        // Test that generate creates different CPFs
        let cpf1 = generate();
        let cpf2 = generate();
        let cpf3 = generate();

        // While theoretically they could be the same, the probability is very low
        // Just check they're all valid
        assert!(is_valid(&cpf1));
        assert!(is_valid(&cpf2));
        assert!(is_valid(&cpf3));
    }
}