|
1 |
| -//! Caesar Cipher |
2 |
| -//! Based on cipher_crypt::caesar |
3 |
| -//! |
4 |
| -//! # Algorithm |
5 |
| -//! |
6 |
| -//! Rotate each ascii character by shift. The most basic example is ROT 13, which rotates 'a' to |
7 |
| -//! 'n'. This implementation does not rotate unicode characters. |
8 |
| -
|
9 |
| -/// Caesar cipher to rotate cipher text by shift and return an owned String. |
10 |
| -pub fn caesar(cipher: &str, shift: u8) -> String { |
11 |
| - cipher |
| 1 | +const ERROR_MESSAGE: &str = "Rotation must be in the range [0, 25]"; |
| 2 | +const ALPHABET_LENGTH: u8 = b'z' - b'a' + 1; |
| 3 | + |
| 4 | +/// Encrypts a given text using the Caesar cipher technique. |
| 5 | +/// |
| 6 | +/// In cryptography, a Caesar cipher, also known as Caesar's cipher, the shift cipher, Caesar's code, |
| 7 | +/// or Caesar shift, is one of the simplest and most widely known encryption techniques. |
| 8 | +/// It is a type of substitution cipher in which each letter in the plaintext is replaced by a letter |
| 9 | +/// some fixed number of positions down the alphabet. |
| 10 | +/// |
| 11 | +/// # Arguments |
| 12 | +/// |
| 13 | +/// * `text` - The text to be encrypted. |
| 14 | +/// * `rotation` - The number of rotations (shift) to be applied. It should be within the range [0, 25]. |
| 15 | +/// |
| 16 | +/// # Returns |
| 17 | +/// |
| 18 | +/// Returns a `Result` containing the encrypted string if successful, or an error message if the rotation |
| 19 | +/// is out of the valid range. |
| 20 | +/// |
| 21 | +/// # Errors |
| 22 | +/// |
| 23 | +/// Returns an error if the rotation value is out of the valid range [0, 25] |
| 24 | +pub fn caesar(text: &str, rotation: isize) -> Result<String, &'static str> { |
| 25 | + if !(0..ALPHABET_LENGTH as isize).contains(&rotation) { |
| 26 | + return Err(ERROR_MESSAGE); |
| 27 | + } |
| 28 | + |
| 29 | + let result = text |
12 | 30 | .chars()
|
13 | 31 | .map(|c| {
|
14 | 32 | if c.is_ascii_alphabetic() {
|
15 |
| - let first = if c.is_ascii_lowercase() { b'a' } else { b'A' }; |
16 |
| - // modulo the distance to keep character range |
17 |
| - (first + (c as u8 + shift - first) % 26) as char |
| 33 | + shift_char(c, rotation) |
18 | 34 | } else {
|
19 | 35 | c
|
20 | 36 | }
|
21 | 37 | })
|
22 |
| - .collect() |
| 38 | + .collect(); |
| 39 | + |
| 40 | + Ok(result) |
| 41 | +} |
| 42 | + |
| 43 | +/// Shifts a single ASCII alphabetic character by a specified number of positions in the alphabet. |
| 44 | +/// |
| 45 | +/// # Arguments |
| 46 | +/// |
| 47 | +/// * `c` - The ASCII alphabetic character to be shifted. |
| 48 | +/// * `rotation` - The number of positions to shift the character. Should be within the range [0, 25]. |
| 49 | +/// |
| 50 | +/// # Returns |
| 51 | +/// |
| 52 | +/// Returns the shifted ASCII alphabetic character. |
| 53 | +fn shift_char(c: char, rotation: isize) -> char { |
| 54 | + let first = if c.is_ascii_lowercase() { b'a' } else { b'A' }; |
| 55 | + let rotation = rotation as u8; // Safe cast as rotation is within [0, 25] |
| 56 | + |
| 57 | + (((c as u8 - first) + rotation) % ALPHABET_LENGTH + first) as char |
23 | 58 | }
|
24 | 59 |
|
25 | 60 | #[cfg(test)]
|
26 | 61 | mod tests {
|
27 | 62 | use super::*;
|
28 | 63 |
|
29 |
| - #[test] |
30 |
| - fn empty() { |
31 |
| - assert_eq!(caesar("", 13), ""); |
| 64 | + macro_rules! test_caesar_happy_path { |
| 65 | + ($($name:ident: $test_case:expr,)*) => { |
| 66 | + $( |
| 67 | + #[test] |
| 68 | + fn $name() { |
| 69 | + let (text, rotation, expected) = $test_case; |
| 70 | + assert_eq!(caesar(&text, rotation).unwrap(), expected); |
| 71 | + |
| 72 | + let backward_rotation = if rotation == 0 { 0 } else { ALPHABET_LENGTH as isize - rotation }; |
| 73 | + assert_eq!(caesar(&expected, backward_rotation).unwrap(), text); |
| 74 | + } |
| 75 | + )* |
| 76 | + }; |
32 | 77 | }
|
33 | 78 |
|
34 |
| - #[test] |
35 |
| - fn caesar_rot_13() { |
36 |
| - assert_eq!(caesar("rust", 13), "ehfg"); |
| 79 | + macro_rules! test_caesar_error_cases { |
| 80 | + ($($name:ident: $test_case:expr,)*) => { |
| 81 | + $( |
| 82 | + #[test] |
| 83 | + fn $name() { |
| 84 | + let (text, rotation) = $test_case; |
| 85 | + assert_eq!(caesar(&text, rotation), Err(ERROR_MESSAGE)); |
| 86 | + } |
| 87 | + )* |
| 88 | + }; |
37 | 89 | }
|
38 | 90 |
|
39 | 91 | #[test]
|
40 |
| - fn caesar_unicode() { |
41 |
| - assert_eq!(caesar("attack at dawn 攻", 5), "fyyfhp fy ifbs 攻"); |
| 92 | + fn alphabet_length_should_be_26() { |
| 93 | + assert_eq!(ALPHABET_LENGTH, 26); |
| 94 | + } |
| 95 | + |
| 96 | + test_caesar_happy_path! { |
| 97 | + empty_text: ("", 13, ""), |
| 98 | + rot_13: ("rust", 13, "ehfg"), |
| 99 | + unicode: ("attack at dawn 攻", 5, "fyyfhp fy ifbs 攻"), |
| 100 | + rotation_within_alphabet_range: ("Hello, World!", 3, "Khoor, Zruog!"), |
| 101 | + no_rotation: ("Hello, World!", 0, "Hello, World!"), |
| 102 | + rotation_at_alphabet_end: ("Hello, World!", 25, "Gdkkn, Vnqkc!"), |
| 103 | + longer: ("The quick brown fox jumps over the lazy dog.", 5, "Ymj vznhp gwtbs ktc ozrux tajw ymj qfed itl."), |
| 104 | + non_alphabetic_characters: ("12345!@#$%", 3, "12345!@#$%"), |
| 105 | + uppercase_letters: ("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 1, "BCDEFGHIJKLMNOPQRSTUVWXYZA"), |
| 106 | + mixed_case: ("HeLlO WoRlD", 7, "OlSsV DvYsK"), |
| 107 | + with_whitespace: ("Hello, World!", 13, "Uryyb, Jbeyq!"), |
| 108 | + with_special_characters: ("Hello!@#$%^&*()_+World", 4, "Lipps!@#$%^&*()_+Asvph"), |
| 109 | + with_numbers: ("Abcd1234XYZ", 10, "Klmn1234HIJ"), |
| 110 | + } |
| 111 | + |
| 112 | + test_caesar_error_cases! { |
| 113 | + negative_rotation: ("Hello, World!", -5), |
| 114 | + empty_input_negative_rotation: ("", -1), |
| 115 | + empty_input_large_rotation: ("", 27), |
| 116 | + large_rotation: ("Large rotation", 139), |
42 | 117 | }
|
43 | 118 | }
|
0 commit comments