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
use rand::Rng;

const SIZE: usize = 14;

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

/// Removes specific symbols from a CNPJ (Brazilian Company Registration Number) string.
///
/// This function takes a CNPJ string as input and removes all occurrences of
/// the '.', '/' and '-' characters from it.
///
/// # Arguments
///
/// * `dirty` - The CNPJ string containing symbols to be removed.
///
/// # Returns
///
/// A new string with the specified symbols removed.
///
/// # Examples
///
/// ```
/// use brazilian_utils::cnpj::remove_symbols;
///
/// assert_eq!(remove_symbols("12.345.678/9012-34"), "12345678901234");
/// assert_eq!(remove_symbols("98.765.432/1098-76"), "98765432109876");
/// ```
pub fn remove_symbols(dirty: &str) -> String {
    dirty
        .chars()
        .filter(|c| *c != '.' && *c != '/' && *c != '-')
        .collect()
}

/// Formats a CNPJ (Brazilian Company Registration Number) string for visual display.
///
/// This function takes a CNPJ string as input, validates its format, and
/// formats it with standard visual aid symbols for display purposes.
///
/// # Arguments
///
/// * `cnpj` - The CNPJ string to be formatted for display.
///
/// # Returns
///
/// The formatted CNPJ with visual aid symbols if it's valid, None if it's not valid.
///
/// # Examples
///
/// ```
/// use brazilian_utils::cnpj::format_cnpj;
///
/// assert_eq!(format_cnpj("03560714000142"), Some("03.560.714/0001-42".to_string()));
/// assert_eq!(format_cnpj("98765432100100"), None);
/// ```
pub fn format_cnpj(cnpj: &str) -> Option<String> {
    if !is_valid(cnpj) {
        return None;
    }

    Some(format!(
        "{}.{}.{}/{}-{}",
        &cnpj[0..2],
        &cnpj[2..5],
        &cnpj[5..8],
        &cnpj[8..12],
        &cnpj[12..14]
    ))
}

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

/// Validates a CNPJ (Brazilian Company Registration Number) by comparing its
/// verifying checksum digits to its base number.
///
/// This function checks the validity of a CNPJ by comparing its verifying
/// checksum digits to its base number. The input should be a string of digits
/// with the appropriate length.
///
/// # Arguments
///
/// * `cnpj` - The CNPJ to be validated.
///
/// # Returns
///
/// `true` if the checksum digits match the base number, `false` otherwise.
///
/// # Examples
///
/// ```
/// use brazilian_utils::cnpj::validate;
///
/// assert_eq!(validate("03560714000142"), true);
/// assert_eq!(validate("00111222000133"), false);
/// ```
pub fn validate(cnpj: &str) -> bool {
    if !cnpj.chars().all(|c| c.is_ascii_digit()) || cnpj.len() != SIZE {
        return false;
    }

    // Check if all digits are the same
    if cnpj.chars().all(|c| c == cnpj.chars().next().unwrap()) {
        return false;
    }

    // Validate both checksum digits
    let digit_13 = hashdigit(cnpj, 13);
    let digit_14 = hashdigit(cnpj, 14);

    cnpj.chars().nth(12).unwrap().to_digit(10).unwrap() == digit_13 as u32
        && cnpj.chars().nth(13).unwrap().to_digit(10).unwrap() == digit_14 as u32
}

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

/// Generates a random valid CNPJ digit string.
///
/// An optional branch number parameter can be given; it defaults to 1.
///
/// # Arguments
///
/// * `branch` - An optional branch number to be included in the CNPJ.
///
/// # Returns
///
/// A randomly generated valid CNPJ string.
///
/// # Examples
///
/// ```
/// use brazilian_utils::cnpj::{generate, is_valid};
///
/// let cnpj = generate(Some(1));
/// assert!(is_valid(&cnpj));
///
/// let cnpj2 = generate(None);
/// assert!(is_valid(&cnpj2));
/// ```
pub fn generate(branch: Option<u32>) -> String {
    let mut rng = rand::thread_rng();

    let mut branch_num = branch.unwrap_or(1);
    branch_num %= 10000;
    if branch_num == 0 {
        branch_num = 1;
    }

    let branch_str = format!("{:04}", branch_num);
    let base_num = format!("{:08}", rng.gen_range(0..=99999999));
    let base = format!("{}{}", base_num, branch_str);

    let checksum = compute_checksum(&base);
    format!("{}{}", base, checksum)
}

