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