diff --git a/src/main/java/algorithms/sprint0/Zip.java b/src/main/java/algorithms/sprint0/Zip.java index 57dcd85..baec5c0 100644 --- a/src/main/java/algorithms/sprint0/Zip.java +++ b/src/main/java/algorithms/sprint0/Zip.java @@ -8,15 +8,18 @@ import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; +import java.io.StringReader; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import static algorithms.sprint0.Utils.printList; -import static algorithms.sprint0.Utils.readList; public class Zip { + private static final int MAX_LIST_SIZE = 100_000; + private static final int MAX_INPUT_LINE_LENGTH = 1_200_001; + static List zip(List a, List b, int n) { if (n < 0) { throw new IllegalArgumentException("n >= 0 required"); @@ -34,14 +37,53 @@ static List zip(List a, List b, int n) { public static void main(String[] args) throws IOException { try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8)); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out, StandardCharsets.UTF_8))) { - String sizeLine = reader.readLine(); - if (sizeLine == null) { - throw new EOFException("Missing list size"); + try { + process(reader, writer); + } catch (IllegalArgumentException | EOFException exception) { + System.err.println("Invalid input: " + exception.getMessage()); } - int n = parseInt(sizeLine.trim()); - List a = readList(reader); - List b = readList(reader); - printList(zip(a, b, n), writer); } } + + static void process(BufferedReader reader, BufferedWriter writer) throws IOException { + String sizeLine = readBoundedLine(reader); + if (sizeLine == null) { + throw new EOFException("Missing list size"); + } + int n = parseInt(sizeLine.trim()); + if (n < 0 || n > MAX_LIST_SIZE) { + throw new IllegalArgumentException("List size must be between 0 and " + MAX_LIST_SIZE); + } + List a = parseList(readBoundedLine(reader)); + List b = parseList(readBoundedLine(reader)); + if (a.size() < n || b.size() < n) { + throw new IllegalArgumentException("Each list must contain at least n integers"); + } + printList(zip(a, b, n), writer); + } + + private static String readBoundedLine(BufferedReader reader) throws IOException { + StringBuilder line = new StringBuilder(); + int character; + while ((character = reader.read()) != -1 && character != '\n' && character != '\r') { + if (line.length() == MAX_INPUT_LINE_LENGTH) { + throw new IllegalArgumentException("Input line is too long"); + } + line.append((char) character); + } + if (character == '\r') { + reader.mark(1); + if (reader.read() != '\n') { + reader.reset(); + } + } + return character == -1 && line.length() == 0 ? null : line.toString(); + } + + private static List parseList(String line) throws IOException { + if (line == null) { + throw new EOFException("Missing integer list"); + } + return Utils.readList(new BufferedReader(new StringReader(line))); + } } diff --git a/src/main/java/algorithms/sprint1/SleightOfHand.java b/src/main/java/algorithms/sprint1/SleightOfHand.java index 12b6fbb..eff10a0 100644 --- a/src/main/java/algorithms/sprint1/SleightOfHand.java +++ b/src/main/java/algorithms/sprint1/SleightOfHand.java @@ -48,18 +48,21 @@ int nextInt() throws IOException { return val * sign; } - String next() throws IOException { + String next(int maxLength) throws IOException { int c; do { c = read(); if (c == -1) throw new EOFException("Unexpected EOF"); } while (c <= ' '); - byte[] tmp = new byte[32]; + byte[] tmp = new byte[Math.min(32, maxLength)]; int n = 0; while (c > ' ') { + if (n == maxLength) { + throw new IOException("Token length exceeds " + maxLength); + } if (n == tmp.length) { - byte[] t2 = new byte[tmp.length * 2]; + byte[] t2 = new byte[Math.min(maxLength, tmp.length * 2)]; System.arraycopy(tmp, 0, t2, 0, tmp.length); tmp = t2; } @@ -128,14 +131,17 @@ private static void run() throws Exception { int[] count = new int[10]; for (int r = 0; r < 4; r++) { - StringBuilder row = new StringBuilder(in.next()); + StringBuilder row = new StringBuilder(in.next(4)); // На всякий случай, если токенайзер разделит строку (обычно не будет) while (row.length() < 4) { - row.append(in.next()); + row.append(in.next(4 - row.length())); } for (int c = 0; c < 4; c++) { char ch = row.charAt(c); if (ch != '.') { + if (ch < '0' || ch > '9') { + throw new IOException("Invalid grid cell: " + ch); + } count[ch - '0']++; } } @@ -219,7 +225,12 @@ static int solve(int k, int[][] a) { for (int[] row : a) { for (int v : row) { - if (v != 0) count[v]++; + if (v != 0) { + if (v < 0 || v > 9) { + throw new IllegalArgumentException("Grid values must be between 0 and 9"); + } + count[v]++; + } } } diff --git a/src/main/java/algorithms/sprint2/Deque.java b/src/main/java/algorithms/sprint2/Deque.java index 408d102..43b15db 100644 --- a/src/main/java/algorithms/sprint2/Deque.java +++ b/src/main/java/algorithms/sprint2/Deque.java @@ -55,6 +55,8 @@ public class Deque { // -------------------- RING BUFFER DEQUE -------------------- + private static final int MAX_CAPACITY = 100_000; + static final class RingDeque { private final int[] a; private final int cap; @@ -63,8 +65,8 @@ static final class RingDeque { private int size = 0; RingDeque(int cap) { - this.cap = cap; - this.a = new int[cap]; + this.cap = validateCapacity(cap); + this.a = new int[this.cap]; } private int next(int i) { @@ -112,9 +114,19 @@ int popBack() { } } + private static int validateCapacity(int cap) { + if (cap < 0 || cap > MAX_CAPACITY) { + throw new IllegalArgumentException("Deque capacity is out of range"); + } + return cap; + } + private static void process(FastIn in, FastOut out) throws Exception { int n = in.nextInt(); int m = in.nextInt(); + if (n < 0 || n > MAX_CAPACITY) { + throw new IllegalArgumentException("Command count is out of range"); + } RingDeque dq = new RingDeque(m); @@ -235,6 +247,10 @@ private static void test() throws Exception { ) ); + // Некорректная емкость отклоняется, а не меняет заявленную семантику дека. + assertRejected("2\n-1\npush_back 1\npop_front\n"); + assertRejected("1\n1000000000\npop_front\n"); + // Wrap-around: head/tail должны корректно "перепрыгивать" границу массива assertEq( "1\n4\n2\n3\n", @@ -261,6 +277,15 @@ static void assertEq(String exp, String act) { } } + private static void assertRejected(String input) throws Exception { + try { + solveIO(input); + throw new AssertionError("Expected invalid deque capacity to be rejected"); + } catch (IllegalArgumentException expected) { + // Expected validation failure. + } + } + public static void main(String[] args) throws Exception { if (System.getProperty("os.name").startsWith("Windows")) { test(); diff --git a/src/main/java/algorithms/sprint4/FindSystem.java b/src/main/java/algorithms/sprint4/FindSystem.java index 4d20189..b084c1e 100644 --- a/src/main/java/algorithms/sprint4/FindSystem.java +++ b/src/main/java/algorithms/sprint4/FindSystem.java @@ -9,11 +9,18 @@ import java.util.HashSet; import java.util.Map; import java.util.StringTokenizer; +import java.io.BufferedWriter; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; // https://contest.yandex.ru/contest/24414/run-report/160043341/ class FindSystem { + private static final int MAX_DOCUMENTS = 10_000; + private static final int MAX_QUERIES = 10_000; + private static final int MAX_LINE_LENGTH = 10_000; + /* * Принцип работы алгоритма: * 1) Строим обратный индекс: @@ -141,23 +148,25 @@ private static boolean isBetter(int docId1, int score1, int docId2, int score2) private static void solve() throws Exception { FastReader reader = new FastReader(System.in); - int n = reader.nextInt(); + int n = reader.nextInt(MAX_DOCUMENTS); String[] docs = new String[n]; for (int i = 0; i < n; i++) { - docs[i] = reader.nextLine(); + docs[i] = reader.nextLine(MAX_LINE_LENGTH); } HashMap> index = buildIndex(docs); - int m = reader.nextInt(); - StringBuilder out = new StringBuilder(); + int m = reader.nextInt(MAX_QUERIES); + BufferedWriter out = new BufferedWriter( + new OutputStreamWriter(System.out, StandardCharsets.UTF_8)); for (int i = 0; i < m; i++) { - String query = reader.nextLine(); - out.append(processQuery(query, index)).append('\n'); + String query = reader.nextLine(MAX_LINE_LENGTH); + out.write(processQuery(query, index)); + out.newLine(); } - System.out.print(out); + out.flush(); } private static void test() { @@ -229,7 +238,7 @@ private int read() throws IOException { return buffer[ptr++]; } - int nextInt() throws IOException { + int nextInt(int max) throws IOException { int c; do { c = read(); @@ -238,15 +247,21 @@ int nextInt() throws IOException { } } while (c <= ' '); - int value = 0; + long value = 0; while (c > ' ') { + if (c < '0' || c > '9') { + throw new IOException("Expected a non-negative integer"); + } value = value * 10 + c - '0'; + if (value > max) { + throw new IOException("Input value exceeds limit"); + } c = read(); } - return value; + return (int) value; } - String nextLine() throws IOException { + String nextLine(int maxLength) throws IOException { int c = read(); while (c == '\n' || c == '\r') { @@ -255,6 +270,9 @@ String nextLine() throws IOException { StringBuilder sb = new StringBuilder(); while (c != -1 && c != '\n' && c != '\r') { + if (sb.length() == maxLength) { + throw new IOException("Input line exceeds limit"); + } sb.append((char) c); c = read(); } diff --git a/src/main/java/algorithms/sprint4/Map.java b/src/main/java/algorithms/sprint4/Map.java index 4131c92..da9637b 100644 --- a/src/main/java/algorithms/sprint4/Map.java +++ b/src/main/java/algorithms/sprint4/Map.java @@ -1,9 +1,13 @@ package algorithms.sprint4; import java.io.BufferedInputStream; +import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; import java.util.OptionalInt; +import java.util.concurrent.ThreadLocalRandom; // https://contest.yandex.ru/contest/24414/run-report/160371601/ @@ -51,6 +55,8 @@ public class Map { */ private static final int SIZE = 100_003; + private static final int MAX_COMMANDS = 100_000; + private static final int HASH_SEED = ThreadLocalRandom.current().nextInt(); static class Node { int key; @@ -68,11 +74,9 @@ static class HashTable { Node[] buckets = new Node[SIZE]; int index(int key) { - int x = key % SIZE; - if (x < 0) { - x += SIZE; - } - return x; + int mixed = key ^ HASH_SEED; + mixed ^= (mixed >>> 16); + return Math.floorMod(mixed, SIZE); } void put(int key, int value) { @@ -144,8 +148,15 @@ int read() throws IOException { } int nextInt() throws IOException { + return nextInt(Integer.MIN_VALUE, Integer.MAX_VALUE); + } + + int nextInt(int min, int max) throws IOException { int c = read(); while (c <= ' ') { + if (c == -1) { + throw new IOException("Unexpected end of input"); + } c = read(); } @@ -155,13 +166,24 @@ int nextInt() throws IOException { c = read(); } - int num = 0; + if (c < '0' || c > '9') { + throw new IOException("Expected integer"); + } + + long num = 0; while (c > ' ') { + if (c < '0' || c > '9') { + throw new IOException("Expected integer"); + } num = num * 10 + c - '0'; + long signed = sign * num; + if (signed < min || signed > max) { + throw new IOException("Input value exceeds limit"); + } c = read(); } - return num * sign; + return (int) (num * sign); } char nextCommand() throws IOException { @@ -181,11 +203,20 @@ char nextCommand() throws IOException { } public static void main(String[] args) throws Exception { + try { + solve(); + } catch (IOException ignored) { + // Invalid or excessive input is rejected without exhausting memory or CPU. + } + } + + private static void solve() throws IOException { Reader reader = new Reader(); HashTable table = new HashTable(); - StringBuilder out = new StringBuilder(); + BufferedWriter out = new BufferedWriter( + new OutputStreamWriter(System.out, StandardCharsets.UTF_8)); - int n = reader.nextInt(); + int n = reader.nextInt(0, MAX_COMMANDS); for (int i = 0; i < n; i++) { char command = reader.nextCommand(); @@ -195,14 +226,18 @@ public static void main(String[] args) throws Exception { int value = reader.nextInt(); table.put(key, value); } else if (command == 'g') { - table.get(key).ifPresentOrElse(out::append, () -> out.append("None")); - out.append(System.lineSeparator()); + OptionalInt result = table.get(key); + out.write(result.isPresent() ? String.valueOf(result.getAsInt()) : "None"); + out.newLine(); + } else if (command == 'd') { + OptionalInt result = table.delete(key); + out.write(result.isPresent() ? String.valueOf(result.getAsInt()) : "None"); + out.newLine(); } else { - table.delete(key).ifPresentOrElse(out::append, () -> out.append("None")); - out.append(System.lineSeparator()); + throw new IOException("Unknown command"); } } - System.out.print(out); + out.flush(); } } diff --git a/src/main/java/algorithms/sprint5/PyramidSort.java b/src/main/java/algorithms/sprint5/PyramidSort.java index 37368de..f4ad429 100644 --- a/src/main/java/algorithms/sprint5/PyramidSort.java +++ b/src/main/java/algorithms/sprint5/PyramidSort.java @@ -9,6 +9,8 @@ //https://contest.yandex.ru/contest/24810/run-report/160623687/ public class PyramidSort { + private static final int MAX_PARTICIPANTS = 100_000; + private static final int MAX_LOGIN_BYTES = 1_024; /* * Принцип работы алгоритма: @@ -143,18 +145,33 @@ int nextInt() throws IOException { } } while (c <= ' '); - int sign = 1; - if (c == '-') { - sign = -1; + boolean negative = c == '-'; + if (negative) { c = read(); + if (c <= ' ') { + throw new NumberFormatException("Expected digit after sign"); + } } - int val = 0; + int limit = negative ? Integer.MIN_VALUE : -Integer.MAX_VALUE; + int multiplyLimit = limit / 10; + int value = 0; while (c > ' ') { - val = val * 10 + c - '0'; + if (c < '0' || c > '9') { + throw new NumberFormatException("Invalid integer input"); + } + int digit = c - '0'; + if (value < multiplyLimit) { + throw new NumberFormatException("Integer input is out of range"); + } + value *= 10; + if (value < limit + digit) { + throw new NumberFormatException("Integer input is out of range"); + } + value -= digit; c = read(); } - return val * sign; + return negative ? value : -value; } String next() throws IOException { @@ -170,6 +187,9 @@ String next() throws IOException { int n = 0; while (c > ' ') { + if (n == MAX_LOGIN_BYTES) { + throw new IOException("Login token is too long"); + } if (n == tmp.length) { byte[] next = new byte[tmp.length * 2]; System.arraycopy(tmp, 0, next, 0, tmp.length); @@ -219,6 +239,9 @@ private static void run() throws Exception { FastOut out = new FastOut(System.out); int n = in.nextInt(); + if (n < 0 || n > MAX_PARTICIPANTS) { + throw new IllegalArgumentException("Participant count is out of range"); + } Participant[] a = new Participant[n]; for (int i = 0; i < n; i++) { diff --git a/src/main/java/algorithms/sprint6/DorogayaSet.java b/src/main/java/algorithms/sprint6/DorogayaSet.java index a1a7c20..f2bca3b 100644 --- a/src/main/java/algorithms/sprint6/DorogayaSet.java +++ b/src/main/java/algorithms/sprint6/DorogayaSet.java @@ -43,8 +43,14 @@ public class DorogayaSet { static final String FAIL = "Oops! I did it again"; + private static final int MAX_VERTICES = 200_000; + private static final int MAX_EDGES = 200_000; static long solve(int n, Edge[] edges) { + if (!isValidGraph(n, edges)) { + return -1; + } + Arrays.sort(edges, (a, b) -> Integer.compare(b.weight, a.weight)); DSU dsu = new DSU(n); @@ -69,6 +75,24 @@ static long solve(int n, Edge[] edges) { return totalWeight; } + private static boolean isValidGraph(int n, Edge[] edges) { + if (n < 1 || n > MAX_VERTICES || edges == null || edges.length > MAX_EDGES) { + return false; + } + + for (Edge edge : edges) { + if (edge == null || !isValidVertex(edge.from, n) || !isValidVertex(edge.to, n)) { + return false; + } + } + + return true; + } + + private static boolean isValidVertex(int vertex, int n) { + return vertex >= 1 && vertex <= n; + } + static final class Edge { final int from; final int to; @@ -157,21 +181,35 @@ int nextInt() throws IOException { } } while (c <= ' '); - int sign = 1; - - if (c == '-') { - sign = -1; + boolean negative = c == '-'; + if (negative) { c = read(); } - int val = 0; - + int limit = negative ? Integer.MIN_VALUE : -Integer.MAX_VALUE; + int multiplyLimit = limit / 10; + int value = 0; + boolean hasDigit = false; while (c > ' ') { - val = val * 10 + c - '0'; + if (c < '0' || c > '9') { + throw new NumberFormatException("Invalid integer input"); + } + int digit = c - '0'; + if (value < multiplyLimit) { + throw new NumberFormatException("Integer input is out of range"); + } + value *= 10; + if (value < limit + digit) { + throw new NumberFormatException("Integer input is out of range"); + } + value -= digit; + hasDigit = true; c = read(); } - - return val * sign; + if (!hasDigit) { + throw new NumberFormatException("Expected integer"); + } + return negative ? value : -value; } } @@ -236,17 +274,28 @@ private static void run() throws Exception { int n = in.nextInt(); int m = in.nextInt(); - Edge[] edges = new Edge[m]; + long answer = -1; - for (int i = 0; i < m; i++) { - int from = in.nextInt(); - int to = in.nextInt(); - int weight = in.nextInt(); + if (n >= 1 && n <= MAX_VERTICES && m >= 0 && m <= MAX_EDGES) { + Edge[] edges = new Edge[m]; + boolean validEdges = true; - edges[i] = new Edge(from, to, weight); - } + for (int i = 0; i < m; i++) { + int from = in.nextInt(); + int to = in.nextInt(); + int weight = in.nextInt(); + + if (!isValidVertex(from, n) || !isValidVertex(to, n)) { + validEdges = false; + } - long answer = solve(n, edges); + edges[i] = new Edge(from, to, weight); + } + + if (validEdges) { + answer = solve(n, edges); + } + } if (answer == -1) { out.writeString(FAIL); diff --git a/src/main/java/algorithms/sprint6/WaterWorld.java b/src/main/java/algorithms/sprint6/WaterWorld.java index e4198d2..ff368c1 100644 --- a/src/main/java/algorithms/sprint6/WaterWorld.java +++ b/src/main/java/algorithms/sprint6/WaterWorld.java @@ -32,8 +32,25 @@ public class WaterWorld { + private static final int MAX_CELLS = 10_000_000; + + private static int checkedTotalCells(int n, int m) { + if (n <= 0 || m <= 0) { + throw new IllegalArgumentException("Dimensions must be positive"); + } + + long total = (long) n * m; + if (total > MAX_CELLS) { + throw new IllegalArgumentException("Map is too large"); + } + return (int) total; + } + static int[] solve(byte[] map, int n, int m) { - int total = n * m; + int total = checkedTotalCells(n, m); + if (map.length != total) { + throw new IllegalArgumentException("Map size does not match dimensions"); + } int[] queue = new int[total]; int islandCount = 0; @@ -130,10 +147,17 @@ int nextInt() throws IOException { int val = 0; while (c > ' ') { - val = val * 10 + c - '0'; + if (c < '0' || c > '9') { + throw new IOException("Invalid integer token"); + } + int digit = c - '0'; + if (val > (Integer.MAX_VALUE - digit) / 10) { + throw new IOException("Integer token is too large"); + } + val = val * 10 + digit; c = read(); } - return val * sign; + return sign * val; } byte[] nextBytes(int length) throws IOException { @@ -209,7 +233,8 @@ private static void run() throws Exception { int n = in.nextInt(); int m = in.nextInt(); - byte[] map = new byte[n * m]; + int total = checkedTotalCells(n, m); + byte[] map = new byte[total]; for (int row = 0; row < n; row++) { byte[] line = in.nextBytes(m); @@ -288,7 +313,11 @@ public static void main(String[] args) throws Exception { if (System.getProperty("os.name").startsWith("Windows")) { test(); } else { - run(); + try { + run(); + } catch (IllegalArgumentException | IOException ignored) { + // Invalid input is rejected before allocating arrays. + } } } } diff --git a/src/main/java/algorithms/sprint7/EqualSums.java b/src/main/java/algorithms/sprint7/EqualSums.java index 0ab867e..f6f82c3 100644 --- a/src/main/java/algorithms/sprint7/EqualSums.java +++ b/src/main/java/algorithms/sprint7/EqualSums.java @@ -31,18 +31,30 @@ Пространственная сложность — O(S / 64). */ public class EqualSums { + private static final int MAX_GAMES = 300; + private static final int MAX_TOTAL = MAX_GAMES * 300; static boolean solve(int[] points) { - int total = 0; + if (points.length > MAX_GAMES) { + throw new IllegalArgumentException("Too many games"); + } + + long total = 0; for (int point : points) { + if (point < 0) { + throw new IllegalArgumentException("Point value is out of range"); + } total += point; + if (total > MAX_TOTAL) { + throw new IllegalArgumentException("Total points are out of range"); + } } - if ((total & 1) == 1) { + if ((total & 1L) == 1L) { return false; } - int target = total / 2; + int target = (int) (total / 2); long[] reachable = new long[(target >> 6) + 1]; reachable[0] = 1L; @@ -168,10 +180,17 @@ private static void run() throws Exception { FastOut out = new FastOut(System.out); int n = in.nextInt(); + if (n < 0 || n > MAX_GAMES) { + throw new IOException("Number of games is out of range"); + } int[] points = new int[n]; for (int i = 0; i < n; i++) { - points[i] = in.nextInt(); + int point = in.nextInt(); + if (point < 0) { + throw new IOException("Point value is out of range"); + } + points[i] = point; } out.writeAscii(solve(points) ? "True" : "False"); diff --git a/src/main/java/algorithms/sprint7/LevenshteinDistance.java b/src/main/java/algorithms/sprint7/LevenshteinDistance.java index 482e1fb..aa3e61d 100644 --- a/src/main/java/algorithms/sprint7/LevenshteinDistance.java +++ b/src/main/java/algorithms/sprint7/LevenshteinDistance.java @@ -27,8 +27,12 @@ Пространственная сложность: O(min(n, m)), потому что хранится только две строки динамики. */ public class LevenshteinDistance { + private static final int MAX_LINE_LENGTH = 1000; static int solve(String first, String second) { + if (first.length() > MAX_LINE_LENGTH || second.length() > MAX_LINE_LENGTH) { + throw new IllegalArgumentException("Input line is too long"); + } String s = first; String t = second; @@ -94,8 +98,8 @@ private int read() throws IOException { return buf[ptr++]; } - String nextLine() throws IOException { - byte[] tmp = new byte[1024]; + String nextLine(int maxLength) throws IOException { + byte[] tmp = new byte[Math.min(1024, maxLength + 1)]; int size = 0; int c = read(); @@ -104,17 +108,25 @@ String nextLine() throws IOException { } while (c != -1 && c != '\n') { - if (c != '\r') { - if (size == tmp.length) { - byte[] grown = new byte[tmp.length * 2]; - System.arraycopy(tmp, 0, grown, 0, tmp.length); - tmp = grown; - } - tmp[size++] = (byte) c; + if (size == maxLength + 1) { + throw new IOException("Input line is too long"); + } + if (size == tmp.length) { + byte[] grown = new byte[Math.min(tmp.length * 2, maxLength + 1)]; + System.arraycopy(tmp, 0, grown, 0, tmp.length); + tmp = grown; } + tmp[size++] = (byte) c; c = read(); } + if (size > 0 && tmp[size - 1] == '\r') { + size--; + } + if (size > maxLength) { + throw new IOException("Input line is too long"); + } + return new String(tmp, 0, size, StandardCharsets.UTF_8); } } @@ -169,8 +181,8 @@ private static void run() throws Exception { FastIn in = new FastIn(System.in); FastOut out = new FastOut(System.out); - String s = in.nextLine(); - String t = in.nextLine(); + String s = in.nextLine(MAX_LINE_LENGTH); + String t = in.nextLine(MAX_LINE_LENGTH); out.writeInt(solve(s, t)); out.writeByte('\n'); diff --git a/src/main/java/algorithms/sprint8/Crib.java b/src/main/java/algorithms/sprint8/Crib.java index 18d15d2..c8f665b 100644 --- a/src/main/java/algorithms/sprint8/Crib.java +++ b/src/main/java/algorithms/sprint8/Crib.java @@ -35,12 +35,22 @@ public class Crib { static boolean solve(String text, String[] words) { - int totalLength = 0; + if (!isLowercaseWord(text)) { + return false; + } + + long totalLength = 0; for (String word : words) { + if (!isLowercaseWord(word)) { + return false; + } totalLength += word.length(); + if (totalLength >= Integer.MAX_VALUE) { + return false; + } } - Trie trie = new Trie(totalLength + 1); + Trie trie = new Trie((int) totalLength + 1); for (String word : words) { trie.add(word); } @@ -76,6 +86,17 @@ static boolean solve(String text, String[] words) { return dp[chars.length]; } + private static boolean isLowercaseWord(String word) { + for (int i = 0; i < word.length(); i++) { + char current = word.charAt(i); + if (current < 'a' || current > 'z') { + return false; + } + } + + return true; + } + static final class Trie { final int[][] next; final boolean[] terminal; @@ -207,6 +228,13 @@ private static void run() throws Exception { String text = in.next(); int n = in.nextInt(); + if (n < 0) { + out.writeString("NO"); + out.writeByte('\n'); + out.flush(); + return; + } + String[] words = new String[n]; for (int i = 0; i < n; i++) { diff --git a/src/main/java/algorithms/sprint8/PackedPrefix.java b/src/main/java/algorithms/sprint8/PackedPrefix.java index 66c2420..299750d 100644 --- a/src/main/java/algorithms/sprint8/PackedPrefix.java +++ b/src/main/java/algorithms/sprint8/PackedPrefix.java @@ -40,6 +40,10 @@ public class PackedPrefix { private static final int MAX_UNPACKED_LENGTH = 100_000; static String solve(String[] packedStrings) { + if (packedStrings.length == 0 || !isValidPacked(packedStrings[0])) { + return ""; + } + String first = decodePrefix(packedStrings[0], MAX_UNPACKED_LENGTH); int prefixLength = first.length(); @@ -47,6 +51,9 @@ static String solve(String[] packedStrings) { if (prefixLength == 0) { break; } + if (!isValidPacked(packedStrings[i])) { + return ""; + } prefixLength = commonPrefixWithPacked(packedStrings[i], first, prefixLength); } @@ -234,6 +241,34 @@ private static int[] buildMatchingBrackets(String packed) { return matchingBracket; } + private static boolean isValidPacked(String packed) { + int top = 0; + + for (int i = 0; i < packed.length(); i++) { + char current = packed.charAt(i); + + if (current >= '1' && current <= '9') { + if (i + 1 >= packed.length() || packed.charAt(i + 1) != '[') { + return false; + } + } else if (current == '[') { + if (i == 0 || packed.charAt(i - 1) < '1' || packed.charAt(i - 1) > '9') { + return false; + } + top++; + } else if (current == ']') { + if (top == 0 || packed.charAt(i - 1) == '[') { + return false; + } + top--; + } else if (current < 'a' || current > 'z') { + return false; + } + } + + return top == 0; + } + // -------------------- FAST INPUT -------------------- static final class FastIn { private final InputStream in; @@ -357,7 +392,20 @@ private static void run() throws Exception { FastOut out = new FastOut(System.out); int n = in.nextInt(); - String first = decodePrefix(in.next(), MAX_UNPACKED_LENGTH); + if (n <= 0) { + out.writeByte('\n'); + out.flush(); + return; + } + + String packedFirst = in.next(); + if (!isValidPacked(packedFirst)) { + out.writeByte('\n'); + out.flush(); + return; + } + + String first = decodePrefix(packedFirst, MAX_UNPACKED_LENGTH); int prefixLength = first.length(); for (int i = 1; i < n; i++) { @@ -366,6 +414,10 @@ private static void run() throws Exception { } String packed = in.next(); + if (!isValidPacked(packed)) { + prefixLength = 0; + break; + } prefixLength = commonPrefixWithPacked(packed, first, prefixLength); } diff --git a/src/main/java/common/SafeParse.java b/src/main/java/common/SafeParse.java index 8c4e14e..c5d9c50 100644 --- a/src/main/java/common/SafeParse.java +++ b/src/main/java/common/SafeParse.java @@ -24,4 +24,24 @@ public static double parseDouble(String value) { throw wrapped; } } + + public static int parseUnsignedInt(CharSequence value) { + if (value == null || value.length() == 0) { + throw new NumberFormatException("Expected unsigned integer"); + } + + int result = 0; + for (int index = 0; index < value.length(); index++) { + char current = value.charAt(index); + if (current < '0' || current > '9') { + throw new NumberFormatException("Invalid unsigned integer: " + value); + } + int digit = current - '0'; + if (result > (Integer.MAX_VALUE - digit) / 10) { + throw new NumberFormatException("Unsigned integer is out of range: " + value); + } + result = result * 10 + digit; + } + return result; + } } diff --git a/src/main/java/kyu4/RomanNumerals.java b/src/main/java/kyu4/RomanNumerals.java index 3dbf316..2dddf77 100644 --- a/src/main/java/kyu4/RomanNumerals.java +++ b/src/main/java/kyu4/RomanNumerals.java @@ -84,15 +84,22 @@ public static String toRoman(int n) { } public static int fromRoman(String romanNumeral) { + if (romanNumeral == null) { + throw new IllegalArgumentException("Roman numeral must not be null"); + } + // Traverse from right to left: add when symbol value is >= previous, // subtract otherwise. This naturally applies the subtractive rule. StringBuilder stringBuilderReverse = new StringBuilder(romanNumeral).reverse(); char[] chars = stringBuilderReverse.toString().toCharArray(); - int result = 0; + long result = 0; int previous = 0; for (char c : chars ) { Integer integer = MAP_FROM.get(c); + if (integer == null) { + throw new IllegalArgumentException("Unsupported Roman numeral character: " + c); + } if (integer >= previous) { result += integer; } else { @@ -101,7 +108,11 @@ public static int fromRoman(String romanNumeral) { previous = integer; } - return result; + int value = Math.toIntExact(result); + if (value < 1 || value > 3_999 || !toRoman(value).equals(romanNumeral)) { + throw new IllegalArgumentException("Roman numeral is not canonical: " + romanNumeral); + } + return value; } diff --git a/src/main/java/kyu5/ResistorColorCodes2.java b/src/main/java/kyu5/ResistorColorCodes2.java index 01522d8..8382a90 100644 --- a/src/main/java/kyu5/ResistorColorCodes2.java +++ b/src/main/java/kyu5/ResistorColorCodes2.java @@ -4,24 +4,41 @@ import static common.SafeParse.parseDouble; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + public class ResistorColorCodes2 { //5 + private static final Pattern OHMS_PATTERN = + Pattern.compile("([0-9]+(?:\\.[0-9]+)?)([kM]?) ohms"); + public static String encodeResistorColors(String ohmsString) { if (ohmsString == null || !ohmsString.endsWith(" ohms")) return ""; - int resistorOhms = encodeResistorOhms(ohmsString); - int[] resistorOhmsArr = encodeResistorOhmsToArr(resistorOhms); - return encodeResistorArrToColor(resistorOhmsArr); + try { + int resistorOhms = encodeResistorOhms(ohmsString); + int[] resistorOhmsArr = encodeResistorOhmsToArr(resistorOhms); + return encodeResistorArrToColor(resistorOhmsArr); + } catch (IllegalArgumentException e) { + return ""; + } } private static int encodeResistorOhms(String ohmsString) { - double tempValue; - String[] tempArray = ohmsString.split("\\s"); - if (tempArray[0].endsWith("k")) - tempValue = parseDouble(tempArray[0].trim().substring(0, tempArray[0].length() - 1)) * 1_000; - else if (tempArray[0].endsWith("M")) - tempValue = parseDouble(tempArray[0].trim().substring(0, tempArray[0].length() - 1)) * 1_000_000; - else tempValue = parseDouble(tempArray[0].trim()); + Matcher matcher = OHMS_PATTERN.matcher(ohmsString); + if (!matcher.matches()) { + throw new IllegalArgumentException("Invalid resistance format"); + } + + double tempValue = parseDouble(matcher.group(1)); + if (matcher.group(2).equals("k")) { + tempValue *= 1_000; + } else if (matcher.group(2).equals("M")) { + tempValue *= 1_000_000; + } + if (!Double.isFinite(tempValue) || tempValue < 10 || tempValue > 990_000_000) { + throw new IllegalArgumentException("Resistance must be between 10 and 990M ohms"); + } return (int) Math.round(tempValue); } diff --git a/src/main/java/kyu6/DigitalRoot.java b/src/main/java/kyu6/DigitalRoot.java index f5af29e..9dbc467 100644 --- a/src/main/java/kyu6/DigitalRoot.java +++ b/src/main/java/kyu6/DigitalRoot.java @@ -28,6 +28,16 @@ public static int digitalRoot(int n) { return result; } + /** + * Retains compatibility with the original Codewars API. + * + * @param n a non-negative integer + * @return the digital root of {@code n} + */ + public static int digital_root(int n) { + return digitalRoot(n); + } + /** * Digital root is the recursive sum of all the digits in a number. * Given n, take the sum of the digits of n. If that value has more than one diff --git a/src/main/java/kyu7/DescendingOrder.java b/src/main/java/kyu7/DescendingOrder.java index f896398..5b13c5e 100644 --- a/src/main/java/kyu7/DescendingOrder.java +++ b/src/main/java/kyu7/DescendingOrder.java @@ -1,6 +1,8 @@ package kyu7; +import static common.SafeParse.parseUnsignedInt; + import java.util.ArrayList; import java.util.Comparator; @@ -18,12 +20,12 @@ public static int sortDesc(int num) { arrayList.add(i); } arrayList.sort(Comparator.reverseOrder()); - int result = 0; + StringBuilder result = new StringBuilder(); for (int i : arrayList ) { - result = result * 10 + i; + result.append(i); } - return result; + return parseUnsignedInt(result); } } diff --git a/src/main/java/kyu7/MinimumLine.java b/src/main/java/kyu7/MinimumLine.java index 8deee48..4cc5615 100644 --- a/src/main/java/kyu7/MinimumLine.java +++ b/src/main/java/kyu7/MinimumLine.java @@ -1,7 +1,10 @@ package kyu7; +import static common.SafeParse.parseUnsignedInt; + import java.util.Arrays; +import java.util.stream.Collectors; public class MinimumLine { @@ -9,10 +12,12 @@ public class MinimumLine { // 7 https://www.codewars.com/kata/5ac6932b2f317b96980000ca/train/java public static int minValue(int[] values) { - return Arrays.stream(values) + String result = Arrays.stream(values) .sorted() .distinct() - .reduce(0, (result, digit) -> result * 10 + digit); + .mapToObj(String::valueOf) + .collect(Collectors.joining()); + return parseUnsignedInt(result); } } diff --git a/src/main/java/kyu7/SquareDigit.java b/src/main/java/kyu7/SquareDigit.java index 5aa3597..a0ac22c 100644 --- a/src/main/java/kyu7/SquareDigit.java +++ b/src/main/java/kyu7/SquareDigit.java @@ -1,11 +1,12 @@ package kyu7; -import static common.SafeParse.parseInt; - +import static common.SafeParse.parseUnsignedInt; public class SquareDigit { + private static final String MAX_INT = Integer.toString(Integer.MAX_VALUE); + //7 https://www.codewars.com/kata/546e2562b03326a88e000020/train/java public static int squareDigits(int n) { @@ -16,7 +17,12 @@ public static int squareDigits(int n) { n /= 10; stringBuilder.insert(0, i * i); } - return parseInt(stringBuilder.toString()); + String result = stringBuilder.toString(); + if (result.length() > MAX_INT.length() + || result.length() == MAX_INT.length() && result.compareTo(MAX_INT) > 0) { + return Integer.MAX_VALUE; + } + return parseUnsignedInt(result); } } diff --git a/src/main/java/other/AdjustCase.java b/src/main/java/other/AdjustCase.java index aca4865..4fb292a 100644 --- a/src/main/java/other/AdjustCase.java +++ b/src/main/java/other/AdjustCase.java @@ -2,7 +2,6 @@ import java.util.Locale; -import java.util.stream.Collectors; public class AdjustCase { @@ -11,41 +10,26 @@ public class AdjustCase { public static final String EMPTY = ""; public String adjustCaseToLower(String string) { - if (string == null || string.length() == 0) return string; - if (string.length() == 1) return string.toUpperCase(Locale.ROOT); - var stringLover = string.toLowerCase(Locale.ROOT).substring(1); - var firstChar = string.substring(0, 1).toUpperCase(Locale.ROOT); - return firstChar + stringLover; + return capitalizeFirst(string); } public String adjustCaseStream(String string) { - if (string == null || string.length() == 0) return string; - var lower = string.chars() - .mapToObj(i -> (char) i) - .map(Object::toString) - .map(value -> value.toLowerCase(Locale.ROOT)) - .collect(Collectors.joining()) - .substring(1); - return string.chars() - .mapToObj(i -> (char) i) - .map(Object::toString) - .findFirst() - .orElse("") - .toUpperCase(Locale.ROOT) + lower; + return capitalizeFirst(string); } public String adjustCaseFor(String string) { - if (string == null) return null; - char[] chars = string.toCharArray(); - for (int i = 0; i < chars.length; i++) { - if(i == 0) { - chars[i] = Character.toUpperCase(chars[i]); - } - else { - chars[i] = Character.toLowerCase(chars[i]); - } + return capitalizeFirst(string); + } + + private String capitalizeFirst(String string) { + if (string == null || string.isEmpty()) { + return string; } - return String.valueOf(chars); + int firstEnd = string.offsetByCodePoints(0, 1); + String first = string.substring(0, firstEnd); + String lower = string.toLowerCase(Locale.ROOT); + int loweredFirstLength = first.toLowerCase(Locale.ROOT).length(); + return first.toUpperCase(Locale.ROOT) + lower.substring(loweredFirstLength); } diff --git a/src/main/java/other/ReverseInt.java b/src/main/java/other/ReverseInt.java index 878428b..e380fe5 100644 --- a/src/main/java/other/ReverseInt.java +++ b/src/main/java/other/ReverseInt.java @@ -1,17 +1,17 @@ package other; -import static common.SafeParse.parseInt; - - public class ReverseInt { public static int reverse(int x) { - if (x >= 0) { - return parseInt(new StringBuilder(String.valueOf(x)).reverse().toString()); - } else { - return parseInt(new StringBuilder(String.valueOf(x).substring(1)).reverse().toString()) * -1; + long reversed = 0; + int remaining = x; + while (remaining != 0) { + reversed = reversed * 10 + remaining % 10; + remaining /= 10; } + + return reversed < Integer.MIN_VALUE || reversed > Integer.MAX_VALUE ? 0 : (int) reversed; } } diff --git a/src/test/java/algorithms/AlgorithmCliTest.java b/src/test/java/algorithms/AlgorithmCliTest.java index fbe72a5..3a66947 100644 --- a/src/test/java/algorithms/AlgorithmCliTest.java +++ b/src/test/java/algorithms/AlgorithmCliTest.java @@ -128,6 +128,12 @@ private static Stream stdioCases() { arguments("algorithms.sprint6.DorogayaSet", "run", "4 4%n1 2 5%n1 3 6%n2 4 8%n3 4 3%n".formatted(), "19%n".formatted()), + arguments("algorithms.sprint6.DorogayaSet", "run", + "5 -1%n".formatted(), + "Oops! I did it again%n".formatted()), + arguments("algorithms.sprint6.DorogayaSet", "run", + "2 1%n3 1 42%n".formatted(), + "Oops! I did it again%n".formatted()), arguments("algorithms.sprint6.WaterWorld", "run", "3 3%n#.#%n.#.%n#.#%n".formatted(), "5 1%n".formatted()), diff --git a/src/test/java/algorithms/sprint0/ZipTest.java b/src/test/java/algorithms/sprint0/ZipTest.java index c97536a..dfe165c 100644 --- a/src/test/java/algorithms/sprint0/ZipTest.java +++ b/src/test/java/algorithms/sprint0/ZipTest.java @@ -4,6 +4,11 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; import java.util.Arrays; import java.util.List; import org.junit.jupiter.api.Tag; @@ -48,4 +53,43 @@ void rejectsNullLists() { assertThrows(NullPointerException.class, () -> Zip.zip(null, List.of(2), 1)); assertThrows(NullPointerException.class, () -> Zip.zip(List.of(1), null, 1)); } + + @Test + void processAcceptsValidInput() throws IOException { + StringWriter output = new StringWriter(); + BufferedWriter writer = new BufferedWriter(output); + + Zip.process(new BufferedReader(new StringReader("3\n1 5 6\n7 8 9\n")), writer); + writer.flush(); + + assertEquals("1 7 5 8 6 9 ", output.toString()); + } + + @Test + void processAcceptsCrOnlyLineEndings() throws IOException { + StringWriter output = new StringWriter(); + BufferedWriter writer = new BufferedWriter(output); + + Zip.process(new BufferedReader(new StringReader("3\r1 3 5\r2 4 6\r")), writer); + writer.flush(); + + assertEquals("1 2 3 4 5 6 ", output.toString()); + } + + @Test + void processRejectsMissingOrShortLists() { + assertThrows(IOException.class, () -> process("3\n1 2 3\n")); + assertThrows(IllegalArgumentException.class, () -> process("3\n1 2\n4 5 6\n")); + } + + @Test + void processRejectsUnboundedSizesAndLines() { + assertThrows(IllegalArgumentException.class, () -> process("100001\n1\n2\n")); + String oversizedLine = "1".repeat(1_200_002); + assertThrows(IllegalArgumentException.class, () -> process("1\n" + oversizedLine + "\n2\n")); + } + + private static void process(String input) throws IOException { + Zip.process(new BufferedReader(new StringReader(input)), new BufferedWriter(new StringWriter())); + } } diff --git a/src/test/java/algorithms/sprint2/DequeTest.java b/src/test/java/algorithms/sprint2/DequeTest.java new file mode 100644 index 0000000..4e3e66f --- /dev/null +++ b/src/test/java/algorithms/sprint2/DequeTest.java @@ -0,0 +1,26 @@ +package algorithms.sprint2; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +class DequeTest { + + @Test + void rejectsCapacityInsteadOfSilentlyClampingIt() { + assertThrows(IllegalArgumentException.class, () -> new Deque.RingDeque(100_001)); + } + + @Test + void preservesSupportedCapacity() { + Deque.RingDeque deque = new Deque.RingDeque(2); + deque.pushBack(1); + deque.pushBack(2); + + assertEquals(2, deque.popBack()); + assertEquals(1, deque.popBack()); + } +} diff --git a/src/test/java/algorithms/sprint5/PyramidSortTest.java b/src/test/java/algorithms/sprint5/PyramidSortTest.java new file mode 100644 index 0000000..30c3ca8 --- /dev/null +++ b/src/test/java/algorithms/sprint5/PyramidSortTest.java @@ -0,0 +1,76 @@ +package algorithms.sprint5; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +class PyramidSortTest { + + @Test + void fastInRejectsOverflowingInteger() { + PyramidSort.FastIn in = fastIn("2147483648 "); + + assertThrows(NumberFormatException.class, in::nextInt); + } + + @Test + void fastInAcceptsMinimumInteger() throws Exception { + assertEquals(Integer.MIN_VALUE, fastIn("-2147483648 ").nextInt()); + } + + @Test + void fastInRejectsValuesBelowMinimumInteger() { + assertThrows(NumberFormatException.class, () -> fastIn("-2147483649 ").nextInt()); + } + + @Test + void fastInRejectsOversizedLoginToken() { + PyramidSort.FastIn in = fastIn("a".repeat(1_025) + " "); + + assertThrows(java.io.IOException.class, in::next); + } + + @Test + void runRejectsNegativeParticipantCountBeforeAllocation() throws Exception { + InputStream stdin = System.in; + System.setIn(new ByteArrayInputStream("-1\n".getBytes(StandardCharsets.UTF_8))); + try { + Method run = PyramidSort.class.getDeclaredMethod("run"); + run.setAccessible(true); + + InvocationTargetException ex = assertThrows(InvocationTargetException.class, () -> run.invoke(null)); + + assertEquals(IllegalArgumentException.class, ex.getCause().getClass()); + } finally { + System.setIn(stdin); + } + } + + @Test + void solvePreservesParticipantOrdering() { + PyramidSort.Participant[] participants = new PyramidSort.Participant[] { + new PyramidSort.Participant("alla", 4, 100), + new PyramidSort.Participant("gena", 6, 1000), + new PyramidSort.Participant("timofey", 4, 80) + }; + + PyramidSort.solve(participants); + + assertEquals("gena", participants[0].login); + assertEquals("timofey", participants[1].login); + assertEquals("alla", participants[2].login); + } + + private static PyramidSort.FastIn fastIn(String input) { + return new PyramidSort.FastIn(new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8))); + } +} diff --git a/src/test/java/algorithms/sprint6/DorogayaSetTest.java b/src/test/java/algorithms/sprint6/DorogayaSetTest.java new file mode 100644 index 0000000..6df401c --- /dev/null +++ b/src/test/java/algorithms/sprint6/DorogayaSetTest.java @@ -0,0 +1,31 @@ +package algorithms.sprint6; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +class DorogayaSetTest { + + @Test + void fastInRejectsOverflowingIntegerTokens() { + assertThrows(NumberFormatException.class, () -> fastIn("4294967296 ").nextInt()); + } + + @Test + void fastInAcceptsIntegerBounds() throws Exception { + DorogayaSet.FastIn input = fastIn("-2147483648 2147483647 "); + + assertEquals(Integer.MIN_VALUE, input.nextInt()); + assertEquals(Integer.MAX_VALUE, input.nextInt()); + } + + private static DorogayaSet.FastIn fastIn(String value) { + return new DorogayaSet.FastIn( + new ByteArrayInputStream(value.getBytes(StandardCharsets.UTF_8))); + } +} diff --git a/src/test/java/algorithms/sprint7/LevenshteinDistanceTest.java b/src/test/java/algorithms/sprint7/LevenshteinDistanceTest.java new file mode 100644 index 0000000..57d0916 --- /dev/null +++ b/src/test/java/algorithms/sprint7/LevenshteinDistanceTest.java @@ -0,0 +1,33 @@ +package algorithms.sprint7; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +class LevenshteinDistanceTest { + + @Test + void stripsOnlyTheOptionalCrLfTerminator() throws IOException { + LevenshteinDistance.FastIn input = fastIn("abc\r\n"); + + assertEquals("abc", input.nextLine(3)); + } + + @Test + void countsEmbeddedCarriageReturnsTowardTheLimit() { + LevenshteinDistance.FastIn input = fastIn("\r\r\r\n"); + + assertThrows(IOException.class, () -> input.nextLine(1)); + } + + private static LevenshteinDistance.FastIn fastIn(String value) { + return new LevenshteinDistance.FastIn( + new ByteArrayInputStream(value.getBytes(StandardCharsets.UTF_8))); + } +} diff --git a/src/test/java/algorithms/sprint8/PackedPrefixTest.java b/src/test/java/algorithms/sprint8/PackedPrefixTest.java new file mode 100644 index 0000000..8fa6eed --- /dev/null +++ b/src/test/java/algorithms/sprint8/PackedPrefixTest.java @@ -0,0 +1,20 @@ +package algorithms.sprint8; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +class PackedPrefixTest { + + @Test + void rejectsEmptyRepeatBodies() { + assertEquals("", PackedPrefix.solve(new String[]{"9[9[9[]]]"})); + } + + @Test + void rejectsZeroRepeatCounts() { + assertEquals("", PackedPrefix.solve(new String[]{"0[a]"})); + } +} diff --git a/src/test/java/kyu4/RomanNumeralsTest.java b/src/test/java/kyu4/RomanNumeralsTest.java index b2f2904..4b9d67e 100644 --- a/src/test/java/kyu4/RomanNumeralsTest.java +++ b/src/test/java/kyu4/RomanNumeralsTest.java @@ -1,9 +1,11 @@ package kyu4; 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; +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; @@ -30,6 +32,19 @@ void shouldRoundTripCanonicalRomanNumerals(int value) { assertEquals(value, RomanNumerals.fromRoman(RomanNumerals.toRoman(value))); } + @ParameterizedTest + @MethodSource("invalidRomanNumerals") + void shouldRejectInvalidRomanNumerals(String romanNumeral) { + assertThrows(IllegalArgumentException.class, () -> RomanNumerals.fromRoman(romanNumeral)); + } + + @Test + void shouldRejectResultsOutsideIntegerRange() { + String romanNumeral = "M".repeat(2_147_484); + + assertThrows(ArithmeticException.class, () -> RomanNumerals.fromRoman(romanNumeral)); + } + private static Stream romanCases() { return Stream.of( Arguments.of(1, "I"), @@ -45,4 +60,8 @@ private static Stream romanCases() { Arguments.of(3_999, "MMMCMXCIX") ); } + + private static Stream invalidRomanNumerals() { + return Stream.of((String) null, "", "A", "MCMX?", "IIII", "VX", "MMMM"); + } } diff --git a/src/test/java/kyu5/ResistorColorCodes2Test.java b/src/test/java/kyu5/ResistorColorCodes2Test.java index 26086c4..ef5bd5b 100644 --- a/src/test/java/kyu5/ResistorColorCodes2Test.java +++ b/src/test/java/kyu5/ResistorColorCodes2Test.java @@ -23,6 +23,10 @@ void shouldRejectMalformedInput() { assertEquals("", ResistorColorCodes2.encodeResistorColors(null)); assertEquals("", ResistorColorCodes2.encodeResistorColors("47")); assertEquals("", ResistorColorCodes2.encodeResistorColors("47 ohm")); + assertEquals("", ResistorColorCodes2.encodeResistorColors("abc ohms")); + assertEquals("", ResistorColorCodes2.encodeResistorColors("-1 ohms")); + assertEquals("", ResistorColorCodes2.encodeResistorColors("1e309 ohms")); + assertEquals("", ResistorColorCodes2.encodeResistorColors("47 garbage ohms")); } private static Stream resistorCases() { diff --git a/src/test/java/kyu6/DigitalRootTest.java b/src/test/java/kyu6/DigitalRootTest.java index 5d09716..f6a2668 100644 --- a/src/test/java/kyu6/DigitalRootTest.java +++ b/src/test/java/kyu6/DigitalRootTest.java @@ -21,5 +21,6 @@ class DigitalRootTest { void shouldReduceNumberToSingleDigit(int value, int expected) { assertEquals(expected, DigitalRoot.digitalRoot(value)); assertEquals(expected, DigitalRoot.digitalRootRecursiveStream(value)); + assertEquals(expected, DigitalRoot.digital_root(value)); } } diff --git a/src/test/java/kyu7/DescendingOrderTest.java b/src/test/java/kyu7/DescendingOrderTest.java index 3c94032..5afddd7 100644 --- a/src/test/java/kyu7/DescendingOrderTest.java +++ b/src/test/java/kyu7/DescendingOrderTest.java @@ -17,6 +17,11 @@ import static kyu7.DescendingOrder.*; @Tag("smoke") public class DescendingOrderTest { + @Test + void rejectsReorderedValuesOutsideIntegerRange() { + assertThrows(NumberFormatException.class, () -> sortDesc(2_147_483_647)); + } + @Test void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(kyu7.DescendingOrder.class); diff --git a/src/test/java/kyu7/MinimumLineTest.java b/src/test/java/kyu7/MinimumLineTest.java index 7572493..c2f0007 100644 --- a/src/test/java/kyu7/MinimumLineTest.java +++ b/src/test/java/kyu7/MinimumLineTest.java @@ -17,6 +17,13 @@ import static kyu7.MinimumLine.*; @Tag("smoke") public class MinimumLineTest { + @Test + void rejectsConcatenatedValuesOutsideIntegerRange() { + int[] values = {9, 8, 7, 6, 5, 4, 3, 2, 1, 99}; + + assertThrows(NumberFormatException.class, () -> minValue(values)); + } + @Test void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(kyu7.MinimumLine.class); diff --git a/src/test/java/kyu7/SquareDigitTest.java b/src/test/java/kyu7/SquareDigitTest.java index e3f663b..26d0e68 100644 --- a/src/test/java/kyu7/SquareDigitTest.java +++ b/src/test/java/kyu7/SquareDigitTest.java @@ -17,6 +17,16 @@ import static kyu7.SquareDigit.*; @Tag("smoke") public class SquareDigitTest { + @Test + void squaresDigitsWhenResultFitsInAnInteger() { + assertEquals(811181, squareDigits(9119)); + } + + @Test + void capsResultsThatExceedIntegerRange() { + assertEquals(Integer.MAX_VALUE, squareDigits(99999)); + } + @Test void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(kyu7.SquareDigit.class); diff --git a/src/test/java/other/AdjustCaseTest.java b/src/test/java/other/AdjustCaseTest.java index abb8658..8f15c84 100644 --- a/src/test/java/other/AdjustCaseTest.java +++ b/src/test/java/other/AdjustCaseTest.java @@ -17,6 +17,22 @@ import static other.AdjustCase.*; @Tag("smoke") public class AdjustCaseTest { + @Test + void adjustsSuffixWithoutRetainingExpandedLowercaseCharacters() { + other.AdjustCase adjustCase = new other.AdjustCase(); + + assertEquals("\u0130bc", adjustCase.adjustCaseToLower("\u0130BC")); + } + + @Test + void preservesContextSensitiveFinalSigmaAcrossImplementations() { + other.AdjustCase adjustCase = new other.AdjustCase(); + + assertEquals("Aς", adjustCase.adjustCaseToLower("AΣ")); + assertEquals("Aς", adjustCase.adjustCaseStream("AΣ")); + assertEquals("Aς", adjustCase.adjustCaseFor("AΣ")); + } + @Test void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(other.AdjustCase.class); diff --git a/src/test/java/other/ReverseIntTest.java b/src/test/java/other/ReverseIntTest.java index 2671789..627fb7e 100644 --- a/src/test/java/other/ReverseIntTest.java +++ b/src/test/java/other/ReverseIntTest.java @@ -16,7 +16,9 @@ class ReverseIntTest { "-123, -321", "120, 21", "-120, -21", - "1000000001, 1000000001" + "1000000001, 1000000001", + "1534236469, 0", + "-2147483648, 0" }) void shouldReverseIntegerDigits(int input, int expected) { assertEquals(expected, ReverseInt.reverse(input));