From 546fde18df04a994c98970e75a696bcf6bbd3f1e Mon Sep 17 00:00:00 2001
From: Andrei Ovcharenko
Date: Fri, 31 Jul 2026 17:44:56 +0300
Subject: [PATCH 01/20] Validate FastSort input bounds
---
.../java/algorithms/sprint3/FastSort.java | 60 ++++++++++++-------
1 file changed, 38 insertions(+), 22 deletions(-)
diff --git a/src/main/java/algorithms/sprint3/FastSort.java b/src/main/java/algorithms/sprint3/FastSort.java
index b5a4de2..da71ce5 100644
--- a/src/main/java/algorithms/sprint3/FastSort.java
+++ b/src/main/java/algorithms/sprint3/FastSort.java
@@ -8,6 +8,9 @@
public class FastSort {
+ private static final int MAX_PARTICIPANTS = 100_000;
+ private static final int MAX_LOGIN_LENGTH = 256;
+
/*
Принцип работы алгоритма:
1) Считываем n участников в массив.
@@ -140,24 +143,43 @@ private int read() throws IOException {
return buffer[ptr++];
}
- int nextInt() throws IOException {
- int c;
- do {
- c = read();
- if (c == -1) {
- throw new EOFException();
- }
- } while (c <= ' ');
-
+ int nextInt(String fieldName, int minValue, int maxValue) throws IOException {
+ int c = nextNonWhitespace();
int value = 0;
+
while (c > ' ') {
- value = value * 10 + c - '0';
+ if (c < '0' || c > '9') {
+ throw new IOException("Invalid integer token for " + fieldName);
+ }
+ int digit = c - '0';
+ if (value > (maxValue - digit) / 10) {
+ throw new IOException("Integer token for " + fieldName + " exceeds " + maxValue);
+ }
+ value = value * 10 + digit;
c = read();
}
+
+ if (value < minValue) {
+ throw new IOException("Integer token for " + fieldName + " is below " + minValue);
+ }
return value;
}
- String next() throws IOException {
+ String next(int maxLength) throws IOException {
+ int c = nextNonWhitespace();
+ StringBuilder sb = new StringBuilder(Math.min(maxLength, 16));
+
+ while (c > ' ') {
+ if (sb.length() == maxLength) {
+ throw new IOException("Token length exceeds " + maxLength);
+ }
+ sb.append((char) c);
+ c = read();
+ }
+ return sb.toString();
+ }
+
+ private int nextNonWhitespace() throws IOException {
int c;
do {
c = read();
@@ -165,26 +187,20 @@ String next() throws IOException {
throw new EOFException();
}
} while (c <= ' ');
-
- StringBuilder sb = new StringBuilder();
- while (c > ' ') {
- sb.append((char) c);
- c = read();
- }
- return sb.toString();
+ return c;
}
}
private static void run() throws Exception {
FastIn in = new FastIn(System.in);
- int n = in.nextInt(); // n — количество участников
+ int n = in.nextInt("participant count", 0, MAX_PARTICIPANTS); // n — количество участников
Participant[] participants = new Participant[n];
for (int i = 0; i < n; i++) {
- String login = in.next();
- int solved = in.nextInt();
- int penalty = in.nextInt();
+ String login = in.next(MAX_LOGIN_LENGTH);
+ int solved = in.nextInt("solved", 0, Integer.MAX_VALUE);
+ int penalty = in.nextInt("penalty", 0, Integer.MAX_VALUE);
participants[i] = new Participant(login, solved, penalty);
}
From 0d47d123862c02a623a1b38e4e6164108db7f0cf Mon Sep 17 00:00:00 2001
From: Andrei Ovcharenko
Date: Fri, 31 Jul 2026 17:45:01 +0300
Subject: [PATCH 02/20] Fix deque index wraparound overflow
---
src/main/java/algorithms/sprint2/Deque.java | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/main/java/algorithms/sprint2/Deque.java b/src/main/java/algorithms/sprint2/Deque.java
index c3f3141..408d102 100644
--- a/src/main/java/algorithms/sprint2/Deque.java
+++ b/src/main/java/algorithms/sprint2/Deque.java
@@ -68,11 +68,13 @@ static final class RingDeque {
}
private int next(int i) {
- return (i+1) % cap;
+ i++;
+ return i == cap ? 0 : i;
}
private int prev(int i) {
- return (i-1+cap) % cap;
+ i--;
+ return i == -1 ? cap - 1 : i;
}
boolean isEmpty() {
From 3b52bcc622e22c5dd3b12c0c26d92ce7fe3a9366 Mon Sep 17 00:00:00 2001
From: Andrei Ovcharenko
Date: Fri, 31 Jul 2026 17:45:19 +0300
Subject: [PATCH 03/20] Harden Solution2 stdin handling
---
.../java/algorithms/sprint1/Solution2.java | 50 ++++++++++++++++---
.../java/algorithms/AlgorithmCliTest.java | 17 +++++++
2 files changed, 59 insertions(+), 8 deletions(-)
diff --git a/src/main/java/algorithms/sprint1/Solution2.java b/src/main/java/algorithms/sprint1/Solution2.java
index 66bb225..acab909 100644
--- a/src/main/java/algorithms/sprint1/Solution2.java
+++ b/src/main/java/algorithms/sprint1/Solution2.java
@@ -5,10 +5,13 @@
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
+import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.StringTokenizer;
public class Solution2 {
+ private static final int MAX_LINE_COUNT = 1_000_000;
+
public static void solution(Node head) {
StringBuilder output = new StringBuilder();
Node current = head;
@@ -35,16 +38,47 @@ static void test() {
}
public static void main(String[] args) throws IOException {
- StringBuilder outputBuffer = new StringBuilder();
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));
- int lineCount = parseInt(reader.readLine());
+ int lineCount = readLineCount(reader);
+ PrintWriter writer = new PrintWriter(System.out, false, StandardCharsets.UTF_8);
for (int i = 0; i < lineCount; ++i) {
- StringTokenizer tokenizer = new StringTokenizer(reader.readLine());
- int firstValue = parseInt(tokenizer.nextToken());
- int secondValue = parseInt(tokenizer.nextToken());
- int result = firstValue + secondValue;
- outputBuffer.append(result).append("\n");
+ StringTokenizer tokenizer = new StringTokenizer(readInputLine(reader, i + 1));
+ int firstValue = parseInt(nextToken(tokenizer, i + 1));
+ int secondValue = parseInt(nextToken(tokenizer, i + 1));
+ if (tokenizer.hasMoreTokens()) {
+ throw new IllegalArgumentException("Line " + (i + 1) + " must contain exactly two integers");
+ }
+ int result = Math.addExact(firstValue, secondValue);
+ writer.println(result);
+ }
+ writer.println();
+ writer.flush();
+ }
+
+ private static int readLineCount(BufferedReader reader) throws IOException {
+ String countLine = reader.readLine();
+ if (countLine == null) {
+ throw new IllegalArgumentException("Missing line count");
+ }
+ int lineCount = parseInt(countLine);
+ if (lineCount < 0 || lineCount > MAX_LINE_COUNT) {
+ throw new IllegalArgumentException("Line count must be between 0 and " + MAX_LINE_COUNT);
+ }
+ return lineCount;
+ }
+
+ private static String readInputLine(BufferedReader reader, int lineNumber) throws IOException {
+ String inputLine = reader.readLine();
+ if (inputLine == null) {
+ throw new IllegalArgumentException("Missing input line " + lineNumber);
+ }
+ return inputLine;
+ }
+
+ private static String nextToken(StringTokenizer tokenizer, int lineNumber) {
+ if (!tokenizer.hasMoreTokens()) {
+ throw new IllegalArgumentException("Line " + lineNumber + " must contain exactly two integers");
}
- System.out.println(outputBuffer);
+ return tokenizer.nextToken();
}
}
diff --git a/src/test/java/algorithms/AlgorithmCliTest.java b/src/test/java/algorithms/AlgorithmCliTest.java
index 0da7388..d46a235 100644
--- a/src/test/java/algorithms/AlgorithmCliTest.java
+++ b/src/test/java/algorithms/AlgorithmCliTest.java
@@ -1,6 +1,7 @@
package algorithms;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.params.provider.Arguments.arguments;
import java.io.ByteArrayInputStream;
@@ -33,6 +34,22 @@ void stdioEntryPointProducesExpectedOutput(
assertEquals(normalizeLines(expectedOutput), normalizeLines(invokeWithStdio(className, methodName, input)));
}
+ @Test
+ void solution2RejectsMissingInputLine() {
+ assertThrows(IllegalArgumentException.class, () -> invokeWithStdio(
+ "algorithms.sprint1.Solution2",
+ "main",
+ "2%n1 2%n".formatted()));
+ }
+
+ @Test
+ void solution2RejectsOversizedLineCount() {
+ assertThrows(IllegalArgumentException.class, () -> invokeWithStdio(
+ "algorithms.sprint1.Solution2",
+ "main",
+ "1000001%n".formatted()));
+ }
+
@Test
void distancesRunReadsAndWritesContestFiles() throws Exception {
Path input = Path.of("input.txt");
From 78a9f94751319608114be75f07322852b664ba5b Mon Sep 17 00:00:00 2001
From: Andrei Ovcharenko
Date: Sat, 1 Aug 2026 04:50:02 +0300
Subject: [PATCH 04/20] Avoid recursion in BST deletion
---
.../java/algorithms/sprint5/Solution.java | 56 ++++++++++---------
.../java/algorithms/sprint5/SolutionTest.java | 22 ++++++++
2 files changed, 53 insertions(+), 25 deletions(-)
diff --git a/src/main/java/algorithms/sprint5/Solution.java b/src/main/java/algorithms/sprint5/Solution.java
index 82ccb6c..e2b03e1 100644
--- a/src/main/java/algorithms/sprint5/Solution.java
+++ b/src/main/java/algorithms/sprint5/Solution.java
@@ -26,7 +26,7 @@ public class Solution {
*
* Почему алгоритм корректен:
* 1) По свойству BST искомый ключ может находиться только
- * в одном из двух поддеревьев, поэтому рекурсивный спуск
+ * в одном из двух поддеревьев, поэтому спуск
* идёт ровно по нужному пути.
*
* 2) Если у удаляемой вершины не более одного ребёнка,
@@ -46,43 +46,49 @@ public class Solution {
* Временная сложность: O(h), где h — высота дерева,
* что в худшем случае даёт O(n), где n — число узлов в дереве.
*
- * Дополнительная пространственная сложность: O(h) из-за стека рекурсии,
- * что в худшем случае даёт O(n).
+ * Дополнительная пространственная сложность: O(1), поскольку обход
+ * выполняется итеративно.
*/
public static Node remove(Node root, int key) {
- if (root == null) {
- return null;
+ Node parent = null;
+ Node current = root;
+
+ while (current != null && current.getValue() != key) {
+ parent = current;
+ current = key < current.getValue() ? current.getLeft() : current.getRight();
}
- if (key < root.getValue()) {
- root.setLeft(remove(root.getLeft(), key));
+ if (current == null) {
return root;
}
- if (key > root.getValue()) {
- root.setRight(remove(root.getRight(), key));
+ if (current.getLeft() != null && current.getRight() != null) {
+ Node predecessorParent = current;
+ Node predecessor = current.getLeft();
+ while (predecessor.getRight() != null) {
+ predecessorParent = predecessor;
+ predecessor = predecessor.getRight();
+ }
+
+ current.setValue(predecessor.getValue());
+ if (predecessorParent == current) {
+ predecessorParent.setLeft(predecessor.getLeft());
+ } else {
+ predecessorParent.setRight(predecessor.getLeft());
+ }
return root;
}
- if (root.getLeft() == null) {
- return root.getRight();
+ Node replacement = current.getLeft() != null ? current.getLeft() : current.getRight();
+ if (parent == null) {
+ return replacement;
}
-
- if (root.getRight() == null) {
- return root.getLeft();
+ if (parent.getLeft() == current) {
+ parent.setLeft(replacement);
+ } else {
+ parent.setRight(replacement);
}
-
- Node predecessor = findMax(root.getLeft());
- root.setValue(predecessor.getValue());
- root.setLeft(remove(root.getLeft(), predecessor.getValue()));
return root;
}
-
- private static Node findMax(Node node) {
- while (node.getRight() != null) {
- node = node.getRight();
- }
- return node;
- }
}
diff --git a/src/test/java/algorithms/sprint5/SolutionTest.java b/src/test/java/algorithms/sprint5/SolutionTest.java
index ed6a4ee..82da844 100644
--- a/src/test/java/algorithms/sprint5/SolutionTest.java
+++ b/src/test/java/algorithms/sprint5/SolutionTest.java
@@ -51,6 +51,28 @@ void removeDeletesNodeWithTwoChildrenUsingPredecessor() {
assertEquals(5, result.getRight().getValue());
}
+ @Test
+ void removeHandlesDeepSkewedTreeWithoutOverflowingStack() {
+ Node root = node(null, null, 0);
+ Node current = root;
+ for (int value = 1; value < 100_000; value++) {
+ Node next = node(null, null, value);
+ current.setRight(next);
+ current = next;
+ }
+
+ Node result = Solution.remove(root, 99_999);
+
+ assertEquals(0, result.getValue());
+ assertNull(current.getRight());
+ current = result;
+ for (int value = 0; value < 99_999; value++) {
+ assertEquals(value, current.getValue());
+ current = current.getRight();
+ }
+ assertNull(current);
+ }
+
private static Node node(Node left, Node right, int value) {
return new Node(left, right, value);
}
From 93d024c605440b075fe29914ef9a30b601947ebe Mon Sep 17 00:00:00 2001
From: Andrei Ovcharenko
Date: Sat, 1 Aug 2026 04:50:23 +0300
Subject: [PATCH 05/20] Handle malformed calculator expressions
---
.../java/algorithms/sprint2/Calculator.java | 24 +++++++++++++++++--
.../java/algorithms/AlgorithmCliTest.java | 7 ++++++
2 files changed, 29 insertions(+), 2 deletions(-)
diff --git a/src/main/java/algorithms/sprint2/Calculator.java b/src/main/java/algorithms/sprint2/Calculator.java
index 5f1da48..0a5ad1c 100644
--- a/src/main/java/algorithms/sprint2/Calculator.java
+++ b/src/main/java/algorithms/sprint2/Calculator.java
@@ -45,9 +45,16 @@ private static int eval(FastIn in) throws IOException {
if (t.length() == 1) {
char op = t.charAt(0);
if (op == '+' || op == '-' || op == '*' || op == '/') {
+ if (st.size() < 2) {
+ throw new IllegalArgumentException("Operator requires two operands: " + op);
+ }
int b = st.pop();
int a = st.pop();
+ if (op == '/' && b == 0) {
+ throw new IllegalArgumentException("Division by zero");
+ }
+
int r;
if (op == '+') {
r = a + b;
@@ -64,9 +71,16 @@ private static int eval(FastIn in) throws IOException {
}
}
- st.push(parseInt(t));
+ try {
+ st.push(parseInt(t));
+ } catch (NumberFormatException exception) {
+ throw new IllegalArgumentException("Invalid token: " + t, exception);
+ }
}
+ if (st.size() != 1) {
+ throw new IllegalArgumentException("Expression must produce exactly one result");
+ }
return st.peek();
}
@@ -74,7 +88,13 @@ private static void run() throws Exception {
FastIn in = new FastIn(System.in);
FastOut out = new FastOut(System.out);
- int ans = eval(in);
+ final int ans;
+ try {
+ ans = eval(in);
+ } catch (IllegalArgumentException exception) {
+ System.err.println("Invalid expression: " + exception.getMessage());
+ return;
+ }
out.writeInt(ans);
out.writeByte('\n');
diff --git a/src/test/java/algorithms/AlgorithmCliTest.java b/src/test/java/algorithms/AlgorithmCliTest.java
index d46a235..e036e49 100644
--- a/src/test/java/algorithms/AlgorithmCliTest.java
+++ b/src/test/java/algorithms/AlgorithmCliTest.java
@@ -19,6 +19,7 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
@Tag("integration")
class AlgorithmCliTest {
@@ -67,6 +68,12 @@ void distancesRunReadsAndWritesContestFiles() throws Exception {
}
}
+ @ParameterizedTest
+ @ValueSource(strings = {"+", "", "1 0 /", "abc", "1 2"})
+ void calculatorRejectsMalformedExpressionsWithoutThrowing(String input) throws Exception {
+ assertEquals("", invokeWithStdio("algorithms.sprint2.Calculator", "run", input));
+ }
+
private static Stream stdioCases() {
return Stream.of(
arguments("algorithms.sprint0.SlidingAverage", "main",
From 3447ca87a59234ac9536ed3fd74881b978af3d64 Mon Sep 17 00:00:00 2001
From: Andrei Ovcharenko
Date: Sat, 1 Aug 2026 04:50:40 +0300
Subject: [PATCH 06/20] Handle empty input in Distances
---
src/main/java/algorithms/sprint1/Distances.java | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/main/java/algorithms/sprint1/Distances.java b/src/main/java/algorithms/sprint1/Distances.java
index fbb674e..9c37f61 100644
--- a/src/main/java/algorithms/sprint1/Distances.java
+++ b/src/main/java/algorithms/sprint1/Distances.java
@@ -17,6 +17,7 @@ public class Distances {
static int[] solve(int[] a) {
int n = a.length;
int[] dist = new int[n];
+ if (n == 0) return dist;
int lastZero = -n;
for (int i = 0; i < n; i++) {
@@ -149,6 +150,7 @@ static void assertEq(int[] exp, int[] act, String name) {
}
static void test() {
+ assertEq(new int[]{}, solve(new int[]{}), "empty");
assertEq(new int[]{0, 1, 2, 1, 0}, solve(new int[]{0, 1, 4, 9, 0}), "sample1");
assertEq(new int[]{0, 1, 2, 3, 4, 5}, solve(new int[]{0, 7, 9, 4, 8, 20}), "sample2");
assertEq(new int[]{0}, solve(new int[]{0}), "n=1");
From 140dd4c5b3db481861e3a3cfdaae5f2088b12524 Mon Sep 17 00:00:00 2001
From: Andrei Ovcharenko
Date: Sat, 1 Aug 2026 04:51:08 +0300
Subject: [PATCH 07/20] Harden AverageElement input processing
---
src/main/java/coderun/AverageElement.java | 52 ++++++++++++++-----
src/test/java/coderun/AverageElementTest.java | 27 ++++++++++
2 files changed, 66 insertions(+), 13 deletions(-)
diff --git a/src/main/java/coderun/AverageElement.java b/src/main/java/coderun/AverageElement.java
index 461cce6..99c8dc6 100644
--- a/src/main/java/coderun/AverageElement.java
+++ b/src/main/java/coderun/AverageElement.java
@@ -3,7 +3,6 @@
import static common.SafeParse.parseInt;
-import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
@@ -11,12 +10,13 @@
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
public class AverageElement {
+ private static final int MAX_INPUT_CHARACTERS = 4096;
+
/**
* Для чтения входных данных необходимо получить их
* из стандартного потока ввода (System.in).
@@ -58,26 +58,52 @@ public static void main(String[] args) throws IOException {
}
static void solve(Reader input, Writer output) throws IOException {
- BufferedReader reader = new BufferedReader(input);
BufferedWriter writer = new BufferedWriter(output);
- String line = reader.readLine();
- if (line == null) {
+ String line = readBoundedLine(input);
+ if (line == null || line.isBlank()) {
return;
}
String[] parts = line.trim().split("\\s+");
- writer.write(String.valueOf(average(parts)));
+ try {
+ writer.write(String.valueOf(average(parts)));
+ } catch (IllegalArgumentException ignored) {
+ return;
+ }
writer.flush();
}
static Integer average(String[] list) {
- return Arrays
- .stream(list)
- .sequential()
- .map(AverageElement::parseInteger)
- .sorted()
- .toList()
- .get(1);
+ if (list.length < 2) {
+ throw new IllegalArgumentException("At least two integers are required");
+ }
+
+ int smallest = Integer.MAX_VALUE;
+ int secondSmallest = Integer.MAX_VALUE;
+ for (String value : list) {
+ int number = parseInteger(value);
+ if (number < smallest) {
+ secondSmallest = smallest;
+ smallest = number;
+ } else if (number < secondSmallest) {
+ secondSmallest = number;
+ }
+ }
+ return secondSmallest;
+ }
+
+ private static String readBoundedLine(Reader input) throws IOException {
+ StringBuilder line = new StringBuilder();
+ for (int character = input.read(); character != -1 && character != '\n'; character = input.read()) {
+ if (character == '\r') {
+ break;
+ }
+ if (line.length() == MAX_INPUT_CHARACTERS) {
+ return null;
+ }
+ line.append((char) character);
+ }
+ return line.isEmpty() ? null : line.toString();
}
private static Integer parseInteger(String value) {
diff --git a/src/test/java/coderun/AverageElementTest.java b/src/test/java/coderun/AverageElementTest.java
index 0ae380b..4cf9df9 100644
--- a/src/test/java/coderun/AverageElementTest.java
+++ b/src/test/java/coderun/AverageElementTest.java
@@ -29,4 +29,31 @@ void shouldSolveFromReaderAndNormalizeWhitespace() throws IOException {
assertEquals("5", output.toString());
}
+
+ @Test
+ void shouldIgnoreInvalidOrInsufficientInput() throws IOException {
+ assertProducesNoOutput("");
+ assertProducesNoOutput(" ");
+ assertProducesNoOutput("5");
+ assertProducesNoOutput("1 invalid 3");
+ }
+
+ @Test
+ void shouldRejectAnOversizedInputLine() throws IOException {
+ assertProducesNoOutput("1 ".repeat(2049));
+ }
+
+ @Test
+ void shouldFindSecondValueWithoutSortingTheInput() {
+ assertEquals(2, AverageElement.average(new String[]{"4", "2", "3", "1"}));
+ assertEquals(1, AverageElement.average(new String[]{"1", "1"}));
+ }
+
+ private static void assertProducesNoOutput(String input) throws IOException {
+ StringWriter output = new StringWriter();
+
+ AverageElement.solve(new StringReader(input), output);
+
+ assertEquals("", output.toString());
+ }
}
From 7a1dd39a318d100a9130bf3c2f6496750491ddee Mon Sep 17 00:00:00 2001
From: Andrei Ovcharenko
Date: Sat, 1 Aug 2026 04:51:13 +0300
Subject: [PATCH 08/20] Handle blank input in EncryptThis
---
src/main/java/kyu6/EncryptThis.java | 4 ++--
src/test/java/kyu6/EncryptThisTest.java | 11 +++++++++++
2 files changed, 13 insertions(+), 2 deletions(-)
diff --git a/src/main/java/kyu6/EncryptThis.java b/src/main/java/kyu6/EncryptThis.java
index 452f4a0..e9fa91a 100644
--- a/src/main/java/kyu6/EncryptThis.java
+++ b/src/main/java/kyu6/EncryptThis.java
@@ -7,9 +7,9 @@ public class EncryptThis {
//6 https://www.codewars.com/kata/5848565e273af816fb000449/train/java
public static String encryptThis(String text) {
- if (text == null || text.isEmpty()) return "";
+ if (text == null || text.isBlank()) return "";
StringBuilder stringBuilder = new StringBuilder();
- String[] split = text.trim().split("\\s");
+ String[] split = text.trim().split("\\s+");
for (String s : split) {
String res = "";
if (s.length() == 1) {
diff --git a/src/test/java/kyu6/EncryptThisTest.java b/src/test/java/kyu6/EncryptThisTest.java
index b11a2f5..1547c39 100644
--- a/src/test/java/kyu6/EncryptThisTest.java
+++ b/src/test/java/kyu6/EncryptThisTest.java
@@ -17,6 +17,17 @@
import static kyu6.EncryptThis.*;
@Tag("smoke")
public class EncryptThisTest {
+ @Test
+ void encryptsWordsSeparatedByWhitespace() {
+ assertEquals("65 66", encryptThis("A B"));
+ assertEquals("65 66", encryptThis(" A\tB "));
+ }
+
+ @Test
+ void returnsEmptyStringForBlankInput() {
+ assertEquals("", encryptThis(" \t\n"));
+ }
+
@Test
void smokeTestsShouldExecuteApi() {
quality.SmokeMethodTestHarness.verify(kyu6.EncryptThis.class);
From 9c941a9fd654aa4a9202fd595bba63fd2575de75 Mon Sep 17 00:00:00 2001
From: Andrei Ovcharenko
Date: Sat, 1 Aug 2026 04:51:28 +0300
Subject: [PATCH 09/20] Prevent RecursionMax stack exhaustion
---
src/main/java/other/RecursionMax.java | 15 +--------------
src/test/java/other/RecursionMaxTest.java | 8 ++++++++
2 files changed, 9 insertions(+), 14 deletions(-)
diff --git a/src/main/java/other/RecursionMax.java b/src/main/java/other/RecursionMax.java
index 65022bf..29bab74 100644
--- a/src/main/java/other/RecursionMax.java
+++ b/src/main/java/other/RecursionMax.java
@@ -7,20 +7,7 @@
public class RecursionMax {
public static int recursionMax(List array) {
- if (array == null) {
- return -1;
- }
- if (array.isEmpty()) {
- return -1;
- }
- return recursionMax(array, 0, Integer.MIN_VALUE);
- }
-
- private static int recursionMax(List array, int index, int currentMax) {
- if (index == array.size()) {
- return currentMax;
- }
- return recursionMax(array, index + 1, Math.max(currentMax, array.get(index)));
+ return max(array);
}
public static int max(List array) {
diff --git a/src/test/java/other/RecursionMaxTest.java b/src/test/java/other/RecursionMaxTest.java
index dd6f531..75cb27c 100644
--- a/src/test/java/other/RecursionMaxTest.java
+++ b/src/test/java/other/RecursionMaxTest.java
@@ -3,6 +3,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.api.Tag;
@@ -29,6 +30,13 @@ void shouldNotMutateInputList() {
assertEquals(List.of(3, 8, 1, 5), values);
}
+ @Test
+ void shouldHandleLargeListsWithoutExhaustingTheStack() {
+ List values = Collections.nCopies(100_000, 42);
+
+ assertEquals(42, RecursionMax.recursionMax(values));
+ }
+
private static Stream maxCases() {
return Stream.of(
Arguments.of(null, -1),
From ee070e763a1296b5aa4bd07a6c1371562cfb65a2 Mon Sep 17 00:00:00 2001
From: Andrei Ovcharenko
Date: Sat, 1 Aug 2026 04:51:50 +0300
Subject: [PATCH 10/20] Fail smoke verification on any invocation error
---
src/test/java/quality/SmokeMethodTestHarness.java | 2 +-
src/test/java/quality/SmokeMethodTestHarnessTest.java | 5 ++---
2 files changed, 3 insertions(+), 4 deletions(-)
diff --git a/src/test/java/quality/SmokeMethodTestHarness.java b/src/test/java/quality/SmokeMethodTestHarness.java
index 0b931ef..61d420d 100644
--- a/src/test/java/quality/SmokeMethodTestHarness.java
+++ b/src/test/java/quality/SmokeMethodTestHarness.java
@@ -58,7 +58,7 @@ public static void verify(Class> clazz) {
}
String summary = String.format("%s attempted=%d failed=%d", clazz.getSimpleName(), attempted, failed);
- Assertions.assertTrue(failed < attempted, () -> "Every executable smoke path failed: " + summary);
+ Assertions.assertEquals(0, failed, () -> "Executable smoke path failed: " + summary);
}
private static Object resolveTargetInstance(Method method, Class> clazz) throws Exception {
diff --git a/src/test/java/quality/SmokeMethodTestHarnessTest.java b/src/test/java/quality/SmokeMethodTestHarnessTest.java
index 8fef2ae..8187185 100644
--- a/src/test/java/quality/SmokeMethodTestHarnessTest.java
+++ b/src/test/java/quality/SmokeMethodTestHarnessTest.java
@@ -1,6 +1,5 @@
package quality;
-import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;
@@ -13,8 +12,8 @@ void rejectsClassesWhoseEveryExecutablePathFails() {
}
@Test
- void acceptsAClassWhenAtLeastOneExecutablePathSucceeds() {
- assertDoesNotThrow(() -> SmokeMethodTestHarness.verify(PartiallyWorks.class));
+ void rejectsAClassWhenAnyExecutablePathFails() {
+ assertThrows(AssertionError.class, () -> SmokeMethodTestHarness.verify(PartiallyWorks.class));
}
private static final class AlwaysFails {
From 85f506223102f56f30209297d50c8f60239c8975 Mon Sep 17 00:00:00 2001
From: Andrei Ovcharenko
Date: Sat, 1 Aug 2026 04:51:57 +0300
Subject: [PATCH 11/20] Prevent overflow in sliding average updates
---
src/main/java/algorithms/sprint0/SlidingAverage.java | 2 +-
src/test/java/algorithms/sprint0/SlidingAverageTest.java | 7 +++++++
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/src/main/java/algorithms/sprint0/SlidingAverage.java b/src/main/java/algorithms/sprint0/SlidingAverage.java
index 868506e..189fb1f 100644
--- a/src/main/java/algorithms/sprint0/SlidingAverage.java
+++ b/src/main/java/algorithms/sprint0/SlidingAverage.java
@@ -28,7 +28,7 @@ static List movingAverage(int n, List arr, int windowSize) {
}
result.add(sum / (double) windowSize);
for (int i = windowSize; i < minSize; i++) {
- sum += arr.get(i) - arr.get(i - windowSize);
+ sum += (long) arr.get(i) - arr.get(i - windowSize);
result.add(sum / (double) windowSize);
}
return result;
diff --git a/src/test/java/algorithms/sprint0/SlidingAverageTest.java b/src/test/java/algorithms/sprint0/SlidingAverageTest.java
index 7042b1f..5dbcd44 100644
--- a/src/test/java/algorithms/sprint0/SlidingAverageTest.java
+++ b/src/test/java/algorithms/sprint0/SlidingAverageTest.java
@@ -49,6 +49,13 @@ void largeValuesDoNotOverflowTheWindowSum() {
assertListDoubles(SlidingAverage.movingAverage(4, List.of(max, max, max, max), 2), max, max, max);
}
+ @Test
+ void extremeValuesDoNotOverflowTheRollingUpdate() {
+ assertListDoubles(SlidingAverage.movingAverage(
+ 2, List.of(Integer.MIN_VALUE, Integer.MAX_VALUE), 1),
+ Integer.MIN_VALUE, Integer.MAX_VALUE);
+ }
+
private static void assertListDoubles(List actual, double... expected) {
assertEquals(expected.length, actual.size(), "size");
for (int i = 0; i < expected.length; i++) {
From 62dbc9ce0803daeffafd488df51dc6a7e213d240 Mon Sep 17 00:00:00 2001
From: Andrei Ovcharenko
Date: Sat, 1 Aug 2026 04:52:09 +0300
Subject: [PATCH 12/20] Fix sequence counting across separated zeros
---
.../interview/MaximumSequenceWithOneZero.java | 27 ++++++++-----------
.../MaximumSequenceWithOneZeroTest.java | 5 ++++
2 files changed, 16 insertions(+), 16 deletions(-)
diff --git a/src/main/java/interview/MaximumSequenceWithOneZero.java b/src/main/java/interview/MaximumSequenceWithOneZero.java
index 55a3596..17decf2 100644
--- a/src/main/java/interview/MaximumSequenceWithOneZero.java
+++ b/src/main/java/interview/MaximumSequenceWithOneZero.java
@@ -1,7 +1,6 @@
package interview;
-import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@@ -16,24 +15,20 @@ public static int maximumSequenceWithOneZero(List b) {
if (b.size() == 1 && b.get(0) == 1) return 1;
if (b.size() == 1 && b.get(0) == 0) return 0;
- List bytes = new ArrayList<>(b);
int maxCount = 0;
- int count = 0;
- if (bytes.get(0) == 1) count++;
-
- for (int i = 1; i < bytes.size(); i++) {
- if (bytes.get(i) == 1 && bytes.get(i - 1) == 1) {
- count++;
- }
- if (bytes.get(i) == 1 && bytes.get(i - 1) == 0) {
- count++;
- }
- if (bytes.get(i) == 0 && bytes.get(i - 1) == 0) {
- maxCount = Math.max(count, maxCount);
- count = 0;
+ int previousCount = 0;
+ int currentCount = 0;
+
+ for (Integer value : b) {
+ if (value == 1) {
+ currentCount++;
+ maxCount = Math.max(maxCount, previousCount + currentCount);
+ } else {
+ previousCount = currentCount;
+ currentCount = 0;
}
}
- return Math.max(count, maxCount);
+ return maxCount;
}
diff --git a/src/test/java/interview/MaximumSequenceWithOneZeroTest.java b/src/test/java/interview/MaximumSequenceWithOneZeroTest.java
index c30b939..1ce4edb 100644
--- a/src/test/java/interview/MaximumSequenceWithOneZeroTest.java
+++ b/src/test/java/interview/MaximumSequenceWithOneZeroTest.java
@@ -28,6 +28,11 @@ void shouldSplitSequenceOnTwoConsecutiveZeros() {
assertEquals(3, maximumSequenceWithOneZero(List.of(1, 1, 0, 0, 1, 1, 1)));
}
+ @Test
+ void shouldNotCountAcrossMultipleSeparatedZeros() {
+ assertEquals(2, maximumSequenceWithOneZero(List.of(1, 0, 1, 0, 1)));
+ }
+
@Test
void shouldCountAllOnesWhenThereIsNoZeroSeparator() {
assertEquals(5, maximumSequenceWithOneZero(List.of(1, 1, 1, 1, 1)));
From bc29b9c9382cc508ba4a87e64476b215ff50dc2f Mon Sep 17 00:00:00 2001
From: Andrei Ovcharenko
Date: Sat, 1 Aug 2026 04:52:19 +0300
Subject: [PATCH 13/20] Handle literal characters in CamelCase conversion
---
src/main/java/kyu6/CamelCase.java | 3 ++-
src/test/java/kyu6/CamelCaseTest.java | 10 ++++++++++
2 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/src/main/java/kyu6/CamelCase.java b/src/main/java/kyu6/CamelCase.java
index 1093287..c14043b 100644
--- a/src/main/java/kyu6/CamelCase.java
+++ b/src/main/java/kyu6/CamelCase.java
@@ -29,9 +29,10 @@ public static String fromCamelCase(String input) {
}
public static String toCamelCase(String str) {
+ if (str == null || str.isEmpty()) return "";
return Arrays.stream(str.split(" "))
.filter(s -> s.length() > 0)
- .map(s -> s.replaceFirst(s.substring(0, 1), s.substring(0, 1).toUpperCase(Locale.ROOT)))
+ .map(s -> s.substring(0, 1).toUpperCase(Locale.ROOT) + s.substring(1))
.collect(Collectors.joining()).trim();
}
diff --git a/src/test/java/kyu6/CamelCaseTest.java b/src/test/java/kyu6/CamelCaseTest.java
index e632d8d..1b80eb2 100644
--- a/src/test/java/kyu6/CamelCaseTest.java
+++ b/src/test/java/kyu6/CamelCaseTest.java
@@ -21,4 +21,14 @@ public class CamelCaseTest {
void smokeTestsShouldExecuteApi() {
quality.SmokeMethodTestHarness.verify(kyu6.CamelCase.class);
}
+
+ @Test
+ void toCamelCaseShouldTreatRegexCharactersLiterally() {
+ assertEquals("[abc?def\\ghi$there", kyu6.CamelCase.toCamelCase("[abc ?def \\ghi $there"));
+ }
+
+ @Test
+ void toCamelCaseShouldHandleNullInput() {
+ assertEquals("", kyu6.CamelCase.toCamelCase(null));
+ }
}
From 51902ac5356abaf3ede60d2203695d7fa594719f Mon Sep 17 00:00:00 2001
From: Andrei Ovcharenko
Date: Sat, 1 Aug 2026 04:52:33 +0300
Subject: [PATCH 14/20] Handle zero in expanded form
---
src/main/java/kyu6/NumberInExpandedForm.java | 3 +++
src/test/java/kyu6/NumberInExpandedFormTest.java | 5 +++++
2 files changed, 8 insertions(+)
diff --git a/src/main/java/kyu6/NumberInExpandedForm.java b/src/main/java/kyu6/NumberInExpandedForm.java
index e64151c..bb35662 100644
--- a/src/main/java/kyu6/NumberInExpandedForm.java
+++ b/src/main/java/kyu6/NumberInExpandedForm.java
@@ -8,6 +8,9 @@ public class NumberInExpandedForm {
//6 https://www.codewars.com/kata/5842df8ccbd22792a4000245/train/java
public static String expandedForm(int num) {
+ if (num == 0) {
+ return "0";
+ }
StringBuilder result = new StringBuilder();
char[] chars = String.valueOf(num).toCharArray();
for (int i = 0; i < chars.length; i++) {
diff --git a/src/test/java/kyu6/NumberInExpandedFormTest.java b/src/test/java/kyu6/NumberInExpandedFormTest.java
index 08cc5a3..0ffe3ce 100644
--- a/src/test/java/kyu6/NumberInExpandedFormTest.java
+++ b/src/test/java/kyu6/NumberInExpandedFormTest.java
@@ -17,6 +17,11 @@
import static kyu6.NumberInExpandedForm.*;
@Tag("smoke")
public class NumberInExpandedFormTest {
+ @Test
+ void zeroShouldRemainZero() {
+ assertEquals("0", expandedForm(0));
+ }
+
@Test
void smokeTestsShouldExecuteApi() {
quality.SmokeMethodTestHarness.verify(kyu6.NumberInExpandedForm.class);
From 712618f2995a07197b42b9fb876a817df9bef4cf Mon Sep 17 00:00:00 2001
From: Andrei Ovcharenko
Date: Sat, 1 Aug 2026 04:52:38 +0300
Subject: [PATCH 15/20] fix: bound tribonacci sequence allocation
---
src/main/java/kyu6/TribonacciSequence.java | 7 +++++--
src/test/java/kyu6/TribonacciSequenceTest.java | 4 ++++
2 files changed, 9 insertions(+), 2 deletions(-)
diff --git a/src/main/java/kyu6/TribonacciSequence.java b/src/main/java/kyu6/TribonacciSequence.java
index a8749d1..2438955 100644
--- a/src/main/java/kyu6/TribonacciSequence.java
+++ b/src/main/java/kyu6/TribonacciSequence.java
@@ -5,6 +5,7 @@
public class TribonacciSequence {
private static final int SIGNATURE_SIZE = 3;
+ private static final int MAX_SEQUENCE_LENGTH = 1_000_000;
//6 https://www.codewars.com/kata/556deca17c58da83c00002db
@@ -12,8 +13,10 @@ public static double[] tribonacci(double[] s, int n) {
if (s == null || s.length != SIGNATURE_SIZE) {
throw new IllegalArgumentException("signature must contain exactly three values");
}
- if (n < 0) {
- throw new IllegalArgumentException("sequence length must not be negative");
+ if (n < 0 || n > MAX_SEQUENCE_LENGTH) {
+ throw new IllegalArgumentException(
+ "sequence length must be between 0 and " + MAX_SEQUENCE_LENGTH
+ );
}
double[] result = new double[n];
int signatureLength = Math.min(SIGNATURE_SIZE, n);
diff --git a/src/test/java/kyu6/TribonacciSequenceTest.java b/src/test/java/kyu6/TribonacciSequenceTest.java
index c308ca7..34e022b 100644
--- a/src/test/java/kyu6/TribonacciSequenceTest.java
+++ b/src/test/java/kyu6/TribonacciSequenceTest.java
@@ -31,5 +31,9 @@ void shouldRejectInvalidSignatureAndLength() {
assertThrows(IllegalArgumentException.class, () -> TribonacciSequence.tribonacci(null, 1));
assertThrows(IllegalArgumentException.class, () -> TribonacciSequence.tribonacci(new double[]{1, 1}, 2));
assertThrows(IllegalArgumentException.class, () -> TribonacciSequence.tribonacci(new double[]{1, 1, 1}, -1));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> TribonacciSequence.tribonacci(new double[]{1, 1, 1}, Integer.MAX_VALUE)
+ );
}
}
From 3071184c82408bcd2a0f8351fdfabf18fc5ecca7 Mon Sep 17 00:00:00 2001
From: Andrei Ovcharenko
Date: Sat, 1 Aug 2026 04:52:43 +0300
Subject: [PATCH 16/20] Handle null input in Maskify
---
src/main/java/kyu7/Maskify.java | 2 +-
src/test/java/kyu7/MaskifyTest.java | 5 +++++
2 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/src/main/java/kyu7/Maskify.java b/src/main/java/kyu7/Maskify.java
index 42ddda5..34bccb8 100644
--- a/src/main/java/kyu7/Maskify.java
+++ b/src/main/java/kyu7/Maskify.java
@@ -7,7 +7,7 @@ public class Maskify {
//7 https://www.codewars.com/kata/5412509bd436bd33920011bc/train/java
public static String maskify(String str) {
- if (str.length() < 5) return str;
+ if (str == null || str.length() < 5) return str;
return "#".repeat(str.length() - 4) + str.substring(str.length() - 4);
}
diff --git a/src/test/java/kyu7/MaskifyTest.java b/src/test/java/kyu7/MaskifyTest.java
index 803a117..aa874aa 100644
--- a/src/test/java/kyu7/MaskifyTest.java
+++ b/src/test/java/kyu7/MaskifyTest.java
@@ -17,6 +17,11 @@
import static kyu7.Maskify.*;
@Tag("smoke")
public class MaskifyTest {
+ @Test
+ void nullInputShouldRemainNull() {
+ assertNull(maskify(null));
+ }
+
@Test
void smokeTestsShouldExecuteApi() {
quality.SmokeMethodTestHarness.verify(kyu7.Maskify.class);
From 97c59c71c8a7e2c3274e9673fc9287a281eb3423 Mon Sep 17 00:00:00 2001
From: Andrei Ovcharenko
Date: Sat, 1 Aug 2026 04:52:58 +0300
Subject: [PATCH 17/20] Validate Rail Fence cipher rail counts
---
src/main/java/kyu3/RailFenceCipher.java | 8 ++++++++
src/test/java/kyu3/RailFenceCipherTest.java | 18 ++++++++++++++++++
2 files changed, 26 insertions(+)
diff --git a/src/main/java/kyu3/RailFenceCipher.java b/src/main/java/kyu3/RailFenceCipher.java
index 4c5c22f..c7008a5 100644
--- a/src/main/java/kyu3/RailFenceCipher.java
+++ b/src/main/java/kyu3/RailFenceCipher.java
@@ -11,6 +11,7 @@ public class RailFenceCipher {
//3 https://www.codewars.com/kata/58c5577d61aefcf3ff000081/train/java
static String encode(String s, int n) {
+ validateRailCount(n);
// Distributes characters across rails using a periodic index that walks
// up and down between boundary rails, then concatenates each rail content.
Map map = new TreeMap<>();
@@ -37,6 +38,7 @@ static String encode(String s, int n) {
}
static String decode(String s, int n) {
+ validateRailCount(n);
// First computes the exact size of each rail, splits ciphertext into
// contiguous rail segments, then reconstructs plaintext by replaying the
// same rail traversal cycle.
@@ -80,6 +82,12 @@ static String decode(String s, int n) {
return result.toString();
}
+ private static void validateRailCount(int railCount) {
+ if (railCount < 2) {
+ throw new IllegalArgumentException("Rail count must be at least 2");
+ }
+ }
+
private static int sumArr(int[] arr, int i) {
// Sum widths of all previous rails to determine the start offset
// of the current rail segment.
diff --git a/src/test/java/kyu3/RailFenceCipherTest.java b/src/test/java/kyu3/RailFenceCipherTest.java
index ccc1fcc..4693629 100644
--- a/src/test/java/kyu3/RailFenceCipherTest.java
+++ b/src/test/java/kyu3/RailFenceCipherTest.java
@@ -1,6 +1,7 @@
package kyu3;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.stream.Stream;
import org.junit.jupiter.api.Tag;
@@ -28,6 +29,15 @@ void shouldRoundTripPlainTextForDifferentRails(String text, int rails) {
assertEquals(text, RailFenceCipher.decode(encoded, rails));
}
+ @ParameterizedTest
+ @MethodSource("invalidRailCounts")
+ void shouldRejectInvalidRailCounts(int rails) {
+ assertThrows(IllegalArgumentException.class,
+ () -> RailFenceCipher.encode("ABC", rails));
+ assertThrows(IllegalArgumentException.class,
+ () -> RailFenceCipher.decode("ABC", rails));
+ }
+
private static Stream roundTripCases() {
return Stream.of(
Arguments.of("", 2),
@@ -35,4 +45,12 @@ private static Stream roundTripCases() {
Arguments.of("Rail fence cipher keeps punctuation.", 5)
);
}
+
+ private static Stream invalidRailCounts() {
+ return Stream.of(
+ Arguments.of(-1),
+ Arguments.of(0),
+ Arguments.of(1)
+ );
+ }
}
From fa42de55c0120135f00430a5548112feb3526363 Mon Sep 17 00:00:00 2001
From: Andrei Ovcharenko
Date: Sat, 1 Aug 2026 04:53:11 +0300
Subject: [PATCH 18/20] Handle empty Pig Latin tokens safely
---
src/main/java/kyu5/PigLatin.java | 4 +++-
src/test/java/kyu5/PigLatinTest.java | 7 +++++++
2 files changed, 10 insertions(+), 1 deletion(-)
diff --git a/src/main/java/kyu5/PigLatin.java b/src/main/java/kyu5/PigLatin.java
index 4527a4a..32b5bdd 100644
--- a/src/main/java/kyu5/PigLatin.java
+++ b/src/main/java/kyu5/PigLatin.java
@@ -21,7 +21,9 @@ public static String pigIt(String str) {
String[] words = str.split("\\s");
for (String w : words
) {
- if (!w.matches("\\W")) {
+ if (w.isEmpty()) {
+ continue;
+ } else if (!w.matches("\\W")) {
stringBuilder.append(w.substring(1)).append(w.charAt(0)).append(AY);
} else {
stringBuilder.append(w).append(" ");
diff --git a/src/test/java/kyu5/PigLatinTest.java b/src/test/java/kyu5/PigLatinTest.java
index a6ee6ef..ba173ce 100644
--- a/src/test/java/kyu5/PigLatinTest.java
+++ b/src/test/java/kyu5/PigLatinTest.java
@@ -21,4 +21,11 @@ public class PigLatinTest {
void smokeTestsShouldExecuteApi() {
quality.SmokeMethodTestHarness.verify(kyu5.PigLatin.class);
}
+
+ @Test
+ void handlesEmptyTokensWithoutThrowing() {
+ assertEquals("", pigIt(""));
+ assertEquals("eadinglay", pigIt(" leading"));
+ assertEquals("igPay atinlay", pigIt("Pig latin"));
+ }
}
From 53b1b1f6927002b06147f32e5c0181aeb68e03a7 Mon Sep 17 00:00:00 2001
From: Andrei Ovcharenko
Date: Sat, 1 Aug 2026 04:51:00 +0300
Subject: [PATCH 19/20] Harden Distances input and output handling
---
.../java/algorithms/sprint1/Distances.java | 21 ++++++----
.../java/algorithms/AlgorithmCliTest.java | 26 ++-----------
.../algorithms/sprint1/DistancesTest.java | 38 +++++++++++++++++++
3 files changed, 54 insertions(+), 31 deletions(-)
create mode 100644 src/test/java/algorithms/sprint1/DistancesTest.java
diff --git a/src/main/java/algorithms/sprint1/Distances.java b/src/main/java/algorithms/sprint1/Distances.java
index 9c37f61..a7041fd 100644
--- a/src/main/java/algorithms/sprint1/Distances.java
+++ b/src/main/java/algorithms/sprint1/Distances.java
@@ -3,8 +3,6 @@
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.EOFException;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -12,6 +10,7 @@
// https://contest.yandex.ru/contest/22450/run-report/157289807/
public class Distances {
+ private static final int MAX_INPUT_SIZE = 100_000;
// -------------------- SOLUTION --------------------
static int[] solve(int[] a) {
@@ -114,18 +113,20 @@ void writeInt(int x) throws IOException {
void flush() throws IOException {
out.write(buf, 0, p);
p = 0;
+ out.flush();
}
}
// -------------------- INPUT / OUTPUT --------------------
- static void run() {
-
- try (InputStream input = new BufferedInputStream(new FileInputStream("input.txt"));
- OutputStream output = new BufferedOutputStream(new FileOutputStream("output.txt"))) {
- FastIn in = new FastIn(input);
- FastOut out = new FastOut(output);
+ static void run(InputStream input, OutputStream output) {
+ try {
+ FastIn in = new FastIn(new BufferedInputStream(input));
+ FastOut out = new FastOut(new BufferedOutputStream(output));
int n = in.nextInt();
+ if (n < 1 || n > MAX_INPUT_SIZE) {
+ throw new IllegalArgumentException("Input size must be between 1 and " + MAX_INPUT_SIZE);
+ }
int[] a = new int[n];
for (int i = 0; i < n; i++) a[i] = in.nextInt();
@@ -142,6 +143,10 @@ static void run() {
}
}
+ static void run() {
+ run(System.in, System.out);
+ }
+
// -------------------- TESTS --------------------
static void assertEq(int[] exp, int[] act, String name) {
if (!Arrays.equals(exp, act)) {
diff --git a/src/test/java/algorithms/AlgorithmCliTest.java b/src/test/java/algorithms/AlgorithmCliTest.java
index e036e49..fbe72a5 100644
--- a/src/test/java/algorithms/AlgorithmCliTest.java
+++ b/src/test/java/algorithms/AlgorithmCliTest.java
@@ -11,8 +11,6 @@
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
import java.util.stream.Stream;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@@ -51,23 +49,6 @@ void solution2RejectsOversizedLineCount() {
"1000001%n".formatted()));
}
- @Test
- void distancesRunReadsAndWritesContestFiles() throws Exception {
- Path input = Path.of("input.txt");
- Path output = Path.of("output.txt");
- Files.writeString(input, "5%n0 1 4 9 0%n".formatted(), StandardCharsets.UTF_8);
- Files.deleteIfExists(output);
-
- try {
- invokeNoArg("algorithms.sprint1.Distances", "run");
-
- assertEquals("0 1 2 1 0\n", normalizeLines(Files.readString(output, StandardCharsets.UTF_8)));
- } finally {
- Files.deleteIfExists(input);
- Files.deleteIfExists(output);
- }
- }
-
@ParameterizedTest
@ValueSource(strings = {"+", "", "1 0 /", "abc", "1 2"})
void calculatorRejectsMalformedExpressionsWithoutThrowing(String input) throws Exception {
@@ -85,6 +66,9 @@ private static Stream stdioCases() {
arguments("algorithms.sprint1.SleightOfHand", "run",
"3%n1231%n2..2%n2..2%n2..2%n".formatted(),
"2%n".formatted()),
+ arguments("algorithms.sprint1.Distances", "run",
+ "5%n0 1 4 9 0%n".formatted(),
+ "0 1 2 1 0%n".formatted()),
arguments("algorithms.sprint1.Solution2", "main",
"2%n1 2%n3 4%n".formatted(),
"3%n7%n%n".formatted()),
@@ -178,10 +162,6 @@ private static String invokeWithStdio(String className, String methodName, Strin
}
}
- private static void invokeNoArg(String className, String methodName) throws Exception {
- invoke(className, methodName);
- }
-
private static void invoke(String className, String methodName) throws Exception {
Method method = "main".equals(methodName)
? Class.forName(className).getDeclaredMethod(methodName, String[].class)
diff --git a/src/test/java/algorithms/sprint1/DistancesTest.java b/src/test/java/algorithms/sprint1/DistancesTest.java
new file mode 100644
index 0000000..b71466e
--- /dev/null
+++ b/src/test/java/algorithms/sprint1/DistancesTest.java
@@ -0,0 +1,38 @@
+package algorithms.sprint1;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class DistancesTest {
+
+ @Test
+ void readsAndWritesThroughProvidedStreams() {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+
+ Distances.run(input("5 0 1 4 9 0"), output);
+
+ assertEquals("0 1 2 1 0\n", output.toString(StandardCharsets.UTF_8));
+ }
+
+ @Test
+ void rejectsNegativeInputSize() {
+ assertThrows(IllegalArgumentException.class,
+ () -> Distances.run(input("-1"), new ByteArrayOutputStream()));
+ }
+
+ @Test
+ void rejectsExcessiveInputSize() {
+ assertThrows(IllegalArgumentException.class,
+ () -> Distances.run(input("100001"), new ByteArrayOutputStream()));
+ }
+
+ private static ByteArrayInputStream input(String value) {
+ return new ByteArrayInputStream(value.getBytes(StandardCharsets.UTF_8));
+ }
+}
From aa4f7ce1820cfc2d331aaca9fcf51b46992f3c25 Mon Sep 17 00:00:00 2001
From: Andrei Ovcharenko
Date: Sat, 1 Aug 2026 04:52:54 +0300
Subject: [PATCH 20/20] Harden rail fence cipher decoding
---
src/main/java/kyu3/RailFenceCipher.java | 44 +++++----------------
src/test/java/kyu3/RailFenceCipherTest.java | 6 +++
2 files changed, 16 insertions(+), 34 deletions(-)
diff --git a/src/main/java/kyu3/RailFenceCipher.java b/src/main/java/kyu3/RailFenceCipher.java
index c7008a5..6a8ff0f 100644
--- a/src/main/java/kyu3/RailFenceCipher.java
+++ b/src/main/java/kyu3/RailFenceCipher.java
@@ -1,7 +1,6 @@
package kyu3;
-import java.util.LinkedList;
import java.util.Map;
import java.util.TreeMap;
@@ -42,41 +41,28 @@ static String decode(String s, int n) {
// First computes the exact size of each rail, splits ciphertext into
// contiguous rail segments, then reconstructs plaintext by replaying the
// same rail traversal cycle.
+ if (s.isEmpty() || n >= s.length()) {
+ return s;
+ }
+
int[] size = new int[n];
Counter counter = new Counter(n - 1);
for (int i = 0; i < s.length(); i++) {
int tik = counter.tik();
size[tik]++;
}
- Map map = new TreeMap<>();
- StringBuilder result = new StringBuilder();
+ int[] position = new int[n];
+ int start = 0;
for (int i = 0; i < n; i++) {
- StringBuilder stringBuilder = new StringBuilder();
- if (i == 0) {
- stringBuilder.append(s, 0, size[0]);
- } else {
- int start = sumArr(size, i);
- int end = start + size[i];
- stringBuilder.append(s, start, end);
- }
- map.put(i, stringBuilder);
- }
-
- Map> map2 = new TreeMap<>();
-
- for (Map.Entry e : map.entrySet()
- ) {
- LinkedList strings = new LinkedList<>();
- for (char c : e.getValue().toString().toCharArray()) {
- strings.add(String.valueOf(c));
- }
- map2.put(e.getKey(), strings);
+ position[i] = start;
+ start += size[i];
}
+ StringBuilder result = new StringBuilder(s.length());
Counter counterRes = new Counter(n - 1);
for (int i = 0; i < s.length(); i++) {
int tik = counterRes.tik();
- result.append(map2.get(tik).poll());
+ result.append(s.charAt(position[tik]++));
}
return result.toString();
@@ -88,16 +74,6 @@ private static void validateRailCount(int railCount) {
}
}
- private static int sumArr(int[] arr, int i) {
- // Sum widths of all previous rails to determine the start offset
- // of the current rail segment.
- int result = 0;
- for (int j = 0; j < i; j++) {
- result += arr[j];
- }
- return result;
- }
-
/**
* Create two functions to encode and then decode a string using the Rail Fence Cipher. This cipher is used to encode
* a string by placing each character successively in a diagonal along a set of "rails". First start off moving diagonally
diff --git a/src/test/java/kyu3/RailFenceCipherTest.java b/src/test/java/kyu3/RailFenceCipherTest.java
index 4693629..1ad9787 100644
--- a/src/test/java/kyu3/RailFenceCipherTest.java
+++ b/src/test/java/kyu3/RailFenceCipherTest.java
@@ -21,6 +21,12 @@ void shouldEncodeAndDecodeKnownExample() {
assertEquals("WEAREDISCOVEREDFLEEATONCE", RailFenceCipher.decode(encoded, 3));
}
+ @Test
+ void shouldDecodeWithoutAllocatingForUnusedRails() {
+ assertEquals("", RailFenceCipher.decode("", Integer.MAX_VALUE));
+ assertEquals("abc", RailFenceCipher.decode("abc", Integer.MAX_VALUE));
+ }
+
@ParameterizedTest
@MethodSource("roundTripCases")
void shouldRoundTripPlainTextForDifferentRails(String text, int rails) {