Skip to content

Commit 75728a4

Browse files
Fix character loss in RailFenceCipher for inputs containing newlines
1 parent 346f591 commit 75728a4

2 files changed

Lines changed: 99 additions & 27 deletions

File tree

src/main/java/com/thealgorithms/ciphers/RailFenceCipher.java

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package com.thealgorithms.ciphers;
22

3-
import java.util.Arrays;
4-
53
/**
64
* The rail fence cipher (also called a zigzag cipher) is a classical type of transposition cipher.
75
* It derives its name from the manner in which encryption is performed, in analogy to a fence built with horizontal rails.
@@ -14,28 +12,27 @@ public class RailFenceCipher {
1412
// Encrypts the input string using the rail fence cipher method with the given number of rails.
1513
public String encrypt(String str, int rails) {
1614

15+
checkInput(str, rails);
16+
1717
// Base case of single rail or rails are more than the number of characters in the string
1818
if (rails == 1 || rails >= str.length()) {
1919
return str;
2020
}
2121

22-
// Boolean flag to determine if the movement is downward or upward in the rail matrix.
22+
// Boolean flag to determine if the movement is downward or upward in the rail pattern.
2323
boolean down = true;
24-
// Create a 2D array to represent the rails (rows) and the length of the string (columns).
25-
char[][] strRail = new char[rails][str.length()];
26-
27-
// Initialize all positions in the rail matrix with a placeholder character ('\n').
24+
// Collect the characters of every rail separately. Using one buffer per rail (instead of a
25+
// rails x length matrix with a placeholder character) keeps every character of the input,
26+
// including characters that would otherwise be indistinguishable from the placeholder.
27+
StringBuilder[] railBuffers = new StringBuilder[rails];
2828
for (int i = 0; i < rails; i++) {
29-
Arrays.fill(strRail[i], '\n');
29+
railBuffers[i] = new StringBuilder();
3030
}
3131

32-
int row = 0; // Start at the first row
33-
int col = 0; // Start at the first column
32+
int row = 0; // Start at the first rail
3433

35-
int i = 0;
36-
37-
// Fill the rail matrix with characters from the string based on the rail pattern.
38-
while (col < str.length()) {
34+
// Distribute the characters of the string over the rails following the zigzag pattern.
35+
for (int i = 0; i < str.length(); i++) {
3936
// Change direction to down when at the first row.
4037
if (row == 0) {
4138
down = true;
@@ -45,33 +42,28 @@ else if (row == rails - 1) {
4542
down = false;
4643
}
4744

48-
// Place the character in the current position of the rail matrix.
49-
strRail[row][col] = str.charAt(i);
50-
col++; // Move to the next column.
45+
// Append the character to the rail it belongs to.
46+
railBuffers[row].append(str.charAt(i));
5147
// Move to the next row based on the direction.
5248
if (down) {
5349
row++;
5450
} else {
5551
row--;
5652
}
57-
58-
i++;
5953
}
6054

61-
// Construct the encrypted string by reading characters row by row.
62-
StringBuilder encryptedString = new StringBuilder();
63-
for (char[] chRow : strRail) {
64-
for (char ch : chRow) {
65-
if (ch != '\n') {
66-
encryptedString.append(ch);
67-
}
68-
}
55+
// Construct the encrypted string by reading the rails top to bottom.
56+
StringBuilder encryptedString = new StringBuilder(str.length());
57+
for (StringBuilder railBuffer : railBuffers) {
58+
encryptedString.append(railBuffer);
6959
}
7060
return encryptedString.toString();
7161
}
7262
// Decrypts the input string using the rail fence cipher method with the given number of rails.
7363
public String decrypt(String str, int rails) {
7464

65+
checkInput(str, rails);
66+
7567
// Base case of single rail or rails are more than the number of characters in the string
7668
if (rails == 1 || rails >= str.length()) {
7769
return str;
@@ -144,4 +136,14 @@ else if (row == rails - 1) {
144136

145137
return decryptedString.toString();
146138
}
139+
140+
// Rejects inputs the zigzag pattern is not defined for.
141+
private static void checkInput(String str, int rails) {
142+
if (str == null) {
143+
throw new IllegalArgumentException("Input string must not be null");
144+
}
145+
if (rails <= 0) {
146+
throw new IllegalArgumentException("Number of rails must be positive, but was " + rails);
147+
}
148+
}
147149
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package com.thealgorithms.ciphers;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertThrows;
5+
6+
import org.junit.jupiter.api.Test;
7+
import org.junit.jupiter.params.ParameterizedTest;
8+
import org.junit.jupiter.params.provider.CsvSource;
9+
import org.junit.jupiter.params.provider.ValueSource;
10+
11+
class RailFenceCipherTest {
12+
13+
private final RailFenceCipher railFenceCipher = new RailFenceCipher();
14+
15+
@Test
16+
void testEncrypt() {
17+
assertEquals("WECRLTEERDSOEEFEAOCAIVDEN", railFenceCipher.encrypt("WEAREDISCOVEREDFLEEATONCE", 3));
18+
}
19+
20+
@Test
21+
void testDecrypt() {
22+
assertEquals("WEAREDISCOVEREDFLEEATONCE", railFenceCipher.decrypt("WECRLTEERDSOEEFEAOCAIVDEN", 3));
23+
}
24+
25+
@ParameterizedTest
26+
@CsvSource({"HELLOWORLD, 2", "HELLOWORLD, 3", "HELLOWORLD, 4", "ATTACKATDAWN, 5", "abcdefghij, 6"})
27+
void testRoundTrip(String message, int rails) {
28+
assertEquals(message, railFenceCipher.decrypt(railFenceCipher.encrypt(message, rails), rails));
29+
}
30+
31+
/**
32+
* Every character of the input must survive encryption, including the ones that used to collide
33+
* with the placeholder that marked unused cells of the rail matrix.
34+
*/
35+
@ParameterizedTest
36+
@ValueSource(strings = {"ab\ncdef", "line1\nline2\nline3", "\n\n\n\n\n", "a\nb", "tabs\tand\nnewlines\r\n"})
37+
void testControlCharactersArePreserved(String message) {
38+
for (int rails = 2; rails <= 5; rails++) {
39+
String encrypted = railFenceCipher.encrypt(message, rails);
40+
assertEquals(message.length(), encrypted.length(), "characters were dropped with " + rails + " rails");
41+
assertEquals(message, railFenceCipher.decrypt(encrypted, rails), "round trip failed with " + rails + " rails");
42+
}
43+
}
44+
45+
@Test
46+
void testEncryptWithNewlineMatchesReferencePattern() {
47+
// Rails of "ab\ncdef" with 3 rails: {a, d} / {b, c, e} / {\n, f}
48+
assertEquals("adbce\nf", railFenceCipher.encrypt("ab\ncdef", 3));
49+
}
50+
51+
@ParameterizedTest
52+
@CsvSource({"HELLO, 1", "HELLO, 5", "HELLO, 9", "'', 1", "'', 4"})
53+
void testDegenerateRailCountsReturnInput(String message, int rails) {
54+
assertEquals(message, railFenceCipher.encrypt(message, rails));
55+
assertEquals(message, railFenceCipher.decrypt(message, rails));
56+
}
57+
58+
@ParameterizedTest
59+
@ValueSource(ints = {0, -1, -7})
60+
void testNonPositiveRailCountThrows(int rails) {
61+
assertThrows(IllegalArgumentException.class, () -> railFenceCipher.encrypt("HELLO", rails));
62+
assertThrows(IllegalArgumentException.class, () -> railFenceCipher.decrypt("HELLO", rails));
63+
}
64+
65+
@Test
66+
void testNullInputThrows() {
67+
assertThrows(IllegalArgumentException.class, () -> railFenceCipher.encrypt(null, 3));
68+
assertThrows(IllegalArgumentException.class, () -> railFenceCipher.decrypt(null, 3));
69+
}
70+
}

0 commit comments

Comments
 (0)