Khandkar Islam
Khandkar Islam

Reputation: 27

Calculating remainder for Caesar Cipher implementation in java

I'm working on a HackerRank problem. Solution this involves converting a string into the Caesar cipher with k shifts.

Julius Caesar protected his confidential information by encrypting it using a cipher. Caesar's cipher shifts each letter by a number of letters. If the shift takes you past the end of the alphabet, just rotate back to the front of the alphabet. In the case of a rotation by 3, w, x, y and z would map to z, a, b and c.

Original alphabet:   abcdefghijklmnopqrstuvwxyz 
Alphabet rotated +3: defghijklmnopqrstuvwxyzabc

Example:

s = "There's-a-starman-wayting-in-the-sky"

k = 3

The encrypted string: "Wkhuh'v-d-vwdupdq-zdlwlqj-lq-wkh-vnb"

Note: The cipher only encrypts letters; symbols, such as -, remain unencrypted.

Function Description:

string s: clear text - a valid ASCII string without any spaces; int k: the alphabet rotation factor.

Constraints:

1 <= n <= 100
0 <= k <= 100

I need some help to understand where the issue with my code is rooted.

My guess is that it might reside in the lines responsible for handling upper case and lower case letters.

Lower case letters

int remainder = (asciiArr[i] + k) % UPPER_BOUND_LOWERCASE;

The upperbound (the highest ascii int value) for lower case character is z which is equal to the character z which is 122.

Now lets consider a case where my code works.

Lets assume we wish to shift the letter b by 27 places.

ASCII value of b which is 98 therefore remainder is

remainder = 98 + 27 % 122 which is 3

In our second line we simply add the remainder to the array of ascii ints, so our array contains only one single element [98] after the remainder is calculated we simply do.

96 (lowerbound for lowercase characters) + remainder = 98 therefore our shift from b to 27 places is c.

However my code does not work for cases such as A,Z,z and a.

I suspect it is to do with my modulo operation.

Can someone please help me out here is my full code and my approach.

  1. Convert string to char array
  2. Convert char array to ascii int array.
  3. if ascii int value + remainder > 122 for lower case characters calculate remainder and shift to new position else just do value + shift.

My code:

public static String caesarCipher(String s, int k) {
    final int UPPER_BOUND_LOWERCASE = 122;
    final int UPPER_BOUND_UPPERCASE = 90;
    int[] asciiArr = new int[s.length()];
    
    //populate ascii Array;
    
    for (int i = 0; i < s.toCharArray().length; ++i) {
        asciiArr[i] = (int) s.toCharArray()[i];
        
        char c = (char) asciiArr[i];
        
        if (asciiArr[i] + k <= UPPER_BOUND_LOWERCASE && Character.isAlphabetic(c) && !Character.isUpperCase(c)) {
            asciiArr[i] = asciiArr[i] + k;
        } else if (asciiArr[i] + k > UPPER_BOUND_LOWERCASE && Character.isAlphabetic(c) && !Character.isUpperCase(c)) {
            int remainder = (asciiArr[i] + k) % UPPER_BOUND_LOWERCASE;
            asciiArr[i] = 96 + remainder;
        }
        if (asciiArr[i] + k <= UPPER_BOUND_UPPERCASE && Character.isAlphabetic(c) && Character.isUpperCase(c)) {
            asciiArr[i] = asciiArr[i] + k;
        } else if (asciiArr[i] + k > UPPER_BOUND_UPPERCASE && Character.isAlphabetic(c) && Character.isUpperCase(c)) {
            int remainder = (asciiArr[i] + k) % UPPER_BOUND_UPPERCASE;
            asciiArr[i] = 64 + remainder;
        }
    }
    return arrayToString(asciiArr);
}


public static String arrayToString(int[] arr) {
    StringBuilder stringBuilder = new StringBuilder();
    
    for (int i = 0; i < arr.length; ++i) {
        stringBuilder.append((char) arr[i]);
    }
    
    return stringBuilder.toString();
}

Upvotes: 0

Views: 950

Answers (1)

Alexander Ivanchenko
Alexander Ivanchenko

Reputation: 28988

There's no need in generating a character array via String.toCharArray() (because it allocates in memory a new array and populates it with the contents of the string). Instead, you can iterate over the given String examining each character using method String.charAt().

The idea of using ASCII codes isn't nice from the perspective of readability of code. And more over, using magic numbers 96 and 64 directly in the code is a huge red flag. Avoid doing this, it's very error-prone.

Conversely, the characters 'A' and 'a' from the perspective of clean-coding are quite self-descriptive, and therefore could be used directly in the code without even introducing a variable.

According to the problem description, the value of k is guaranteed to be non-negative. That simplifies the problem - we should deal only with cases when 'Z' turns into 'A' (or subsequent letter after 'A'), but not the opposite.

To obtain the encrypted character, we need to calculate the difference between the initial character and 'A' (or 'a') add k and apply modulo (%) 26 (which is the length of English alphabet). We need to needs to use modulo because k can have a value up to 100, therefore we need to adjust the difference to the alphabet size. Then we need to add the adjusted value to 'A' (or 'a') and cast the result into char (because the result of the arithmetic operation in Java would be of type int).

Note: after applying the modulo, the difference between the initial character and base ('A' or 'a') there's plus k, i.e. (letter-'A'+k)%26, this value is guaranteed to be less than 26. Therefore, we can safely add it to the base ('A' or 'a') without checking if it would be greater than 'Z' or 'z', like you've done in your code, because it can't be the case.

While encrypting a separate character, there are three cases you need to address:

  • Character is non-alphabetic - simply append it to the result as is.
  • A lower-case character - encrypted result would be 'a' + (letter - 'a' + k) % 26.
  • An upper-case character - encrypted result would be 'A' + (letter - 'A' + k) % 26.

In order to store the result without overheads of string concatenation, we can use StringBuilder.

That's how it might be implemented:

public static final int ALPHA_SIZE = 26;

public static String caesarCipher(String str, int k) {
    
    StringBuilder result = new StringBuilder();
    
    for (int i = 0; i < str.length(); i++) {
        char next = str.charAt(i);
        char encrypted = next;
        
        if (Character.isUpperCase(next)) {
            encrypted = (char) ('A' + (next - 'A' + k) % ALPHA_SIZE);
        }
        if (Character.isLowerCase(next))  {
            encrypted = (char) ('a' + (next - 'a' + k) % ALPHA_SIZE);
        }
        result.append(encrypted);
    }
    return result.toString();
}

This solution passes all test cases on HackerRank.

Upvotes: 1

Related Questions