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/main/java/algorithms/sprint1/Distances.java b/src/main/java/algorithms/sprint1/Distances.java index fbb674e..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,11 +10,13 @@ // 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) { int n = a.length; int[] dist = new int[n]; + if (n == 0) return dist; int lastZero = -n; for (int i = 0; i < n; i++) { @@ -113,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(); @@ -141,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)) { @@ -149,6 +155,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"); 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/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/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() { 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); } 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/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/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/main/java/kyu3/RailFenceCipher.java b/src/main/java/kyu3/RailFenceCipher.java index 4c5c22f..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; @@ -11,6 +10,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,57 +37,41 @@ 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. + 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(); } - 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]; + private static void validateRailCount(int railCount) { + if (railCount < 2) { + throw new IllegalArgumentException("Rail count must be at least 2"); } - return result; } /** 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/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/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/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/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/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/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/algorithms/AlgorithmCliTest.java b/src/test/java/algorithms/AlgorithmCliTest.java index 0da7388..fbe72a5 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; @@ -10,14 +11,13 @@ 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; 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 { @@ -34,20 +34,25 @@ void stdioEntryPointProducesExpectedOutput( } @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); + void solution2RejectsMissingInputLine() { + assertThrows(IllegalArgumentException.class, () -> invokeWithStdio( + "algorithms.sprint1.Solution2", + "main", + "2%n1 2%n".formatted())); + } - try { - invokeNoArg("algorithms.sprint1.Distances", "run"); + @Test + void solution2RejectsOversizedLineCount() { + assertThrows(IllegalArgumentException.class, () -> invokeWithStdio( + "algorithms.sprint1.Solution2", + "main", + "1000001%n".formatted())); + } - 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 { + assertEquals("", invokeWithStdio("algorithms.sprint2.Calculator", "run", input)); } private static Stream stdioCases() { @@ -61,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()), @@ -154,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/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++) { 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)); + } +} 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); } 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()); + } } 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))); diff --git a/src/test/java/kyu3/RailFenceCipherTest.java b/src/test/java/kyu3/RailFenceCipherTest.java index ccc1fcc..1ad9787 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; @@ -20,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) { @@ -28,6 +35,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 +51,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) + ); + } } 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")); + } } 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)); + } } 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); 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); 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) + ); } } 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); 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), 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 {