/// Calculates the checksum digit at the given position for the provided CNPJ.
///
/// The input must contain all elements before position.
///
/// # Arguments
///
/// * `cnpj` - The CNPJ for which the checksum digit is calculated.
/// * `position` - The position of the checksum digit to be calculated (13 or 14).
///
/// # Returns
///
/// The calculated checksum digit.
///
/// # Examples
///
/// ```
/// use brazilian_utils::cnpj::hashdigit;
///
/// assert_eq!(hashdigit("12345678901234", 13), 3);
/// assert_eq!(hashdigit("00000000000000", 13), 0);
/// ```
pub fn hashdigit(cnpj: &str, position: usize) -> usize {
    let weights: Vec<usize> = if position == 13 {
        vec![5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]
    } else {
        vec![6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]
    };

    let sum: usize = cnpj
        .chars()
        .take(position - 1)
        .enumerate()
        .map(|(i, c)| c.to_digit(10).unwrap() as usize * weights[i])
        .sum();

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

/// Calculates the verifying checksum digits for a given CNPJ base number.
///
/// This function computes the verifying checksum digits for a provided CNPJ
/// base number. The `basenum` should be a digit-string of the appropriate length.
///
/// # Arguments
///
/// * `basenum` - The base number of the CNPJ for which verifying checksum digits are calculated.
///
/// # Returns
///
/// The verifying checksum digits as a string.
///
/// # Examples
///
/// ```
/// use brazilian_utils::cnpj::compute_checksum;
///
/// assert_eq!(compute_checksum("123456789012"), "30");
/// assert_eq!(compute_checksum("000000000000"), "00");
/// ```
pub fn compute_checksum(basenum: &str) -> String {
    let digit1 = hashdigit(basenum, 13);
    let with_digit1 = format!("{}{}", basenum, digit1);
    let digit2 = hashdigit(&with_digit1, 14);

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

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

    #[test]
    fn test_remove_symbols() {
        assert_eq!(remove_symbols("00000000000"), "00000000000");
        assert_eq!(remove_symbols("12.345.678/0001-90"), "12345678000190");
        assert_eq!(remove_symbols("134..2435/.-1892.-"), "13424351892");
        assert_eq!(remove_symbols("abc1230916*!*&#"), "abc1230916*!*&#");
        assert_eq!(
            remove_symbols("ab.c1.--.2-3/09.-1-./6/-.*.-!*&#"),
            "abc1230916*!*&#"
        );
        assert_eq!(remove_symbols("/...---.../"), "");
    }

    #[test]
    fn test_format_cnpj() {
        // Valid CNPJs should be formatted
        assert_eq!(
            format_cnpj("03560714000142"),
            Some("03.560.714/0001-42".to_string())
        );
        assert_eq!(
            format_cnpj("01838723000127"),
            Some("01.838.723/0001-27".to_string())
        );
        assert_eq!(
            format_cnpj("34665388000161"),
            Some("34.665.388/0001-61".to_string())
        );

        // Invalid CNPJs should return None
        assert_eq!(format_cnpj("98765432100100"), None);
        assert_eq!(format_cnpj("00111222000133"), None);
        assert_eq!(format_cnpj("00000000000000"), None);
        assert_eq!(format_cnpj("12345"), None);
    }

    #[test]
    fn test_validate() {
        // Valid CNPJs
        assert!(validate("34665388000161"));
        assert!(validate("03560714000142"));
        assert!(validate("01838723000127"));

        // Invalid CNPJs
        assert!(!validate("52599927000100"));
        assert!(!validate("00000000000"));
        assert!(!validate("00000000000000"));
        assert!(!validate("11111111111111"));
        assert!(!validate("00111222000133"));
    }

    #[test]
    fn test_is_valid() {
        // When CNPJ's len is different of 14, returns False
        assert!(!is_valid("1"));

        // When CNPJ does not contain only digits, returns False
        assert!(!is_valid("1112223334445-"));

        // When CNPJ has only the same digit, returns false
        assert!(!is_valid("11111111111111"));

        // When rest_1 is lt 2 and the 13th digit is not 0, returns False
        assert!(!is_valid("1111111111315"));

        // When rest_1 is gte 2 and the 13th digit is not (11 - rest), returns False
        assert!(!is_valid("1111111111115"));

        // When rest_2 is lt 2 and the 14th digit is not 0, returns False
        assert!(!is_valid("11111111121205"));

        // When rest_2 is gte 2 and the 14th digit is not (11 - rest), returns False
        assert!(!is_valid("11111111113105"));

        // When CNPJ is valid
        assert!(is_valid("34665388000161"));
        assert!(is_valid("01838723000127"));
    }

    #[test]
    fn test_generate() {
        // Test that generate creates valid CNPJs
        for _ in 0..1000 {
            let cnpj = generate(None);
            assert!(is_valid(&cnpj));
            assert_eq!(cnpj.len(), 14);
        }

        // Test with specific branch numbers
        for branch in [1, 100, 1234, 9999] {
            let cnpj = generate(Some(branch));
            assert!(is_valid(&cnpj));
            assert_eq!(cnpj.len(), 14);
        }
    }

    #[test]
    fn test_hashdigit() {
        assert_eq!(hashdigit("00000000000000", 13), 0);
        assert_eq!(hashdigit("00000000000000", 14), 0);
        assert_eq!(hashdigit("52513127000292", 13), 9);
        assert_eq!(hashdigit("52513127000292", 14), 9);
        assert_eq!(hashdigit("12345678901234", 13), 3);
    }

    #[test]
    fn test_compute_checksum() {
        assert_eq!(compute_checksum("000000000000"), "00");
        assert_eq!(compute_checksum("525131270002"), "99");
        assert_eq!(compute_checksum("123456789012"), "30");
    }

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

        // Too short
        assert!(!is_valid("123456789012"));

        // Too long
        assert!(!is_valid("123456789012345"));

        // Contains letters
        assert!(!is_valid("1234567890123a"));

        // All same digit
        assert!(!is_valid("00000000000000"));
        assert!(!is_valid("99999999999999"));
    }

    #[test]
    fn test_generate_with_zero_branch() {
        // Branch 0 should become 1
        let cnpj = generate(Some(0));
        assert!(is_valid(&cnpj));
        // Branch should be "0001"
        assert_eq!(&cnpj[8..12], "0001");
    }

    #[test]
    fn test_generate_branch_modulo() {
        // Branch larger than 9999 should wrap around
        let cnpj = generate(Some(10000));
        assert!(is_valid(&cnpj));
        // Should wrap to 0, then become 1
        assert_eq!(&cnpj[8..12], "0001");
    }
}