diff --git a/algorithms-miscellaneous-5/pom.xml b/algorithms-miscellaneous-5/pom.xml index d17f93e6e0..98a7f33474 100644 --- a/algorithms-miscellaneous-5/pom.xml +++ b/algorithms-miscellaneous-5/pom.xml @@ -17,6 +17,11 @@ commons-codec ${commons-codec.version} + + org.apache.commons + commons-math3 + ${commons-math3.version} + org.projectlombok lombok @@ -28,6 +33,7 @@ tradukisto ${tradukisto.version} + org.assertj assertj-core @@ -52,6 +58,7 @@ 1.0.1 3.9.0 1.11 + 3.6.1 \ No newline at end of file diff --git a/algorithms-miscellaneous-5/src/main/java/com/baeldung/algorithms/caesarcipher/CaesarCipher.java b/algorithms-miscellaneous-5/src/main/java/com/baeldung/algorithms/caesarcipher/CaesarCipher.java new file mode 100644 index 0000000000..5ee913d251 --- /dev/null +++ b/algorithms-miscellaneous-5/src/main/java/com/baeldung/algorithms/caesarcipher/CaesarCipher.java @@ -0,0 +1,83 @@ +package com.baeldung.algorithms.caesarcipher; + +import org.apache.commons.math3.stat.inference.ChiSquareTest; + +import java.util.Arrays; +import java.util.stream.IntStream; + +public class CaesarCipher { + private static final char LETTER_A = 'a'; + private static final char LETTER_Z = 'z'; + private static final int ALPHABET_SIZE = LETTER_Z - LETTER_A + 1; + private static final double[] ENGLISH_LETTERS_PROBABILITIES = {0.073, 0.009, 0.030, 0.044, 0.130, 0.028, 0.016, 0.035, 0.074, 0.002, 0.003, 0.035, 0.025, 0.078, 0.074, 0.027, 0.003, 0.077, 0.063, 0.093, 0.027, 0.013, 0.016, 0.005, 0.019, 0.001}; + + public String cipher(String message, int offset) { + StringBuilder result = new StringBuilder(); + + for (char character : message.toCharArray()) { + if (character != ' ') { + int originalAlphabetPosition = character - LETTER_A; + int newAlphabetPosition = (originalAlphabetPosition + offset) % ALPHABET_SIZE; + char newCharacter = (char) (LETTER_A + newAlphabetPosition); + result.append(newCharacter); + } else { + result.append(character); + } + } + + return result.toString(); + } + + public String decipher(String message, int offset) { + return cipher(message, ALPHABET_SIZE - (offset % ALPHABET_SIZE)); + } + + public int breakCipher(String message) { + return probableOffset(chiSquares(message)); + } + + private double[] chiSquares(String message) { + double[] expectedLettersFrequencies = expectedLettersFrequencies(message.length()); + + double[] chiSquares = new double[ALPHABET_SIZE]; + + for (int offset = 0; offset < chiSquares.length; offset++) { + String decipheredMessage = decipher(message, offset); + long[] lettersFrequencies = observedLettersFrequencies(decipheredMessage); + double chiSquare = new ChiSquareTest().chiSquare(expectedLettersFrequencies, lettersFrequencies); + chiSquares[offset] = chiSquare; + } + + return chiSquares; + } + + private long[] observedLettersFrequencies(String message) { + return IntStream.rangeClosed(LETTER_A, LETTER_Z) + .mapToLong(letter -> countLetter((char) letter, message)) + .toArray(); + } + + private long countLetter(char letter, String message) { + return message.chars() + .filter(character -> character == letter) + .count(); + } + + private double[] expectedLettersFrequencies(int messageLength) { + return Arrays.stream(ENGLISH_LETTERS_PROBABILITIES) + .map(probability -> probability * messageLength) + .toArray(); + } + + private int probableOffset(double[] chiSquares) { + int probableOffset = 0; + for (int offset = 0; offset < chiSquares.length; offset++) { + System.out.println(String.format("Chi-Square for offset %d: %.2f", offset, chiSquares[offset])); + if (chiSquares[offset] < chiSquares[probableOffset]) { + probableOffset = offset; + } + } + + return probableOffset; + } +} diff --git a/algorithms-miscellaneous-5/src/test/java/com/baeldung/algorithms/caesarcipher/CaesarCipherUnitTest.java b/algorithms-miscellaneous-5/src/test/java/com/baeldung/algorithms/caesarcipher/CaesarCipherUnitTest.java new file mode 100644 index 0000000000..c3a22a8978 --- /dev/null +++ b/algorithms-miscellaneous-5/src/test/java/com/baeldung/algorithms/caesarcipher/CaesarCipherUnitTest.java @@ -0,0 +1,83 @@ +package com.baeldung.algorithms.caesarcipher; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class CaesarCipherUnitTest { + private static final String SENTENCE = "he told me i could never teach a llama to drive"; + private static final String SENTENCE_SHIFTED_THREE = "kh wrog ph l frxog qhyhu whdfk d oodpd wr gulyh"; + private static final String SENTENCE_SHIFTED_TEN = "ro dyvn wo s myevn xofob dokmr k vvkwk dy nbsfo"; + + private CaesarCipher algorithm = new CaesarCipher(); + + @Test + void givenSentenceAndShiftThree_whenCipher_thenCipheredMessageWithoutOverflow() { + String cipheredSentence = algorithm.cipher(SENTENCE, 3); + + assertThat(cipheredSentence) + .isEqualTo(SENTENCE_SHIFTED_THREE); + } + + @Test + void givenSentenceAndShiftTen_whenCipher_thenCipheredMessageWithOverflow() { + String cipheredSentence = algorithm.cipher(SENTENCE, 10); + + assertThat(cipheredSentence) + .isEqualTo(SENTENCE_SHIFTED_TEN); + } + + @Test + void givenSentenceAndShiftThirtySix_whenCipher_thenCipheredLikeTenMessageWithOverflow() { + String cipheredSentence = algorithm.cipher(SENTENCE, 36); + + assertThat(cipheredSentence) + .isEqualTo(SENTENCE_SHIFTED_TEN); + } + + @Test + void givenSentenceShiftedThreeAndShiftThree_whenDecipher_thenOriginalSentenceWithoutOverflow() { + String decipheredSentence = algorithm.decipher(SENTENCE_SHIFTED_THREE, 3); + + assertThat(decipheredSentence) + .isEqualTo(SENTENCE); + } + + @Test + void givenSentenceShiftedTenAndShiftTen_whenDecipher_thenOriginalSentenceWithOverflow() { + String decipheredSentence = algorithm.decipher(SENTENCE_SHIFTED_TEN, 10); + + assertThat(decipheredSentence) + .isEqualTo(SENTENCE); + } + + @Test + void givenSentenceShiftedTenAndShiftThirtySix_whenDecipher_thenOriginalSentenceWithOverflow() { + String decipheredSentence = algorithm.decipher(SENTENCE_SHIFTED_TEN, 36); + + assertThat(decipheredSentence) + .isEqualTo(SENTENCE); + } + + @Test + void givenSentenceShiftedThree_whenBreakCipher_thenOriginalSentence() { + int offset = algorithm.breakCipher(SENTENCE_SHIFTED_THREE); + + assertThat(offset) + .isEqualTo(3); + + assertThat(algorithm.decipher(SENTENCE_SHIFTED_THREE, offset)) + .isEqualTo(SENTENCE); + } + + @Test + void givenSentenceShiftedTen_whenBreakCipher_thenOriginalSentence() { + int offset = algorithm.breakCipher(SENTENCE_SHIFTED_TEN); + + assertThat(offset) + .isEqualTo(10); + + assertThat(algorithm.decipher(SENTENCE_SHIFTED_TEN, offset)) + .isEqualTo(SENTENCE); + } +} \ No newline at end of file