From a52382a94b1bd38fb01dad1de1d7e52d6a413b6e Mon Sep 17 00:00:00 2001 From: "Andrei.Ovcharenko" Date: Sat, 11 Jul 2026 10:29:28 +0300 Subject: [PATCH 1/3] fix: harden kata boundaries and validation --- .github/workflows/maven.yml | 20 +++++ ARCHITECTURE.md | 3 +- CHANGELOG.md | 11 +++ README.en.md | 10 +++ README.md | 10 +++ TESTING.md | 20 ++++- scripts/run-offline-gate.ps1 | 75 ++++++++++++++++ .../java/interview/CallbackScheduler.java | 15 +++- src/main/java/kyu4/MorseCodeDecoder.java | 5 +- src/main/java/kyu5/HumanReadableTime.java | 4 +- src/main/java/kyu5/MaximumSubarraySum.java | 29 ++----- .../java/kyu5/NumberOfTrailingZerosOfN.java | 29 +++---- src/main/java/kyu5/ProductFib.java | 38 ++++++-- src/main/java/kyu5/Scrambles.java | 29 +++---- src/main/java/kyu5/SumSquaredDivisors.java | 72 ++++++++-------- src/main/java/kyu6/ArmstrongNumber.java | 12 ++- src/main/java/kyu6/CountingDuplicates.java | 13 ++- src/main/java/kyu6/DigPow.java | 34 ++++++-- src/main/java/kyu6/FindEvenIndex.java | 53 ++++-------- src/main/java/kyu6/PhoneNumber.java | 12 ++- src/main/java/kyu6/SimpleEncryption.java | 82 ++++++++++++------ src/main/java/kyu6/SortTheOdd.java | 45 ++++------ src/main/java/kyu6/TheSupermarketQueue.java | 61 ++++--------- src/main/java/kyu6/TribonacciSequence.java | 27 ++---- src/main/java/kyu6/TwoSum.java | 2 +- src/main/java/kyu6/VINChecker.java | 12 ++- src/main/java/kyu6/toCamelCase.java | 15 +++- src/main/java/kyu7/Accumul.java | 15 +--- src/main/java/kyu7/CountTheDigit.java | 9 +- src/main/java/kyu7/DontGiveMeFive.java | 42 ++++++++- src/main/java/kyu7/FindDivisor.java | 16 ++-- src/main/java/kyu7/GetSum.java | 6 +- src/main/java/kyu7/GetXO.java | 11 ++- src/main/java/kyu7/IsAnagram.java | 5 +- src/main/java/kyu7/Isogram.java | 3 +- src/main/java/kyu7/JadenCase.java | 21 +++-- src/main/java/kyu7/MaxMultiple.java | 12 +-- src/main/java/kyu7/Money.java | 15 +++- src/main/java/kyu7/NbYear.java | 15 +++- src/main/java/kyu7/NumberFun.java | 17 +++- src/main/java/kyu7/ReverseWords.java | 26 ++++-- .../java/kyu7/RoundToTheNextMultipleOf5.java | 8 +- src/main/java/kyu7/SumOddNumbers.java | 10 +-- src/main/java/kyu7/TriangleTester.java | 5 +- .../leetcode/BestTimeToBuyAndSellStock.java | 22 ++--- src/main/java/leetcode/MergeIntervals.java | 2 +- src/main/java/other/AdjustCase.java | 6 +- .../java/other/AllMostPopularityKyu8.java | 65 ++++++++------ src/main/java/other/HowManyNumbers.java | 62 ++++++------- src/main/java/other/Permutations.java | 13 +++ src/main/java/other/Persist.java | 5 +- src/main/java/other/SpinWords.java | 31 ++++--- .../InMemoryValidationService.java | 86 +++++++++++-------- .../java/transactions/ValidationResult.java | 2 +- .../java/interview/CallbackSchedulerTest.java | 9 ++ src/test/java/kyu5/HumanReadableTimeTest.java | 8 ++ .../java/kyu5/MaximumSubarraySumTest.java | 8 ++ .../kyu5/NumberOfTrailingZerosOfNTest.java | 13 +++ src/test/java/kyu5/ProductFibTest.java | 16 ++++ src/test/java/kyu5/ScramblesTest.java | 8 ++ .../java/kyu5/SumSquaredDivisorsTest.java | 10 +++ src/test/java/kyu6/ArmstrongNumberTest.java | 2 +- .../java/kyu6/CountingDuplicatesTest.java | 7 ++ src/test/java/kyu6/DigPowTest.java | 11 +++ src/test/java/kyu6/FindEvenIndexTest.java | 15 ++++ src/test/java/kyu6/PhoneNumberTest.java | 9 ++ src/test/java/kyu6/SimpleEncryptionTest.java | 21 +++++ src/test/java/kyu6/SortTheOddTest.java | 15 ++++ .../java/kyu6/TheSupermarketQueueTest.java | 13 +++ .../java/kyu6/TribonacciSequenceTest.java | 8 ++ src/test/java/kyu6/TwoSumTest.java | 4 + src/test/java/kyu6/toCamelCaseTest.java | 9 +- src/test/java/kyu7/AccumulTest.java | 8 ++ src/test/java/kyu7/CountTheDigitTest.java | 7 ++ src/test/java/kyu7/DontGiveMeFiveTest.java | 20 +++++ src/test/java/kyu7/FindDivisorTest.java | 7 ++ src/test/java/kyu7/GetSumTest.java | 7 ++ src/test/java/kyu7/JadenCaseTest.java | 8 ++ src/test/java/kyu7/MaxMultipleTest.java | 7 ++ src/test/java/kyu7/MoneyTest.java | 9 ++ src/test/java/kyu7/NbYearTest.java | 7 ++ src/test/java/kyu7/NumberFunTest.java | 8 ++ src/test/java/kyu7/ReverseWordsTest.java | 11 +++ .../kyu7/RoundToTheNextMultipleOf5Test.java | 8 ++ .../java/kyu7/SumOddNumbersPropertyTest.java | 9 ++ src/test/java/kyu7/TriangleTesterTest.java | 6 ++ .../BestTimeToBuyAndSellStockTest.java | 8 ++ .../java/leetcode/MergeIntervalsTest.java | 2 + .../java/other/AllMostPopularityKyu8Test.java | 23 +++++ src/test/java/other/HowManyNumbersTest.java | 12 +++ src/test/java/other/PermutationsTest.java | 12 +++ src/test/java/other/PersistTest.java | 7 ++ src/test/java/other/SpinWordsTest.java | 9 ++ .../java/quality/SmokeMethodTestHarness.java | 4 +- .../quality/SmokeMethodTestHarnessTest.java | 35 ++++++++ .../InMemoryValidationServiceTest.java | 57 ++++++++++++ 96 files changed, 1248 insertions(+), 497 deletions(-) create mode 100644 scripts/run-offline-gate.ps1 create mode 100644 src/test/java/quality/SmokeMethodTestHarnessTest.java diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 5fe65d2..28b8499 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -10,6 +10,26 @@ permissions: contents: read jobs: + offline-gate: + name: Offline gate (Windows, JDK 21) + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: Set up JDK + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 + with: + distribution: temurin + java-version: 21 + cache: maven + - name: Prime the exact Maven dependency set + run: mvn -B -ntp -DskipTests test --file pom.xml + - name: Run the strict offline test gate + shell: pwsh + run: .\scripts\run-offline-gate.ps1 -Goal test -StrictDependencies + category-tests: name: Test (${{ matrix.category }}) runs-on: ubuntu-latest diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index eccb0ad..c0375e1 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -9,6 +9,7 @@ - `IntegrationSuite` – stateful transaction validation behavior. - `PropertySuite` – jqwik property/invariant tests. - `.github` contains CI, templates, and dependency automation. +- `scripts/run-offline-gate.ps1` provides a network-free local Maven quality gate. ## Design principles used in this repo @@ -33,5 +34,5 @@ ## Future quality directions - Increase property coverage into additional stable, pure utilities where invariant checks are natural. -- Add explicit domain-level integration tests for edge-case transaction flows (duplicates / ordering / insufficient balance). +- Expand domain-level integration coverage when new transaction rules are introduced. - Keep production methods with concise intent comments for non-obvious logic. diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cecc62..7cc34ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## Unreleased + +- Corrected boundary, overflow, validation, determinism and complexity defects across kata utilities. +- Hardened transaction validation and made returned status collections immutable. +- Replaced unbounded iteration in encryption and range-counting helpers with logarithmic algorithms. +- Made the generated smoke harness fail when every executable API path fails. +- Added regression coverage for scheduler extremes, queueing, Fibonacci, trailing zeros and other edge cases. +- Added a PowerShell offline quality gate with strict dependency-cache mode and a documented cached-JUnit fallback. +- Added a Windows CI job that primes dependencies and then exercises the strict offline gate. +- Expanded the verified test baseline to 594 tests. + ## 1.1 - public quality hardening (2026-06-09) - Added CI, static analysis, and dependency management updates for repository quality. diff --git a/README.en.md b/README.en.md index b2450b5..bedd99c 100644 --- a/README.en.md +++ b/README.en.md @@ -34,6 +34,7 @@ This repository is a portfolio-grade Java kata workspace focused on deterministi ## CI and Quality Signals - `.github/workflows/maven.yml` runs category jobs on JDK 21 and full `mvn verify` on JDK 17 and 21. +- The same workflow validates the strict offline PowerShell gate on Windows after an explicit dependency-prime step. - `.github/workflows/quality.yml` runs Checkstyle, PMD, and SpotBugs without executing tests. - `.github/workflows/codeql-analysis.yml` runs security analysis. - `.github/dependabot.yml` keeps dependency automation visible. @@ -58,6 +59,15 @@ mvn -B test -Dtest='quality.IntegrationSuite' mvn -B test -Dtest='quality.PropertySuite' ``` +Run the complete PowerShell gate without network access: + +```powershell +.\scripts\run-offline-gate.ps1 +``` + +See [`TESTING.md`](TESTING.md) for strict dependency-cache mode and the documented +cached-JUnit fallback. + ## Reviewer Checklist - No hidden mutation of test inputs inside algorithmic methods. diff --git a/README.md b/README.md index ac049da..0fe2172 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ - **Coverage-gate**: JaCoCo ломает `mvn verify`, если покрытие ниже 70% строк, ветвлений или инструкций. - **Отдельный quality-гейт**: workflow `.github/workflows/quality.yml` для чистых статических проверок без прогона тестов. +- **Офлайн-гейт**: Windows CI после явного заполнения кэша запускает PowerShell-проверку в строгом режиме без сети. - **Checkstyle / PMD / SpotBugs**: статические проверки заведены в quality-гейт. - **Checkstyle**: проектный набор правил в `config/checkstyle/checkstyle.xml` и явные suppressions для generated smoke-тестов. - **SpotBugs**: чистый отчёт с явными исключениями совместимости в `config/spotbugs-exclude.xml`. @@ -63,6 +64,15 @@ mvn -B test -Dtest='quality.IntegrationSuite' mvn -B test -Dtest='quality.PropertySuite' ``` +Полный локальный прогон без сети на PowerShell: + +```powershell +.\scripts\run-offline-gate.ps1 +``` + +Подробности о строгом режиме зависимостей и fallback на уже закэшированную версию JUnit +описаны в [`TESTING.md`](TESTING.md). + ### Что отражает репозиторий как "публично привлекательный" - Есть CI с понятными этапами и артефактами (`surefire`, `jacoco`). diff --git a/TESTING.md b/TESTING.md index 26197a8..290a779 100644 --- a/TESTING.md +++ b/TESTING.md @@ -64,20 +64,36 @@ mvn -B test -Dtest='quality.PropertySuite' - line coverage: 70% - branch coverage: 70% - instruction coverage: 70% -- Current verified local baseline: 455 tests, 91.4% line coverage, - 85.1% branch coverage and 92.7% instruction coverage. +- Current verified local baseline: 594 tests. Coverage is enforced by the ratios above; + the generated HTML/XML report under `target/site/jacoco` is the source of exact values. - Run: ```bash mvn -B verify ``` +### Offline gate (PowerShell) + +The repository also provides a network-free local gate: + +```powershell +.\scripts\run-offline-gate.ps1 +.\scripts\run-offline-gate.ps1 -Goal test +.\scripts\run-offline-gate.ps1 -StrictDependencies +``` + +The script always passes Maven `--offline`. By default, when the exact JUnit BOM from +`pom.xml` is not present locally, it warns and uses the newest cached release from the +same major line for validation. `-StrictDependencies` disables that fallback and fails +unless the exact dependency set is already cached. + - CI uploads `target/surefire-reports` and `target/site/jacoco`. ## 6) CI matrix - Category workflow: smoke/integration/property tags (JDK 21) - Full verify workflow: JDK 17 and 21 +- Strict offline PowerShell gate: Windows, JDK 21 (after an explicit cache-prime step) - Static workflow: static checks only (JDK 21) ## 7) UI category status diff --git a/scripts/run-offline-gate.ps1 b/scripts/run-offline-gate.ps1 new file mode 100644 index 0000000..066738a --- /dev/null +++ b/scripts/run-offline-gate.ps1 @@ -0,0 +1,75 @@ +[CmdletBinding()] +param( + [ValidateSet('test', 'verify')] + [string] $Goal = 'verify', + + [switch] $StrictDependencies +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$repoRoot = Split-Path -Parent $PSScriptRoot +$pomPath = Join-Path $repoRoot 'pom.xml' +$maven = Get-Command mvn -ErrorAction Stop +$localRepository = if ($env:MAVEN_REPO_LOCAL) { + $env:MAVEN_REPO_LOCAL +} else { + Join-Path $env:USERPROFILE '.m2\repository' +} + +[xml] $pom = Get-Content -LiteralPath $pomPath -Raw +$configuredJUnit = [string] $pom.project.properties.'junit.version' +$configuredPlatform = [string] $pom.project.properties.'junit.platform.version' +$configuredBom = Join-Path $localRepository ( + 'org\junit\junit-bom\{0}\junit-bom-{0}.pom' -f $configuredJUnit +) + +$mavenArguments = @( + '--offline' + '--batch-mode' + '--no-transfer-progress' + '--file' + $pomPath +) + +if (-not (Test-Path -LiteralPath $configuredBom)) { + if ($StrictDependencies) { + throw "The configured JUnit BOM $configuredJUnit is absent from the local Maven cache." + } + + $configuredMajor = ([version] $configuredJUnit).Major + $bomRoot = Join-Path $localRepository 'org\junit\junit-bom' + $fallback = Get-ChildItem -LiteralPath $bomRoot -Directory -ErrorAction SilentlyContinue | + Where-Object { + $_.Name -match '^\d+\.\d+\.\d+$' -and + ([version] $_.Name).Major -eq $configuredMajor -and + (Test-Path -LiteralPath (Join-Path $_.FullName ("junit-bom-{0}.pom" -f $_.Name))) + } | + Sort-Object { [version] $_.Name } -Descending | + Select-Object -First 1 + + if ($null -eq $fallback) { + throw "No JUnit $configuredMajor.x BOM is available in the local Maven cache." + } + + Write-Warning (( + "JUnit BOM $configuredJUnit is not cached; offline validation uses cached {0}. " + + "Run with -StrictDependencies to require the exact POM version." + ) -f $fallback.Name) + $mavenArguments += "-Djunit.version=$($fallback.Name)" + + if ($configuredPlatform -eq $configuredJUnit) { + $mavenArguments += "-Djunit.platform.version=$($fallback.Name)" + } +} + +Push-Location $repoRoot +try { + & $maven.Source @mavenArguments $Goal + if ($LASTEXITCODE -ne 0) { + throw "Offline Maven gate failed with exit code $LASTEXITCODE." + } +} finally { + Pop-Location +} diff --git a/src/main/java/interview/CallbackScheduler.java b/src/main/java/interview/CallbackScheduler.java index 9a56ada..5c8bd36 100644 --- a/src/main/java/interview/CallbackScheduler.java +++ b/src/main/java/interview/CallbackScheduler.java @@ -33,10 +33,23 @@ public CallbackScheduler() { public void schedule(Runnable callback, Instant when) { Objects.requireNonNull(callback); Objects.requireNonNull(when); - long delayMillis = Math.max(0L, Duration.between(Instant.now(), when).toMillis()); + long delayMillis = delayMillisUntil(when); scheduler.schedule(callback, delayMillis, TimeUnit.MILLISECONDS); } + private static long delayMillisUntil(Instant when) { + Duration delay = Duration.between(Instant.now(), when); + if (delay.isNegative() || delay.isZero()) { + return 0L; + } + try { + return delay.toMillis(); + } catch (ArithmeticException ignored) { + // Instant supports a wider range than a millisecond delay stored in a long. + return Long.MAX_VALUE; + } + } + @Override public void close() { scheduler.shutdownNow(); diff --git a/src/main/java/kyu4/MorseCodeDecoder.java b/src/main/java/kyu4/MorseCodeDecoder.java index 397bbf2..cf6bb3e 100644 --- a/src/main/java/kyu4/MorseCodeDecoder.java +++ b/src/main/java/kyu4/MorseCodeDecoder.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.StringJoiner; @@ -129,7 +130,7 @@ public static String decodeMorse(String morseCode) { } decodedWords.add(decodedWord.toString()); } - return String.join(" ", decodedWords).toUpperCase(); + return String.join(" ", decodedWords).toUpperCase(Locale.ROOT); } public static String encode(String toMorseCode) { @@ -141,7 +142,7 @@ public static String encode(String toMorseCode) { for (String word : toMorseCode.trim().split("\\s+")) { StringJoiner letters = new StringJoiner(" "); for (char letter : word.toCharArray()) { - String morse = ALPHABET_TO_MORSE.get(String.valueOf(letter).toLowerCase()); + String morse = ALPHABET_TO_MORSE.get(String.valueOf(letter).toLowerCase(Locale.ROOT)); if (morse == null) { throw new IllegalArgumentException("Unsupported Morse character: " + letter); } diff --git a/src/main/java/kyu5/HumanReadableTime.java b/src/main/java/kyu5/HumanReadableTime.java index 7e03b66..6c74584 100644 --- a/src/main/java/kyu5/HumanReadableTime.java +++ b/src/main/java/kyu5/HumanReadableTime.java @@ -21,8 +21,8 @@ public class HumanReadableTime { private static final int SIXTY = 60; public static String makeReadable(int seconds) { - if (seconds > 359999) { - throw new IllegalArgumentException("very long arguments"); + if (seconds < 0 || seconds > 359999) { + throw new IllegalArgumentException("seconds must be between 0 and 359999"); } int HH = 0; int MM = 0; diff --git a/src/main/java/kyu5/MaximumSubarraySum.java b/src/main/java/kyu5/MaximumSubarraySum.java index f1c29a7..e915b12 100644 --- a/src/main/java/kyu5/MaximumSubarraySum.java +++ b/src/main/java/kyu5/MaximumSubarraySum.java @@ -1,34 +1,21 @@ package kyu5; -import java.util.Arrays; - - public class MaximumSubarraySum { //5 public static int sequence(int[] arr) { - int maximumSubArraySum = arraySum(arr); - for (int i = 0; i <= arr.length; i++) { - for (int j = arr.length; j >= i; j--) { - int[] ints = Arrays.copyOfRange(arr, i, j); - int subArraySum = arraySum(ints); - if (subArraySum > maximumSubArraySum) { - maximumSubArraySum = subArraySum; - } - } + if (arr == null) { + throw new IllegalArgumentException("array must not be null"); } - return maximumSubArraySum; - } - - private static int arraySum(int[] arr) { - int result = 0; - for (int i : arr - ) { - result += i; + long best = 0L; + long current = 0L; + for (int value : arr) { + current = Math.max(0L, current + value); + best = Math.max(best, current); } - return result; + return Math.toIntExact(best); } /** diff --git a/src/main/java/kyu5/NumberOfTrailingZerosOfN.java b/src/main/java/kyu5/NumberOfTrailingZerosOfN.java index b1ed8ac..7a1eadf 100644 --- a/src/main/java/kyu5/NumberOfTrailingZerosOfN.java +++ b/src/main/java/kyu5/NumberOfTrailingZerosOfN.java @@ -1,32 +1,23 @@ package kyu5; -import java.math.BigInteger; - - public class NumberOfTrailingZerosOfN { //5 https://www.codewars.com/kata/52f787eb172a8b4ae1000a34/train/java public static int zeros(int n) { - return zeroOfTrailing(factorial(n)); - } - - private static int zeroOfTrailing(Long l) { - int count = 0; - while (l % 10 == 0) { - count++; - l /= 10; + if (n < 0) { + throw new IllegalArgumentException("n must not be negative"); } - return count; - } - - private static long factorial(int n) { - BigInteger result = BigInteger.ONE; - for (int i = 1; i <= n; i++) { - result = result.multiply(BigInteger.valueOf(i)); + int zeroCount = 0; + for (int factor = 5; factor <= n; ) { + zeroCount += n / factor; + if (factor > n / 5) { + break; + } + factor *= 5; } - return result.longValue(); + return zeroCount; } } diff --git a/src/main/java/kyu5/ProductFib.java b/src/main/java/kyu5/ProductFib.java index b523523..99892e9 100644 --- a/src/main/java/kyu5/ProductFib.java +++ b/src/main/java/kyu5/ProductFib.java @@ -8,19 +8,39 @@ public class ProductFib { //5 public static long[] productFib(long prod) { - long n = 0, i = 1; - while (n < prod) { - n = getFibonacciValue(i) * getFibonacciValue(i + 1); - i++; + if (prod < 0) { + throw new IllegalArgumentException("product must not be negative"); } - return new long[]{getFibonacciValue(i - 1), getFibonacciValue(i), n == prod ? 1 : 0}; + long first = 0L; + long second = 1L; + while (productIsLessThan(first, second, prod)) { + long next = Math.addExact(first, second); + first = second; + second = next; + } + boolean exact = first == 0L ? prod == 0L : first <= prod / second && first * second == prod; + return new long[]{first, second, exact ? 1L : 0L}; } public static long getFibonacciValue(final long n) { - // https://ru.stackoverflow.com/questions/39229/Последовательности-чисел-Фибоначчи - double p = (1 + Math.sqrt(5)) / 2; - double q = 1 / p; - return (long) ((Math.pow(p, n) + Math.pow(q, n)) / Math.sqrt(5)); + if (n < 0) { + throw new IllegalArgumentException("index must not be negative"); + } + long first = 0L; + long second = 1L; + for (long i = 0L; i < n; i++) { + if (i == n - 1L) { + return second; + } + long next = Math.addExact(first, second); + first = second; + second = next; + } + return first; + } + + private static boolean productIsLessThan(long first, long second, long target) { + return first == 0L ? target > 0L : first <= target / second && first * second < target; } /** diff --git a/src/main/java/kyu5/Scrambles.java b/src/main/java/kyu5/Scrambles.java index f18a533..2260fcf 100644 --- a/src/main/java/kyu5/Scrambles.java +++ b/src/main/java/kyu5/Scrambles.java @@ -1,26 +1,25 @@ package kyu5; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - - - public class Scrambles { // 5 https://www.codewars.com/kata/55c04b4cc56a697bb0000048/train/java public static boolean scramble(String str1, String str2) { - List list1 = str1.chars() - .mapToObj(c -> (char) c) - .collect(Collectors.toCollection(ArrayList::new)); - List list2 = str2.chars() - .mapToObj(c -> (char) c) - .collect(Collectors.toList()); - for (char c2 : list2 - ) { - if (!list1.remove(Character.valueOf(c2))) return false; + if (str1 == null || str2 == null) { + throw new IllegalArgumentException("inputs must not be null"); + } + if (str2.length() > str1.length()) { + return false; + } + int[] available = new int[Character.MAX_VALUE + 1]; + for (int i = 0; i < str1.length(); i++) { + available[str1.charAt(i)]++; + } + for (int i = 0; i < str2.length(); i++) { + if (--available[str2.charAt(i)] < 0) { + return false; + } } return true; } diff --git a/src/main/java/kyu5/SumSquaredDivisors.java b/src/main/java/kyu5/SumSquaredDivisors.java index 98647a6..acc6901 100644 --- a/src/main/java/kyu5/SumSquaredDivisors.java +++ b/src/main/java/kyu5/SumSquaredDivisors.java @@ -1,10 +1,7 @@ package kyu5; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; +import java.util.StringJoiner; public class SumSquaredDivisors { @@ -12,49 +9,48 @@ public class SumSquaredDivisors { //5 https://www.codewars.com/kata/55aa075506463dac6600010d/train/java public static String listSquared(long m, long n) { - Map result = new TreeMap<>(); - for (long i = m; i <= n; i++) { - long sumSquare = sumSquareList(listDivisor(i)); - if (checkSquare(sumSquare)) { - result.putIfAbsent(i, sumSquare); - } - } - - StringBuilder stringBuilder = new StringBuilder(); - stringBuilder.append("["); - for (Map.Entry entry : result.entrySet() - ) { - stringBuilder.append("[").append(entry.getKey()).append(", ").append(entry.getValue()).append("], "); + if (m < 1L || n < m) { + throw new IllegalArgumentException("range must satisfy 1 <= m <= n"); } - String substring = "["; - if (stringBuilder.length() > 2) { - substring = stringBuilder.substring(0, stringBuilder.length() - 2); + StringJoiner result = new StringJoiner(", ", "[", "]"); + for (long value = m; ; value++) { + long sumSquare = sumSquaredDivisors(value); + if (isPerfectSquare(sumSquare)) { + result.add("[" + value + ", " + sumSquare + "]"); + } + if (value == n) { + break; + } } - substring = substring + "]"; - - return substring; - } - - private static boolean checkSquare(long n) { - return Math.sqrt(n) % 1 == 0.0; + return result.toString(); } - private static List listDivisor(long n) { - List l = new ArrayList<>(); - for (long i = 1; i <= n; i++) { - if ((n % i) == 0) { - l.add(i); + private static long sumSquaredDivisors(long value) { + long sum = 0L; + for (long divisor = 1L; divisor <= value / divisor; divisor++) { + if (value % divisor == 0L) { + long pairedDivisor = value / divisor; + sum = Math.addExact(sum, Math.multiplyExact(divisor, divisor)); + if (pairedDivisor != divisor) { + sum = Math.addExact(sum, Math.multiplyExact(pairedDivisor, pairedDivisor)); + } } } - return l; + return sum; } - private static long sumSquareList(List list) { - long result = 0; - for (long i : list) { - result += i * i; + private static boolean isPerfectSquare(long value) { + if (value < 0L) { + return false; + } + long root = (long) Math.sqrt(value); + while (root > 0L && root > value / root) { + root--; + } + while (root + 1L <= value / (root + 1L)) { + root++; } - return result; + return root * root == value; } /** diff --git a/src/main/java/kyu6/ArmstrongNumber.java b/src/main/java/kyu6/ArmstrongNumber.java index e07ffbf..2884fba 100644 --- a/src/main/java/kyu6/ArmstrongNumber.java +++ b/src/main/java/kyu6/ArmstrongNumber.java @@ -10,17 +10,25 @@ public static boolean isNarcissistic(int number) { return false; } - int result = 0; + long result = 0L; int currentNumber = number; int power = numberOfDigits(number); do { int currentDigit = currentNumber % 10; currentNumber /= 10; - result += (int) Math.pow(currentDigit, power); + result += pow(currentDigit, power); } while (currentNumber > 0); return result == number; } + private static long pow(int base, int exponent) { + long result = 1L; + for (int i = 0; i < exponent; i++) { + result *= base; + } + return result; + } + public static int numberOfDigits(int number) { var length = 1; diff --git a/src/main/java/kyu6/CountingDuplicates.java b/src/main/java/kyu6/CountingDuplicates.java index beb93fa..43e7b62 100644 --- a/src/main/java/kyu6/CountingDuplicates.java +++ b/src/main/java/kyu6/CountingDuplicates.java @@ -11,7 +11,7 @@ public class CountingDuplicates { public static int duplicateCountStream(String text) { return (int) text.chars() - .mapToObj(i -> (char) i) + .mapToObj(i -> Character.toLowerCase((char) i)) .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())) .entrySet() .stream() @@ -21,14 +21,13 @@ public static int duplicateCountStream(String text) { public static int duplicateCount(String text) { int result = 0; - long[] ascii = new long[127]; + long[] characterCounts = new long[Character.MAX_VALUE + 1]; - char[] chars = text.toLowerCase().toCharArray(); - for (char aChar : chars) { - ascii[aChar]++; + for (char current : text.toCharArray()) { + characterCounts[Character.toLowerCase(current)]++; } - for (long l : ascii) { - if (l > 1) { + for (long count : characterCounts) { + if (count > 1) { result++; } } diff --git a/src/main/java/kyu6/DigPow.java b/src/main/java/kyu6/DigPow.java index db67de5..d814837 100644 --- a/src/main/java/kyu6/DigPow.java +++ b/src/main/java/kyu6/DigPow.java @@ -1,19 +1,35 @@ package kyu6; -import java.util.concurrent.atomic.AtomicInteger; - - public class DigPow { public static long digPow(int n, int p) { - AtomicInteger pa = new AtomicInteger(p); - int sum = String.valueOf(n) - .chars() - .map(Character::getNumericValue) - .map(i -> (int) Math.pow(i, pa.getAndIncrement())) - .sum(); + if (n <= 0 || p <= 0) { + throw new IllegalArgumentException("n and p must be positive"); + } + long sum = 0L; + long exponent = p; + for (char digit : Integer.toString(n).toCharArray()) { + sum = Math.addExact(sum, powExact(Character.digit(digit, 10), exponent)); + exponent++; + } return sum % n == 0 ? sum / n : -1; } + private static long powExact(long base, long exponent) { + long result = 1L; + long factor = base; + long remaining = exponent; + while (remaining > 0L) { + if ((remaining & 1L) == 1L) { + result = Math.multiplyExact(result, factor); + } + remaining >>>= 1; + if (remaining > 0L) { + factor = Math.multiplyExact(factor, factor); + } + } + return result; + } + } diff --git a/src/main/java/kyu6/FindEvenIndex.java b/src/main/java/kyu6/FindEvenIndex.java index c6573d2..0becafa 100644 --- a/src/main/java/kyu6/FindEvenIndex.java +++ b/src/main/java/kyu6/FindEvenIndex.java @@ -1,55 +1,32 @@ package kyu6; -import java.util.stream.IntStream; - - public class FindEvenIndex { public static int findEvenIndexV2(int[] arr) { - for (int i = 0; i < arr.length; i++) { - if (findHalf1(arr, i) == findHalf2(arr, i)) { - return i; - } - } - return -1; + return findEvenIndex(arr); } - private static int findHalf1(int[] arr, int i) { - int sum = 0; - for (int j = 0; j < i; j++) { - sum += arr[j]; - } - return sum; + public static int findEvenIndexV1(int[] arr) { + return findEvenIndex(arr); } - private static int findHalf2(int[] arr, int i) { - int sum = 0; - for (int j = i + 1; j < arr.length; j++) { - sum += arr[j]; + private static int findEvenIndex(int[] arr) { + if (arr == null) { + throw new IllegalArgumentException("array must not be null"); } - return sum; - - } - - public static int findEvenIndexV1(int[] arr) { - - int sumArr = IntStream.of(arr).sum(); - int firstHalfSumm = 0; - int secondHalfSumm = 0; - int halfIndex = -1; + long rightSum = 0L; + for (int value : arr) { + rightSum += value; + } + long leftSum = 0L; for (int i = 0; i < arr.length; i++) { - firstHalfSumm += arr[i]; - if (firstHalfSumm >= (sumArr / 2)) { - halfIndex = i; - firstHalfSumm -= arr[i]; - break; + rightSum -= arr[i]; + if (leftSum == rightSum) { + return i; } + leftSum += arr[i]; } - for (int i = arr.length - 1; i > halfIndex; i--) { - secondHalfSumm += arr[i]; - } - if (firstHalfSumm == secondHalfSumm) return halfIndex; return -1; } diff --git a/src/main/java/kyu6/PhoneNumber.java b/src/main/java/kyu6/PhoneNumber.java index 2210144..c6cddc6 100644 --- a/src/main/java/kyu6/PhoneNumber.java +++ b/src/main/java/kyu6/PhoneNumber.java @@ -1,13 +1,23 @@ package kyu6; +import java.util.Locale; + public class PhoneNumber { //6 public static String createPhoneNumber(int[] numbers) { - return String.format("(%d%d%d) %d%d%d-%d%d%d%d", numbers[0], numbers[1], numbers[2], numbers[3], + if (numbers == null || numbers.length != 10) { + throw new IllegalArgumentException("a phone number must contain exactly ten digits"); + } + for (int digit : numbers) { + if (digit < 0 || digit > 9) { + throw new IllegalArgumentException("phone number elements must be decimal digits"); + } + } + return String.format(Locale.ROOT, "(%d%d%d) %d%d%d-%d%d%d%d", numbers[0], numbers[1], numbers[2], numbers[3], numbers[4], numbers[5], numbers[6], numbers[7], numbers[8], numbers[9]); } diff --git a/src/main/java/kyu6/SimpleEncryption.java b/src/main/java/kyu6/SimpleEncryption.java index 9bb2a51..32bf30a 100644 --- a/src/main/java/kyu6/SimpleEncryption.java +++ b/src/main/java/kyu6/SimpleEncryption.java @@ -1,51 +1,77 @@ package kyu6; -import java.util.LinkedList; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - - public class SimpleEncryption { //6 https://www.codewars.com/SimpleEncryption/57814d79a56c88e3e0000786/train/java public static String encrypt(final String text, final int n) { - if (text == null || text.isEmpty() || n < 1) return text; + return transform(text, n, false); + } - var start = new StringBuilder(text); - var current = new StringBuilder(); - char[] chars = start.toString().toCharArray(); + public static String decrypt(final String encryptedText, final int n) { + return transform(encryptedText, n, true); + } - for (int j = 1; j < start.length(); j += 2) { - current.append(chars[j]); + private static String transform(String text, int iterations, boolean inverse) { + if (text == null || text.isEmpty() || iterations < 1) { + return text; + } + int[] permutation = encryptionPermutation(text.length()); + if (inverse) { + permutation = invert(permutation); } - for (int j = 0; j < start.length(); j += 2) { - current.append(chars[j]); + int[] poweredPermutation = power(permutation, iterations); + char[] transformed = new char[text.length()]; + for (int source = 0; source < text.length(); source++) { + transformed[poweredPermutation[source]] = text.charAt(source); } - start = new StringBuilder(current); - return encrypt(start.toString(), n - 1); + return String.valueOf(transformed); } - public static String decrypt(final String encryptedText, final int n) { - if (encryptedText == null || encryptedText.isEmpty() || n < 1) return encryptedText; + private static int[] encryptionPermutation(int length) { + int[] permutation = new int[length]; + int split = length / 2; + for (int source = 0; source < length; source++) { + permutation[source] = (source & 1) == 1 ? source / 2 : split + source / 2; + } + return permutation; + } - var start = new StringBuilder(encryptedText); - var current = new char[start.length()]; - var encryptedList = IntStream.range(0, encryptedText.length()) - .mapToObj(encryptedText::charAt) - .collect(Collectors.toCollection(LinkedList::new)); + private static int[] invert(int[] permutation) { + int[] inverse = new int[permutation.length]; + for (int source = 0; source < permutation.length; source++) { + inverse[permutation[source]] = source; + } + return inverse; + } - for (int j = 1; j < start.length(); j += 2) { - current[j] = encryptedList.pollFirst(); + private static int[] power(int[] permutation, int exponent) { + int[] result = new int[permutation.length]; + int[] factor = permutation.clone(); + for (int i = 0; i < result.length; i++) { + result[i] = i; } - for (int j = 0; j < start.length(); j += 2) { - current[j] = current[j] = encryptedList.pollFirst(); + int remaining = exponent; + while (remaining > 0) { + if ((remaining & 1) == 1) { + result = compose(result, factor); + } + remaining >>>= 1; + if (remaining > 0) { + factor = compose(factor, factor); + } } - start = new StringBuilder(String.valueOf(current)); - return decrypt(start.toString(), n - 1); + return result; } + private static int[] compose(int[] first, int[] second) { + int[] result = new int[first.length]; + for (int i = 0; i < first.length; i++) { + result[i] = second[first[i]]; + } + return result; + } } diff --git a/src/main/java/kyu6/SortTheOdd.java b/src/main/java/kyu6/SortTheOdd.java index 97ae271..4c9183b 100644 --- a/src/main/java/kyu6/SortTheOdd.java +++ b/src/main/java/kyu6/SortTheOdd.java @@ -1,11 +1,7 @@ package kyu6; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedList; -import java.util.stream.Collectors; -import java.util.stream.IntStream; +import java.util.Arrays; public class SortTheOdd { @@ -13,37 +9,26 @@ public class SortTheOdd { // 6 https://www.codewars.com/kata/578aa45ee9fd15ff4600090d/train/java public static int[] sortArray(int[] array) { - ArrayList odd = new ArrayList<>(); - - for (int i = 0; i < array.length; i += 2) { - odd.add(array[i]); + if (array == null) { + throw new IllegalArgumentException("array must not be null"); } - Collections.sort(odd); - - for (int i = 0; i < array.length; i += 2) { - array[i] = odd.get((int) (i / 2.0 + 0.5)); + int[] result = array.clone(); + int[] sortedOdds = Arrays.stream(array) + .filter(value -> Math.floorMod(value, 2) == 1) + .sorted() + .toArray(); + int oddIndex = 0; + for (int i = 0; i < result.length; i++) { + if (Math.floorMod(result[i], 2) == 1) { + result[i] = sortedOdds[oddIndex++]; + } } - return array; + return result; } public static int[] sortArrayStream(int[] array) { - LinkedList oddSorted = IntStream - .range(0, array.length) - .filter(x -> x % 2 == 0) - .map(x -> array[x]) - .sorted() - .boxed() - .collect(Collectors.toCollection(LinkedList::new)); - - IntStream.range(0, array.length).forEach( - x -> { - if (x % 2 == 0) { - array[x] = oddSorted.pollFirst(); - } - }); - - return array; + return sortArray(array); } diff --git a/src/main/java/kyu6/TheSupermarketQueue.java b/src/main/java/kyu6/TheSupermarketQueue.java index 84a85eb..b36bbf1 100644 --- a/src/main/java/kyu6/TheSupermarketQueue.java +++ b/src/main/java/kyu6/TheSupermarketQueue.java @@ -1,8 +1,7 @@ package kyu6; -import java.util.Arrays; -import java.util.LinkedList; +import java.util.PriorityQueue; //6 https://www.codewars.com/kata/57b06f90e298a7b53d000a86/train/java @@ -10,53 +9,25 @@ public class TheSupermarketQueue { public static int solveSuperMarketQueue(int[] customers, int n) { - - if (customers.length == 0) return 0; - int max = Arrays.stream(customers).max().orElseThrow(); - - if (max < n) return max; - - LinkedList integers = new LinkedList<>(); - for (int i : customers) - integers.add(i); - - int[] workList = new int[n]; - - for (int i = 0; i < n; i++) { - if (!integers.isEmpty()) { - - workList[i] = integers.pollFirst(); - } else { - workList[i] = 0; - } + if (customers == null) { + throw new IllegalArgumentException("customers must not be null"); + } + if (n <= 0) { + throw new IllegalArgumentException("the number of tills must be positive"); } - int count = 0; - - while (true) { - int stopCount = 0; - for (int j : workList) { - if (j == -1) { - stopCount++; - } - } - - if (stopCount == n) break; - - count++; - - for (int i = 0; i < workList.length; i++) { - if (workList[i] > 1) { - workList[i]--; - } else if (!integers.isEmpty()) { - workList[i] = integers.pollFirst(); - } else { - workList[i] = -1; - } + PriorityQueue tills = new PriorityQueue<>(); + for (int i = 0; i < Math.min(n, customers.length); i++) { + tills.add(0L); + } + for (int customer : customers) { + if (customer < 0) { + throw new IllegalArgumentException("service times must not be negative"); } + long availableAt = tills.remove(); + tills.add(availableAt + customer); } - - return count; + return tills.isEmpty() ? 0 : Math.toIntExact(tills.stream().mapToLong(Long::longValue).max().orElse(0L)); } } diff --git a/src/main/java/kyu6/TribonacciSequence.java b/src/main/java/kyu6/TribonacciSequence.java index 18ef877..90d1868 100644 --- a/src/main/java/kyu6/TribonacciSequence.java +++ b/src/main/java/kyu6/TribonacciSequence.java @@ -7,28 +7,19 @@ public class TribonacciSequence { //6 https://www.codewars.com/kata/556deca17c58da83c00002db public static double[] tribonacci(double[] s, int n) { - if (n == 0) { - return new double[0]; + if (s == null || s.length != 3) { + throw new IllegalArgumentException("signature must contain exactly three values"); } - if (n == 1) { - double[] doubles = new double[1]; - doubles[0] = s[0]; - return doubles; + if (n < 0) { + throw new IllegalArgumentException("sequence length must not be negative"); } - if (n == 2) { - double[] doubles = new double[2]; - doubles[0] = s[0]; - doubles[1] = s[1]; - return doubles; - } - double[] doubles = new double[n]; - - System.arraycopy(s, 0, doubles, 0, s.length); - + double[] result = new double[n]; + int signatureLength = Math.min(s.length, n); + System.arraycopy(s, 0, result, 0, signatureLength); for (int i = s.length; i < n; i++) { - doubles[i] = doubles[i - 1] + doubles[i - 2] + doubles[i - 3]; + result[i] = result[i - 1] + result[i - 2] + result[i - 3]; } - return doubles; + return result; } } diff --git a/src/main/java/kyu6/TwoSum.java b/src/main/java/kyu6/TwoSum.java index 0bb6e56..598f255 100644 --- a/src/main/java/kyu6/TwoSum.java +++ b/src/main/java/kyu6/TwoSum.java @@ -8,7 +8,7 @@ public class TwoSum { public static int[] twoSum(int[] numbers, int target) { for (int i = 0; i < numbers.length; i++) { for (int j = i + 1; j < numbers.length; j++) { - if (numbers[i] + numbers[j] == target) { + if ((long) numbers[i] + numbers[j] == target) { return new int[]{i, j}; } } diff --git a/src/main/java/kyu6/VINChecker.java b/src/main/java/kyu6/VINChecker.java index 0e0e4a2..42d8136 100644 --- a/src/main/java/kyu6/VINChecker.java +++ b/src/main/java/kyu6/VINChecker.java @@ -5,6 +5,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; @@ -51,12 +52,15 @@ public class VINChecker { } public static boolean checkVin(String vin) { - if (vin == null || vin.toUpperCase().contains("I") || vin.toUpperCase().contains("O") - || vin.toUpperCase().contains("Q") || vin.length() != 17) return false; - + if (vin == null) { + return false; + } + String normalizedVin = vin.toUpperCase(Locale.ROOT); + if (normalizedVin.contains("I") || normalizedVin.contains("O") + || normalizedVin.contains("Q") || normalizedVin.length() != 17) return false; ArrayList convertedToNumber = new ArrayList<>(); - char[] vinChars = vin.toUpperCase().toCharArray(); + char[] vinChars = normalizedVin.toCharArray(); for (Character c : vinChars ) { if (!LETTERS.containsKey(c)) return false; diff --git a/src/main/java/kyu6/toCamelCase.java b/src/main/java/kyu6/toCamelCase.java index 970c8f8..f87faaa 100644 --- a/src/main/java/kyu6/toCamelCase.java +++ b/src/main/java/kyu6/toCamelCase.java @@ -1,18 +1,27 @@ package kyu6; +import java.util.Locale; + public class toCamelCase { //6 https://www.codewars.com/kata/517abf86da9663f1d2000003/train/java public static String toCamelCase(String s) { - if (s == null || s.isEmpty()) return ""; - String[] split = s.split("[-_]"); + if (s == null || s.isEmpty()) { + return ""; + } + String[] split = s.split("[-_]", -1); + for (String segment : split) { + if (segment.isEmpty()) { + throw new IllegalArgumentException("delimiters must separate non-empty words"); + } + } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(split[0]); for (int i = 1; i < split.length; i++) { - stringBuilder.append(split[i].substring(0, 1).toUpperCase()).append(split[i].substring(1)); + stringBuilder.append(split[i].substring(0, 1).toUpperCase(Locale.ROOT)).append(split[i].substring(1)); } return stringBuilder.toString(); } diff --git a/src/main/java/kyu7/Accumul.java b/src/main/java/kyu7/Accumul.java index 0328c30..26b2136 100644 --- a/src/main/java/kyu7/Accumul.java +++ b/src/main/java/kyu7/Accumul.java @@ -21,20 +21,7 @@ public class Accumul { public static String accum(String s) { - StringBuilder result = new StringBuilder(); - char[] chars = s.toCharArray(); - for (int i = 0; i < chars.length; i++) { - for (int j = 0; j <= i; j++) { - if (j == 0) { - result.append(String.valueOf(s.charAt(i)).toUpperCase()); - } else { - result.append(String.valueOf(s.charAt(i)).toLowerCase()); - } - } - result.append("-"); - } - - return result.substring(0, result.length() - 1); + return streamAccum(s); } public static String streamAccum(String s) { diff --git a/src/main/java/kyu7/CountTheDigit.java b/src/main/java/kyu7/CountTheDigit.java index d512f89..65d1d6f 100644 --- a/src/main/java/kyu7/CountTheDigit.java +++ b/src/main/java/kyu7/CountTheDigit.java @@ -5,12 +5,15 @@ public class CountTheDigit { public static int nbDig(int n, int d) { + if (n < 0 || d < 0 || d > 9) { + throw new IllegalArgumentException("n must be non-negative and d must be a decimal digit"); + } long count = 0; - for (int i = 0; i <= n; i++) { - String s = String.valueOf((int) Math.pow(i, 2)); + for (long i = 0L; i <= n; i++) { + String s = Long.toString(i * i); count += s.chars().filter(ch -> ch == Character.forDigit(d, 10)).count(); } - return (int) count; + return Math.toIntExact(count); } } diff --git a/src/main/java/kyu7/DontGiveMeFive.java b/src/main/java/kyu7/DontGiveMeFive.java index ca1daea..c9a23bf 100644 --- a/src/main/java/kyu7/DontGiveMeFive.java +++ b/src/main/java/kyu7/DontGiveMeFive.java @@ -7,11 +7,45 @@ public class DontGiveMeFive { //7 https://www.codewars.com/kata/5813d19765d81c592200001a/solutions/java public static int dontGiveMeFive(int start, int end) { - int count = 0; - for (int i = start; i <= end; i++) { - if (!String.valueOf(i).contains("5")) count++; + if (start > end) { + throw new IllegalArgumentException("start must not be greater than end"); } - return count; + long count; + if (end < 0) { + long largestMagnitude = -(long) start; + long smallestMagnitude = -(long) end; + count = countWithoutFive(largestMagnitude) - countWithoutFive(smallestMagnitude - 1L); + } else if (start >= 0) { + count = countWithoutFive(end) - countWithoutFive((long) start - 1L); + } else { + count = countWithoutFive(-(long) start) - 1L + countWithoutFive(end); + } + return Math.toIntExact(count); + } + + private static long countWithoutFive(long upperBound) { + if (upperBound < 0L) { + return 0L; + } + char[] digits = Long.toString(upperBound).toCharArray(); + long count = 0L; + for (int position = 0; position < digits.length; position++) { + int digit = Character.digit(digits[position], 10); + int allowedSmallerDigits = digit - (digit > 5 ? 1 : 0); + count += allowedSmallerDigits * powerOfNine(digits.length - position - 1); + if (digit == 5) { + return count; + } + } + return count + 1L; + } + + private static long powerOfNine(int exponent) { + long result = 1L; + for (int i = 0; i < exponent; i++) { + result *= 9L; + } + return result; } } diff --git a/src/main/java/kyu7/FindDivisor.java b/src/main/java/kyu7/FindDivisor.java index 8c18361..e56b0d3 100644 --- a/src/main/java/kyu7/FindDivisor.java +++ b/src/main/java/kyu7/FindDivisor.java @@ -1,17 +1,21 @@ package kyu7; -import java.util.stream.IntStream; - - public class FindDivisor { //7 https://www.codewars.com/kata/53dbd5315a3c69eed20002dd/train/java public static long numberOFindDivisorivisors(int i) { - return IntStream.range(1, i + 1) - .filter(n -> i % n == 0) - .count(); + if (i <= 0) { + throw new IllegalArgumentException("number must be positive"); + } + long count = 0L; + for (int divisor = 1; divisor <= i / divisor; divisor++) { + if (i % divisor == 0) { + count += divisor == i / divisor ? 1L : 2L; + } + } + return count; } } diff --git a/src/main/java/kyu7/GetSum.java b/src/main/java/kyu7/GetSum.java index cee5edd..5ac63ec 100644 --- a/src/main/java/kyu7/GetSum.java +++ b/src/main/java/kyu7/GetSum.java @@ -7,7 +7,11 @@ public class GetSum { //7 https://www.codewars.com/kata/55f2b110f61eb01779000053/train/java public static int getSum(int a, int b) { - return (a + b) * (Math.abs(a - b) + 1) / 2; + long lower = Math.min(a, b); + long upper = Math.max(a, b); + long count = upper - lower + 1L; + long sum = (lower + upper) * count / 2L; + return Math.toIntExact(sum); } } diff --git a/src/main/java/kyu7/GetXO.java b/src/main/java/kyu7/GetXO.java index 4483d53..72a658e 100644 --- a/src/main/java/kyu7/GetXO.java +++ b/src/main/java/kyu7/GetXO.java @@ -15,10 +15,15 @@ public static boolean getXO(String str) { } public static int counterStr(String str, String target) { - + if (str == null || target == null || target.isEmpty()) { + throw new IllegalArgumentException("input and target must not be null or empty"); + } int result = 0; - for (char element : str.toLowerCase().toCharArray()) { - if (element == target.toLowerCase().toCharArray()[0]) result++; + char expected = Character.toLowerCase(target.charAt(0)); + for (char element : str.toCharArray()) { + if (Character.toLowerCase(element) == expected) { + result++; + } } return result; } diff --git a/src/main/java/kyu7/IsAnagram.java b/src/main/java/kyu7/IsAnagram.java index 29c6c72..07653d0 100644 --- a/src/main/java/kyu7/IsAnagram.java +++ b/src/main/java/kyu7/IsAnagram.java @@ -2,13 +2,14 @@ import java.util.Arrays; +import java.util.Locale; public class IsAnagram { public static boolean isAnagram(String test, String original) { - char[] testChars = test.toLowerCase().toCharArray(); - char[] originalChars = original.toLowerCase().toCharArray(); + char[] testChars = test.toLowerCase(Locale.ROOT).toCharArray(); + char[] originalChars = original.toLowerCase(Locale.ROOT).toCharArray(); Arrays.sort(testChars); Arrays.sort(originalChars); return Arrays.equals(testChars, originalChars); diff --git a/src/main/java/kyu7/Isogram.java b/src/main/java/kyu7/Isogram.java index a069542..bdce601 100644 --- a/src/main/java/kyu7/Isogram.java +++ b/src/main/java/kyu7/Isogram.java @@ -2,6 +2,7 @@ import java.util.HashSet; +import java.util.Locale; public class Isogram { @@ -10,7 +11,7 @@ public class Isogram { public static boolean isIsogram(String str) { HashSet characters = new HashSet<>(); - for (char c : str.toLowerCase().toCharArray()) { + for (char c : str.toLowerCase(Locale.ROOT).toCharArray()) { if (characters.contains(c)) return false; characters.add(c); } diff --git a/src/main/java/kyu7/JadenCase.java b/src/main/java/kyu7/JadenCase.java index 14f1723..7195a02 100644 --- a/src/main/java/kyu7/JadenCase.java +++ b/src/main/java/kyu7/JadenCase.java @@ -1,18 +1,27 @@ package kyu7; - public class JadenCase { //7 https://www.codewars.com/kata/5390bac347d09b7da40006f6/train/java public static String toJadenCase(String phrase) { - if (phrase == null || phrase.length() == 0) return null; - StringBuilder stringBuilder = new StringBuilder(); - for (String s : phrase.split("\\s")) { - stringBuilder.append(s.substring(0, 1).toUpperCase()).append(s.substring(1)).append(" "); + if (phrase == null || phrase.isEmpty()) { + return null; + } + StringBuilder result = new StringBuilder(phrase.length()); + boolean capitalizeNext = true; + for (int i = 0; i < phrase.length(); i++) { + char current = phrase.charAt(i); + if (Character.isWhitespace(current)) { + result.append(current); + capitalizeNext = true; + } else { + result.append(capitalizeNext ? Character.toUpperCase(current) : current); + capitalizeNext = false; + } } - return stringBuilder.toString().trim(); + return result.toString(); } diff --git a/src/main/java/kyu7/MaxMultiple.java b/src/main/java/kyu7/MaxMultiple.java index 1758d7b..603d335 100644 --- a/src/main/java/kyu7/MaxMultiple.java +++ b/src/main/java/kyu7/MaxMultiple.java @@ -1,18 +1,14 @@ package kyu7; -import java.util.stream.IntStream; - - public class MaxMultiple { //7 https://www.codewars.com/kata/5aba780a6a176b029800041c/train/java public static int maxMultiple(int divisor, int bound) { - return IntStream.iterate(bound, i -> i - 1) - .limit(bound) - .filter(n -> n % divisor == 0) - .findFirst() - .orElse(0); + if (divisor <= 0 || bound < divisor) { + throw new IllegalArgumentException("divisor must be positive and no greater than bound"); + } + return bound - bound % divisor; } } diff --git a/src/main/java/kyu7/Money.java b/src/main/java/kyu7/Money.java index a3b621e..7b3485e 100644 --- a/src/main/java/kyu7/Money.java +++ b/src/main/java/kyu7/Money.java @@ -7,11 +7,22 @@ public class Money { //7 https://www.codewars.com/kata/563f037412e5ada593000114/train/java public static int calculateYears(double principal, double interest, double tax, double desired) { - if (principal >= desired) return 0; + if (!Double.isFinite(principal) || !Double.isFinite(interest) + || !Double.isFinite(tax) || !Double.isFinite(desired) + || principal < 0.0 || interest < 0.0 || tax < 0.0 || tax > 1.0 || desired < 0.0) { + throw new IllegalArgumentException("financial inputs must be finite and non-negative"); + } + if (principal >= desired) { + return 0; + } int years = 0; while (principal < desired) { double addOneYear = principal * interest; - principal = principal + (addOneYear - addOneYear * tax); + double nextPrincipal = principal + addOneYear * (1.0 - tax); + if (!(nextPrincipal > principal)) { + throw new IllegalArgumentException("the supplied growth cannot reach the desired amount"); + } + principal = nextPrincipal; years++; } return years; diff --git a/src/main/java/kyu7/NbYear.java b/src/main/java/kyu7/NbYear.java index a26ad1e..2460bce 100644 --- a/src/main/java/kyu7/NbYear.java +++ b/src/main/java/kyu7/NbYear.java @@ -7,10 +7,23 @@ public class NbYear { //7 https://www.codewars.com/kata/563b662a59afc2b5120000c6/train/java public static int nbYear(int p0, double percent, int aug, int p) { + if (p0 < 0 || p < 0 || !Double.isFinite(percent) || percent < 0.0) { + throw new IllegalArgumentException("population and growth inputs must be finite and non-negative"); + } + if (p0 >= p) { + return 0; + } int year = 0; while (p0 < p) { - p0 = (int) (p0 + (p0 * (percent / 100) + aug)); + long nextPopulation = (long) (p0 + p0 * (percent / 100.0) + aug); + if (nextPopulation <= p0) { + throw new IllegalArgumentException("the supplied growth cannot reach the target population"); + } year++; + if (nextPopulation >= p) { + return year; + } + p0 = Math.toIntExact(nextPopulation); } return year; } diff --git a/src/main/java/kyu7/NumberFun.java b/src/main/java/kyu7/NumberFun.java index 68f0d53..db8b696 100644 --- a/src/main/java/kyu7/NumberFun.java +++ b/src/main/java/kyu7/NumberFun.java @@ -5,10 +5,21 @@ public class NumberFun { public static long findNextSquare(long sq) { + if (sq < 0L) { + return -1L; + } long sqrt = (long) Math.sqrt(sq); - if (sqrt * sqrt != sq) return -1; - long next = sqrt + 1; - return next * next; + while (sqrt > 0L && sqrt > sq / sqrt) { + sqrt--; + } + while (sqrt + 1L <= sq / (sqrt + 1L)) { + sqrt++; + } + if (sqrt * sqrt != sq) { + return -1L; + } + long next = Math.addExact(sqrt, 1L); + return Math.multiplyExact(next, next); } } diff --git a/src/main/java/kyu7/ReverseWords.java b/src/main/java/kyu7/ReverseWords.java index 9aa7113..6d595a1 100644 --- a/src/main/java/kyu7/ReverseWords.java +++ b/src/main/java/kyu7/ReverseWords.java @@ -5,14 +5,26 @@ public class ReverseWords { public static String reverseWords(final String original) { - if (original.matches("\\s+")) return original; - StringBuilder stringBuilder = new StringBuilder(); - String[] s = original.split("\\s"); - for (String s1 : s - ) { - stringBuilder.append(new StringBuilder(s1).reverse()).append(" "); + if (original == null) { + throw new IllegalArgumentException("input must not be null"); } - return stringBuilder.substring(0, stringBuilder.length() - 1); + StringBuilder result = new StringBuilder(original.length()); + int wordStart = 0; + while (wordStart < original.length()) { + if (Character.isWhitespace(original.charAt(wordStart))) { + result.append(original.charAt(wordStart++)); + continue; + } + int wordEnd = wordStart + 1; + while (wordEnd < original.length() && !Character.isWhitespace(original.charAt(wordEnd))) { + wordEnd++; + } + for (int i = wordEnd - 1; i >= wordStart; i--) { + result.append(original.charAt(i)); + } + wordStart = wordEnd; + } + return result.toString(); } } diff --git a/src/main/java/kyu7/RoundToTheNextMultipleOf5.java b/src/main/java/kyu7/RoundToTheNextMultipleOf5.java index 075f823..043a72d 100644 --- a/src/main/java/kyu7/RoundToTheNextMultipleOf5.java +++ b/src/main/java/kyu7/RoundToTheNextMultipleOf5.java @@ -6,12 +6,8 @@ public class RoundToTheNextMultipleOf5 { //7 https://www.codewars.com/kata/55d1d6d5955ec6365400006d/train/java public static int roundToNext5(int number) { - int i = number % 5; - if (number < 0) { - return number - i; - } else { - return i == 0 ? number : number - i + 5; - } + int remainder = Math.floorMod(number, 5); + return remainder == 0 ? number : Math.addExact(number, 5 - remainder); } diff --git a/src/main/java/kyu7/SumOddNumbers.java b/src/main/java/kyu7/SumOddNumbers.java index eb8dffd..d1ec410 100644 --- a/src/main/java/kyu7/SumOddNumbers.java +++ b/src/main/java/kyu7/SumOddNumbers.java @@ -7,13 +7,11 @@ public class SumOddNumbers { //7 https://www.codewars.com/kata/55fd2d567d94ac3bc9000064/train/java public static int rowSumOddNumbers(int n) { - int result = 0; - int r = n * (n + 1) / 2 * 2 - 1; - for (int i = 0; i < n; i++) { - result += r - i * 2; + if (n <= 0) { + throw new IllegalArgumentException("row number must be positive"); } - return result; - // return n*n*n; + long square = Math.multiplyExact((long) n, n); + return Math.toIntExact(Math.multiplyExact(square, n)); } } diff --git a/src/main/java/kyu7/TriangleTester.java b/src/main/java/kyu7/TriangleTester.java index 02860b3..891f1d7 100644 --- a/src/main/java/kyu7/TriangleTester.java +++ b/src/main/java/kyu7/TriangleTester.java @@ -7,7 +7,10 @@ public class TriangleTester { //7 https://www.codewars.com/kata/56606694ec01347ce800001b/train/java public static boolean isTriangle(int a, int b, int c) { - return a > 0 && b > 0 && c > 0 && a + b > c && a + c > b && b + c > a; + return a > 0 && b > 0 && c > 0 + && (long) a + b > c + && (long) a + c > b + && (long) b + c > a; } } diff --git a/src/main/java/leetcode/BestTimeToBuyAndSellStock.java b/src/main/java/leetcode/BestTimeToBuyAndSellStock.java index 1829837..393aef0 100644 --- a/src/main/java/leetcode/BestTimeToBuyAndSellStock.java +++ b/src/main/java/leetcode/BestTimeToBuyAndSellStock.java @@ -15,19 +15,19 @@ public class BestTimeToBuyAndSellStock { */ public static int maxProfit1(int[] prices) { - var maxProfits = 0; - for (int i = 0; i < prices.length; i++) { - var currentMax = 0; - for (int j = i + 1; j < prices.length; j++) { - if (prices[j] > prices[i]) { - currentMax = Math.max(currentMax, prices[j] - prices[i]); - } else { - break; - } + if (prices == null) { + throw new IllegalArgumentException("prices must not be null"); + } + long minimumPrice = Long.MAX_VALUE; + long maximumProfit = 0L; + for (int price : prices) { + if (price < 0) { + throw new IllegalArgumentException("prices must not be negative"); } - maxProfits = Math.max(currentMax, maxProfits); + minimumPrice = Math.min(minimumPrice, price); + maximumProfit = Math.max(maximumProfit, price - minimumPrice); } - return maxProfits; + return Math.toIntExact(maximumProfit); } diff --git a/src/main/java/leetcode/MergeIntervals.java b/src/main/java/leetcode/MergeIntervals.java index 4a6ddf5..211fa51 100644 --- a/src/main/java/leetcode/MergeIntervals.java +++ b/src/main/java/leetcode/MergeIntervals.java @@ -42,7 +42,7 @@ public int[][] merge(int[][] intervals) { } private static int[] copyInterval(int[] interval) { - if (interval == null || interval.length < 2) { + if (interval == null || interval.length != 2 || interval[0] > interval[1]) { throw new IllegalArgumentException("Each interval must contain start and end values"); } return new int[]{interval[0], interval[1]}; diff --git a/src/main/java/other/AdjustCase.java b/src/main/java/other/AdjustCase.java index 6a65944..aca4865 100644 --- a/src/main/java/other/AdjustCase.java +++ b/src/main/java/other/AdjustCase.java @@ -12,7 +12,7 @@ public class AdjustCase { public String adjustCaseToLower(String string) { if (string == null || string.length() == 0) return string; - if (string.length() == 1) return string.toUpperCase(); + 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; @@ -23,7 +23,7 @@ public String adjustCaseStream(String string) { var lower = string.chars() .mapToObj(i -> (char) i) .map(Object::toString) - .map(String::toLowerCase) + .map(value -> value.toLowerCase(Locale.ROOT)) .collect(Collectors.joining()) .substring(1); return string.chars() @@ -31,7 +31,7 @@ public String adjustCaseStream(String string) { .map(Object::toString) .findFirst() .orElse("") - .toUpperCase() + lower; + .toUpperCase(Locale.ROOT) + lower; } public String adjustCaseFor(String string) { diff --git a/src/main/java/other/AllMostPopularityKyu8.java b/src/main/java/other/AllMostPopularityKyu8.java index 1756fa6..9025017 100644 --- a/src/main/java/other/AllMostPopularityKyu8.java +++ b/src/main/java/other/AllMostPopularityKyu8.java @@ -4,21 +4,22 @@ import static common.SafeParse.parseInt; import java.util.Arrays; +import java.util.Locale; public class AllMostPopularityKyu8 { public static int sumR(int[] arr) { - int rezult = 0; + long result = 0L; if (arr == null || arr.length == 0) { - return rezult; + return 0; } for (int num : arr) { if (num > 0) { - rezult += num; + result += num; } } - return rezult; + return Math.toIntExact(result); } public static String repeatStr(final int repeat, final String string) { @@ -26,12 +27,15 @@ public static String repeatStr(final int repeat, final String string) { } public static String removeFirstAndLastChar(String str) { + if (str == null || str.length() < 2) { + throw new IllegalArgumentException("input must contain at least two characters"); + } return str.substring(1, str.length() - 1); } public static int opposite(int number) { - return number * -1; + return Math.negateExact(number); } public static String evenOrOdd(int number) { @@ -45,7 +49,7 @@ public static int sumArray(int[] numbers) { } int max = Integer.MIN_VALUE; int min = Integer.MAX_VALUE; - int sum = 0; + long sum = 0L; for (int n : numbers) { sum += n; if (n > max) { @@ -55,7 +59,7 @@ public static int sumArray(int[] numbers) { min = n; } } - return sum - max - min; + return Math.toIntExact(sum - max - (long) min); } public static String fakeBin(String numberString) { @@ -84,11 +88,11 @@ public static String reverseWords(String str) { } public static int[] allTo2(int[] arr) { - return Arrays.stream(arr).map(x -> x * 2).toArray(); + return Arrays.stream(arr).map(x -> Math.multiplyExact(x, 2)).toArray(); } public static int[] invert(int[] array) { - return Arrays.stream(array).map(x -> x * -1).toArray(); + return Arrays.stream(array).map(Math::negateExact).toArray(); } public static int getAverage(int[] marks) { @@ -105,7 +109,7 @@ public static int[] countPositivesSumNegatives(int[] input) { if (i > 0) { countPositive++; } else if (i < 0) { - summNegative += i; + summNegative = Math.addExact(summNegative, i); } } int[] result = new int[2]; @@ -115,18 +119,27 @@ public static int[] countPositivesSumNegatives(int[] input) { } public static String abbrevName(String name) { - String[] s = name.split(" "); - return s[0].substring(0, 1).toUpperCase() + "." + s[1].substring(0, 1).toUpperCase(); + if (name == null || name.isBlank()) { + throw new IllegalArgumentException("name must contain exactly two non-empty parts"); + } + String[] s = name.trim().split("\\s+"); + if (s.length != 2 || s[0].isEmpty() || s[1].isEmpty()) { + throw new IllegalArgumentException("name must contain exactly two non-empty parts"); + } + return s[0].substring(0, 1).toUpperCase(Locale.ROOT) + + "." + s[1].substring(0, 1).toUpperCase(Locale.ROOT); } public static int timeInSeconds(int h, int m, int s) { - return s * 1000 + m * 60 * 1000 + h * 60 * 60 * 1000; + long milliseconds = s * 1_000L + m * 60_000L + h * 3_600_000L; + return Math.toIntExact(milliseconds); } public static int[] digitize(long n) { - String digits = Long.toString(Math.abs(n)); - int[] r = new int[digits.length()]; - for (int i = 0; i < digits.length(); i++) { + String digits = Long.toString(n); + int firstDigit = digits.charAt(0) == '-' ? 1 : 0; + int[] r = new int[digits.length() - firstDigit]; + for (int i = 0; i < r.length; i++) { r[i] = Character.digit(digits.charAt(digits.length() - 1 - i), 10); } return r; @@ -144,7 +157,7 @@ public static int summation(int n) { if (n <= 0) { return 0; } - return n * (n + 1) / 2; + return Math.toIntExact((long) n * (n + 1L) / 2L); } public static String noSpace(final String x) { @@ -192,15 +205,15 @@ public static int squareSum(int[] n) { if (n == null || n.length == 0) { return 0; } - int sum = 0; + long sum = 0L; for (int i : n) { - sum += i * i; + sum = Math.addExact(sum, Math.multiplyExact((long) i, i)); } - return sum; + return Math.toIntExact(sum); } public static int century(int number) { - return number <= 0 ? 0 : (number + 99) / 100; + return number <= 0 ? 0 : (number - 1) / 100 + 1; } public static int liters(double time) { @@ -219,10 +232,12 @@ public static Integer basicMath(String op, int v1, int v2) { throw new IllegalArgumentException(); } return switch (op) { - case "+" -> v1 + v2; - case "-" -> v1 - v2; - case "*" -> v1 * v2; - case "/" -> v1 / v2; + case "+" -> Math.addExact(v1, v2); + case "-" -> Math.subtractExact(v1, v2); + case "*" -> Math.multiplyExact(v1, v2); + case "/" -> v1 == Integer.MIN_VALUE && v2 == -1 + ? Math.negateExact(v1) + : v1 / v2; default -> throw new IllegalArgumentException(); }; } diff --git a/src/main/java/other/HowManyNumbers.java b/src/main/java/other/HowManyNumbers.java index bc3e118..b183df3 100644 --- a/src/main/java/other/HowManyNumbers.java +++ b/src/main/java/other/HowManyNumbers.java @@ -1,7 +1,6 @@ package other; -import java.util.ArrayList; import java.util.List; @@ -10,48 +9,39 @@ public class HowManyNumbers { - private static long sumDigits(long n) { - long result = 0; - while (n > 0) { - result = result + n % 10; - n /= 10; + public List findAll(final int sumDigits, final int numDigits) { + if (numDigits <= 0 || numDigits > 18 || sumDigits < 0) { + throw new IllegalArgumentException("numDigits must be between 1 and 18 and sumDigits must be non-negative"); } - return result; - } - - private static boolean digitsIncreasingOrder(long n) { - long temp = n % 10; - n /= 10; - while (n > 0) { - if (n % 10 > temp) return false; - temp = n % 10; - n /= 10; + if (sumDigits < numDigits || sumDigits > 9 * numDigits) { + return List.of(); } - return true; + long[] summary = new long[3]; + collect(0, numDigits, 1, sumDigits, 0L, summary); + return summary[0] == 0L ? List.of() : List.of(summary[0], summary[1], summary[2]); } - public List findAll(final int sumDigits, final int numDigits) { - long firstValue = 0L; - long lastValue = 0L; - long countValue = 0L; - long start = (long) Math.pow(10, numDigits - 1.0); - long finish = start * 10; - for (long i = start; i < finish; i++) { - if (digitsIncreasingOrder(i) && sumDigits(i) == sumDigits) { - countValue++; - lastValue = i; - if (firstValue == 0L) firstValue = i; + private static void collect(int position, int totalDigits, int minimumDigit, + int remainingSum, long value, long[] summary) { + if (position == totalDigits) { + if (remainingSum == 0) { + summary[0]++; + if (summary[0] == 1L) { + summary[1] = value; + } + summary[2] = value; } + return; } - - List result = new ArrayList<>(3); - if (countValue > 0L) { - result.add(countValue); - result.add(firstValue); - result.add(lastValue); + int positionsAfter = totalDigits - position - 1; + for (int digit = minimumDigit; digit <= 9; digit++) { + int nextSum = remainingSum - digit; + if (nextSum < digit * positionsAfter || nextSum > 9 * positionsAfter) { + continue; + } + long nextValue = Math.addExact(Math.multiplyExact(value, 10L), digit); + collect(position + 1, totalDigits, digit, nextSum, nextValue, summary); } - return result; } - } diff --git a/src/main/java/other/Permutations.java b/src/main/java/other/Permutations.java index 667d35b..1ba990b 100644 --- a/src/main/java/other/Permutations.java +++ b/src/main/java/other/Permutations.java @@ -28,6 +28,19 @@ private void arrOut(int[] pa) { } public void permutations(int[] pa, int i) { + if (pa == null) { + throw new IllegalArgumentException("array must not be null"); + } + if (i < 0 || i > pa.length || (pa.length > 0 && i == pa.length)) { + throw new IllegalArgumentException("start index is outside the array"); + } + if (i == 0) { + result.clear(); + } + if (pa.length == 0) { + result.add(new int[0]); + return; + } if (i == pa.length - 1) { arrOut(pa); } else { diff --git a/src/main/java/other/Persist.java b/src/main/java/other/Persist.java index 5a6e66d..aeec343 100644 --- a/src/main/java/other/Persist.java +++ b/src/main/java/other/Persist.java @@ -8,10 +8,13 @@ public class Persist { public int persistence(long n) { + if (n < 0L) { + throw new IllegalArgumentException("number must not be negative"); + } int result = 0; while (n > 9) { char[] s = String.valueOf(n).toCharArray(); - int temp = Character.digit(s[0], 10); + long temp = Character.digit(s[0], 10); for (int i = 1; i < s.length; i++) { temp *= Character.digit(s[i], 10); } diff --git a/src/main/java/other/SpinWords.java b/src/main/java/other/SpinWords.java index 6257105..e945632 100644 --- a/src/main/java/other/SpinWords.java +++ b/src/main/java/other/SpinWords.java @@ -7,19 +7,30 @@ public class SpinWords { public String spinWords(String sentence) { - String[] arrWords = sentence.split("\\s"); - StringBuilder resultSb = new StringBuilder(); - for (String curentWord : arrWords - ) { - if (curentWord.length() >= 5) { - StringBuilder currentSb = new StringBuilder(curentWord); - resultSb.append(currentSb.reverse()); + if (sentence == null) { + throw new IllegalArgumentException("sentence must not be null"); + } + StringBuilder result = new StringBuilder(sentence.length()); + int wordStart = 0; + while (wordStart < sentence.length()) { + if (Character.isWhitespace(sentence.charAt(wordStart))) { + result.append(sentence.charAt(wordStart++)); + continue; + } + int wordEnd = wordStart + 1; + while (wordEnd < sentence.length() && !Character.isWhitespace(sentence.charAt(wordEnd))) { + wordEnd++; + } + if (wordEnd - wordStart >= 5) { + for (int i = wordEnd - 1; i >= wordStart; i--) { + result.append(sentence.charAt(i)); + } } else { - resultSb.append(curentWord); + result.append(sentence, wordStart, wordEnd); } - resultSb.append(" "); + wordStart = wordEnd; } - return resultSb.toString().trim(); + return result.toString(); } } diff --git a/src/main/java/transactions/InMemoryValidationService.java b/src/main/java/transactions/InMemoryValidationService.java index 19f859b..a98f9f7 100644 --- a/src/main/java/transactions/InMemoryValidationService.java +++ b/src/main/java/transactions/InMemoryValidationService.java @@ -1,11 +1,12 @@ package transactions; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.Map; -import java.util.HashMap; +import java.util.Objects; +import java.util.Set; class InMemoryValidationService implements ValidationService { @@ -21,56 +22,62 @@ class InMemoryValidationService implements ValidationService { @Override public ValidationResult validate(List transactions, long balance) { - // Processes transactions in deterministic id order, marks duplicate ids - // and orderId-cascading invalid sequences, then applies BET/WIN business - // rules to the balance. if (transactions == null || transactions.isEmpty() || balance < 0) { return ValidationResult.empty(); } + if (transactions.stream().anyMatch(Objects::isNull)) { + throw new IllegalArgumentException("transactions must not contain null values"); + } List sortedTransactions = transactions.stream() .sorted() .toList(); - Set transactionsIds = new HashSet<>(); - Set failedOrderIds = new HashSet<>(); Set duplicateIds = findDuplicateIds(sortedTransactions); + Set failedOrderIds = findIntrinsicallyFailedOrders(sortedTransactions, duplicateIds); + while (true) { + SimulationResult simulation = simulate(sortedTransactions, balance, failedOrderIds); + if (simulation.failedOrderId() == null) { + return new ValidationResult(simulation.statuses()); + } + if (!failedOrderIds.add(simulation.failedOrderId())) { + throw new IllegalStateException("validation did not make progress"); + } + } + } - List transactionStatuses = new ArrayList<>(); - - for (Transaction transaction : sortedTransactions) { - boolean alreadyInvalid = transactionsIds.contains(transaction.id()) - || duplicateIds.contains(transaction.id()) - || failedOrderIds.contains(transaction.orderId()); - - if (alreadyInvalid) { - transactionStatuses.add(new TransactionStatus(transaction, false)); + private static Set findIntrinsicallyFailedOrders(List transactions, Set duplicateIds) { + Set failedOrderIds = new HashSet<>(); + for (Transaction transaction : transactions) { + if (duplicateIds.contains(transaction.id()) || transaction.amount() < 0 || transaction.type() == null) { failedOrderIds.add(transaction.orderId()); - transactionsIds.add(transaction.id()); - continue; } + } + return failedOrderIds; + } + private static SimulationResult simulate(List transactions, long initialBalance, + Set failedOrderIds) { + long balance = initialBalance; + List statuses = new ArrayList<>(transactions.size()); + for (Transaction transaction : transactions) { + if (failedOrderIds.contains(transaction.orderId())) { + statuses.add(new TransactionStatus(transaction, false)); + continue; + } if (transaction.type() == TransactionType.BET) { - boolean negativeBalance = balance < transaction.amount(); - - if (negativeBalance) { - transactionStatuses.add(new TransactionStatus(transaction, false)); - failedOrderIds.add(transaction.orderId()); - } else { - balance -= transaction.amount(); - transactionStatuses.add(new TransactionStatus(transaction, true)); + if (balance < transaction.amount()) { + return SimulationResult.failed(transaction.orderId()); } - } - - if (transaction.type() == TransactionType.WIN) { + balance -= transaction.amount(); + } else if (Long.MAX_VALUE - balance < transaction.amount()) { + return SimulationResult.failed(transaction.orderId()); + } else { balance += transaction.amount(); - transactionStatuses.add(new TransactionStatus(transaction, true)); } - transactionsIds.add(transaction.id()); - + statuses.add(new TransactionStatus(transaction, true)); } - - return new ValidationResult(transactionStatuses); + return SimulationResult.success(statuses); } private static Set findDuplicateIds(List transactions) { @@ -89,4 +96,15 @@ private static Set findDuplicateIds(List transactions) { return duplicateIds; } + + private record SimulationResult(List statuses, Long failedOrderId) { + + private static SimulationResult success(List statuses) { + return new SimulationResult(statuses, null); + } + + private static SimulationResult failed(long orderId) { + return new SimulationResult(List.of(), orderId); + } + } } diff --git a/src/main/java/transactions/ValidationResult.java b/src/main/java/transactions/ValidationResult.java index 48943b4..95019a1 100644 --- a/src/main/java/transactions/ValidationResult.java +++ b/src/main/java/transactions/ValidationResult.java @@ -9,7 +9,7 @@ final class ValidationResult { private final List transactions; public ValidationResult(List transactions) { - this.transactions = transactions; + this.transactions = List.copyOf(transactions); } public static ValidationResult empty() { diff --git a/src/test/java/interview/CallbackSchedulerTest.java b/src/test/java/interview/CallbackSchedulerTest.java index 791485a..b8b087c 100644 --- a/src/test/java/interview/CallbackSchedulerTest.java +++ b/src/test/java/interview/CallbackSchedulerTest.java @@ -1,5 +1,6 @@ package interview; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertTrue; import java.time.Instant; @@ -42,4 +43,12 @@ void shouldOrderPreparedTasksByExecutionTime() { assertTrue(earlier.compareTo(later) < 0); } + + @Test + void shouldSaturateDelayForTheMaximumInstant() { + try (CallbackScheduler scheduler = new CallbackScheduler()) { + assertDoesNotThrow(() -> scheduler.schedule(() -> { + }, Instant.MAX)); + } + } } diff --git a/src/test/java/kyu5/HumanReadableTimeTest.java b/src/test/java/kyu5/HumanReadableTimeTest.java index 08ef612..8ce8fbf 100644 --- a/src/test/java/kyu5/HumanReadableTimeTest.java +++ b/src/test/java/kyu5/HumanReadableTimeTest.java @@ -21,4 +21,12 @@ public class HumanReadableTimeTest { void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(kyu5.HumanReadableTime.class); } + + @Test + void shouldFormatBoundariesAndRejectOutOfRangeValues() { + assertEquals("00:00:00", makeReadable(0)); + assertEquals("99:59:59", makeReadable(359_999)); + assertThrows(IllegalArgumentException.class, () -> makeReadable(-1)); + assertThrows(IllegalArgumentException.class, () -> makeReadable(360_000)); + } } diff --git a/src/test/java/kyu5/MaximumSubarraySumTest.java b/src/test/java/kyu5/MaximumSubarraySumTest.java index 8b74c32..dbc51c0 100644 --- a/src/test/java/kyu5/MaximumSubarraySumTest.java +++ b/src/test/java/kyu5/MaximumSubarraySumTest.java @@ -21,4 +21,12 @@ public class MaximumSubarraySumTest { void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(kyu5.MaximumSubarraySum.class); } + + @Test + void shouldFindMaximumContiguousSumWithoutEnumeratingSubarrays() { + assertEquals(6, sequence(new int[]{-2, 1, -3, 4, -1, 2, 1, -5, 4})); + assertEquals(0, sequence(new int[]{-4, -2, -7})); + assertEquals(0, sequence(new int[0])); + assertThrows(ArithmeticException.class, () -> sequence(new int[]{Integer.MAX_VALUE, 1})); + } } diff --git a/src/test/java/kyu5/NumberOfTrailingZerosOfNTest.java b/src/test/java/kyu5/NumberOfTrailingZerosOfNTest.java index 22a3efa..b592c87 100644 --- a/src/test/java/kyu5/NumberOfTrailingZerosOfNTest.java +++ b/src/test/java/kyu5/NumberOfTrailingZerosOfNTest.java @@ -21,4 +21,17 @@ public class NumberOfTrailingZerosOfNTest { void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(kyu5.NumberOfTrailingZerosOfN.class); } + + @Test + void shouldCountFactorsOfFiveWithoutBuildingTheFactorial() { + assertEquals(0, zeros(0)); + assertEquals(1, zeros(5)); + assertEquals(6, zeros(25)); + assertTimeoutPreemptively(java.time.Duration.ofSeconds(1), () -> assertEquals(24, zeros(100))); + } + + @Test + void shouldRejectNegativeInput() { + assertThrows(IllegalArgumentException.class, () -> zeros(-1)); + } } diff --git a/src/test/java/kyu5/ProductFibTest.java b/src/test/java/kyu5/ProductFibTest.java index 658c081..aa7bd18 100644 --- a/src/test/java/kyu5/ProductFibTest.java +++ b/src/test/java/kyu5/ProductFibTest.java @@ -21,4 +21,20 @@ public class ProductFibTest { void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(kyu5.ProductFib.class); } + + @Test + void shouldFindExactAndNextFibonacciProducts() { + assertArrayEquals(new long[]{0, 1, 1}, productFib(0)); + assertArrayEquals(new long[]{21, 34, 1}, productFib(714)); + assertArrayEquals(new long[]{34, 55, 0}, productFib(800)); + assertArrayEquals(new long[]{2_971_215_073L, 4_807_526_976L, 0}, productFib(Long.MAX_VALUE)); + } + + @Test + void shouldCalculateFibonacciValuesExactlyWithinLongRange() { + assertEquals(12_586_269_025L, getFibonacciValue(50)); + assertEquals(7_540_113_804_746_346_429L, getFibonacciValue(92)); + assertThrows(ArithmeticException.class, () -> getFibonacciValue(93)); + assertThrows(IllegalArgumentException.class, () -> productFib(-1)); + } } diff --git a/src/test/java/kyu5/ScramblesTest.java b/src/test/java/kyu5/ScramblesTest.java index aa0a5d8..8a3a133 100644 --- a/src/test/java/kyu5/ScramblesTest.java +++ b/src/test/java/kyu5/ScramblesTest.java @@ -21,4 +21,12 @@ public class ScramblesTest { void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(kyu5.Scrambles.class); } + + @Test + void shouldConsumeEveryCharacterAtMostOnce() { + assertTrue(scramble("cedewaraaossoqqyt", "codewars")); + assertFalse(scramble("katas", "steak")); + assertFalse(scramble("a", "aa")); + assertThrows(IllegalArgumentException.class, () -> scramble(null, "a")); + } } diff --git a/src/test/java/kyu5/SumSquaredDivisorsTest.java b/src/test/java/kyu5/SumSquaredDivisorsTest.java index 79a615b..c0dfd88 100644 --- a/src/test/java/kyu5/SumSquaredDivisorsTest.java +++ b/src/test/java/kyu5/SumSquaredDivisorsTest.java @@ -1,8 +1,10 @@ package kyu5; 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; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -19,4 +21,12 @@ class SumSquaredDivisorsTest { void shouldListNumbersWhoseSquaredDivisorsSumToSquare(long from, long to, String expected) { assertEquals(expected, SumSquaredDivisors.listSquared(from, to)); } + + @Test + void shouldRejectInvalidOrUnrepresentableRangesWithoutLoopOverflow() { + assertThrows(IllegalArgumentException.class, () -> SumSquaredDivisors.listSquared(0, 1)); + assertThrows(IllegalArgumentException.class, () -> SumSquaredDivisors.listSquared(2, 1)); + assertThrows(ArithmeticException.class, + () -> SumSquaredDivisors.listSquared(Long.MAX_VALUE, Long.MAX_VALUE)); + } } diff --git a/src/test/java/kyu6/ArmstrongNumberTest.java b/src/test/java/kyu6/ArmstrongNumberTest.java index 269014e..f6c523a 100644 --- a/src/test/java/kyu6/ArmstrongNumberTest.java +++ b/src/test/java/kyu6/ArmstrongNumberTest.java @@ -19,7 +19,7 @@ void shouldAcceptNarcissisticNumbers(int number) { } @ParameterizedTest - @ValueSource(ints = {-1, 10, 100, 9475}) + @ValueSource(ints = {-1, 10, 100, 9475, Integer.MAX_VALUE}) void shouldRejectNonNarcissisticNumbers(int number) { assertFalse(ArmstrongNumber.isNarcissistic(number)); } diff --git a/src/test/java/kyu6/CountingDuplicatesTest.java b/src/test/java/kyu6/CountingDuplicatesTest.java index d7cb41d..3913007 100644 --- a/src/test/java/kyu6/CountingDuplicatesTest.java +++ b/src/test/java/kyu6/CountingDuplicatesTest.java @@ -21,4 +21,11 @@ public class CountingDuplicatesTest { void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(kyu6.CountingDuplicates.class); } + + @Test + void implementationsShouldBothIgnoreCase() { + assertEquals(2, duplicateCount("aA11")); + assertEquals(2, duplicateCountStream("aA11")); + assertEquals(2, duplicateCountStream("Indivisibilities")); + } } diff --git a/src/test/java/kyu6/DigPowTest.java b/src/test/java/kyu6/DigPowTest.java index 381325a..d489160 100644 --- a/src/test/java/kyu6/DigPowTest.java +++ b/src/test/java/kyu6/DigPowTest.java @@ -21,4 +21,15 @@ public class DigPowTest { void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(kyu6.DigPow.class); } + + @Test + void shouldMatchExamplesAndRejectInvalidOrOverflowingInputs() { + assertEquals(1, digPow(89, 1)); + assertEquals(-1, digPow(92, 1)); + assertEquals(2, digPow(695, 2)); + assertEquals(51, digPow(46288, 3)); + assertThrows(IllegalArgumentException.class, () -> digPow(0, 1)); + assertThrows(IllegalArgumentException.class, () -> digPow(89, 0)); + assertThrows(ArithmeticException.class, () -> digPow(9, Integer.MAX_VALUE)); + } } diff --git a/src/test/java/kyu6/FindEvenIndexTest.java b/src/test/java/kyu6/FindEvenIndexTest.java index 9a0382c..ca3da6c 100644 --- a/src/test/java/kyu6/FindEvenIndexTest.java +++ b/src/test/java/kyu6/FindEvenIndexTest.java @@ -21,4 +21,19 @@ public class FindEvenIndexTest { void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(kyu6.FindEvenIndex.class); } + + @Test + void implementationsShouldHandleNegativeSumsAndOverflow() { + int[][] inputs = { + {20, 10, -80, 10, 10, 15, 35}, + {1, 2, 3, 4, 3, 2, 1}, + {Integer.MAX_VALUE, 0, Integer.MAX_VALUE} + }; + int[] expected = {0, 3, 1}; + + for (int i = 0; i < inputs.length; i++) { + assertEquals(expected[i], findEvenIndexV1(inputs[i])); + assertEquals(expected[i], findEvenIndexV2(inputs[i])); + } + } } diff --git a/src/test/java/kyu6/PhoneNumberTest.java b/src/test/java/kyu6/PhoneNumberTest.java index 931cf30..5d1f221 100644 --- a/src/test/java/kyu6/PhoneNumberTest.java +++ b/src/test/java/kyu6/PhoneNumberTest.java @@ -21,4 +21,13 @@ public class PhoneNumberTest { void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(kyu6.PhoneNumber.class); } + + @Test + void shouldFormatAndValidateExactlyTenDigits() { + assertEquals("(123) 456-7890", createPhoneNumber(new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 0})); + assertThrows(IllegalArgumentException.class, () -> createPhoneNumber(null)); + assertThrows(IllegalArgumentException.class, () -> createPhoneNumber(new int[9])); + assertThrows(IllegalArgumentException.class, + () -> createPhoneNumber(new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10})); + } } diff --git a/src/test/java/kyu6/SimpleEncryptionTest.java b/src/test/java/kyu6/SimpleEncryptionTest.java index bf022b3..2eb7eaf 100644 --- a/src/test/java/kyu6/SimpleEncryptionTest.java +++ b/src/test/java/kyu6/SimpleEncryptionTest.java @@ -21,4 +21,25 @@ public class SimpleEncryptionTest { void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(kyu6.SimpleEncryption.class); } + + @Test + void shouldRoundTripWithoutRecursiveStackGrowth() { + String plainText = "This is a test!"; + String encrypted = encrypt(plainText, 10_000); + + assertEquals(plainText, decrypt(encrypted, 10_000)); + assertNull(encrypt(null, 3)); + assertEquals("", decrypt("", 3)); + } + + @Test + void shouldMatchExamplesAndHandleMaximumIterationCountEfficiently() { + assertEquals("hsi etTi sats!", encrypt("This is a test!", 1)); + assertEquals("s eT ashi tist!", encrypt("This is a test!", 2)); + assertEquals("This is a test!", decrypt("hsi etTi sats!", 1)); + assertEquals("This is a test!", decrypt("s eT ashi tist!", 2)); + + String encrypted = encrypt("bounded permutation", Integer.MAX_VALUE); + assertEquals("bounded permutation", decrypt(encrypted, Integer.MAX_VALUE)); + } } diff --git a/src/test/java/kyu6/SortTheOddTest.java b/src/test/java/kyu6/SortTheOddTest.java index aa327dc..496e02c 100644 --- a/src/test/java/kyu6/SortTheOddTest.java +++ b/src/test/java/kyu6/SortTheOddTest.java @@ -21,4 +21,19 @@ public class SortTheOddTest { void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(kyu6.SortTheOdd.class); } + + @Test + void shouldSortOddValuesAndLeaveEvenValuesInPlace() { + int[] input = {5, 3, 2, 8, 1, 4}; + int[] expected = {1, 3, 2, 8, 5, 4}; + + assertArrayEquals(expected, sortArray(input)); + assertArrayEquals(new int[]{5, 3, 2, 8, 1, 4}, input); + assertArrayEquals(expected, sortArrayStream(input)); + } + + @Test + void shouldRecognizeNegativeOddValues() { + assertArrayEquals(new int[]{-5, 2, -3}, sortArray(new int[]{-3, 2, -5})); + } } diff --git a/src/test/java/kyu6/TheSupermarketQueueTest.java b/src/test/java/kyu6/TheSupermarketQueueTest.java index eb07b5d..7f8f3f1 100644 --- a/src/test/java/kyu6/TheSupermarketQueueTest.java +++ b/src/test/java/kyu6/TheSupermarketQueueTest.java @@ -21,4 +21,17 @@ public class TheSupermarketQueueTest { void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(kyu6.TheSupermarketQueue.class); } + + @Test + void shouldQueueAllCustomersEvenWhenEveryServiceTimeIsShorterThanTillCount() { + assertEquals(3, solveSuperMarketQueue(new int[]{1, 1, 1, 1, 1}, 2)); + assertEquals(9, solveSuperMarketQueue(new int[]{2, 2, 3, 3, 4, 4}, 2)); + } + + @Test + void shouldRejectInvalidQueueConfiguration() { + assertThrows(IllegalArgumentException.class, () -> solveSuperMarketQueue(null, 1)); + assertThrows(IllegalArgumentException.class, () -> solveSuperMarketQueue(new int[]{1}, 0)); + assertThrows(IllegalArgumentException.class, () -> solveSuperMarketQueue(new int[]{-1}, 1)); + } } diff --git a/src/test/java/kyu6/TribonacciSequenceTest.java b/src/test/java/kyu6/TribonacciSequenceTest.java index f908ea8..c308ca7 100644 --- a/src/test/java/kyu6/TribonacciSequenceTest.java +++ b/src/test/java/kyu6/TribonacciSequenceTest.java @@ -1,6 +1,7 @@ package kyu6; import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -24,4 +25,11 @@ void shouldBuildSequenceFromSignature() { TribonacciSequence.tribonacci(new double[]{1, 1, 1}, 10) ); } + + @Test + 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)); + } } diff --git a/src/test/java/kyu6/TwoSumTest.java b/src/test/java/kyu6/TwoSumTest.java index 5710cbc..f4aba6d 100644 --- a/src/test/java/kyu6/TwoSumTest.java +++ b/src/test/java/kyu6/TwoSumTest.java @@ -33,6 +33,10 @@ static Stream basicTests() { @Test void shouldReturnEmptyIfNoPair() { assertArrayEquals(new int[0], twoSum(new int[]{1, 2, 3}, 100)); + assertArrayEquals( + new int[0], + twoSum(new int[]{Integer.MAX_VALUE, Integer.MAX_VALUE}, -2) + ); } @Test diff --git a/src/test/java/kyu6/toCamelCaseTest.java b/src/test/java/kyu6/toCamelCaseTest.java index f7f71ca..edb5c35 100644 --- a/src/test/java/kyu6/toCamelCaseTest.java +++ b/src/test/java/kyu6/toCamelCaseTest.java @@ -1,5 +1,7 @@ package kyu6; +import java.util.Locale; + import net.jqwik.api.ForAll; import net.jqwik.api.constraints.AlphaChars; import net.jqwik.api.constraints.StringLength; @@ -32,7 +34,10 @@ void shouldHandleEmptyInput() { @Test void shouldFailOnEmptySegments() { - assertThrows(RuntimeException.class, () -> toCamelCase("--")); + assertThrows(IllegalArgumentException.class, () -> toCamelCase("--")); + assertThrows(IllegalArgumentException.class, () -> toCamelCase("word-")); + assertThrows(IllegalArgumentException.class, () -> toCamelCase("-word")); + assertThrows(IllegalArgumentException.class, () -> toCamelCase("two--words")); } @Property @@ -40,7 +45,7 @@ void shouldFailOnEmptySegments() { void shouldHandleWordPairWithSingleSeparator(@ForAll @AlphaChars @StringLength(min = 1, max = 12) String head, @ForAll @AlphaChars @StringLength(min = 1, max = 12) String tail) { String input = head + "-" + tail; - assertEquals(head + tail.substring(0, 1).toUpperCase() + tail.substring(1), toCamelCase(input)); + assertEquals(head + tail.substring(0, 1).toUpperCase(Locale.ROOT) + tail.substring(1), toCamelCase(input)); assertNotNull(toCamelCase(input)); } diff --git a/src/test/java/kyu7/AccumulTest.java b/src/test/java/kyu7/AccumulTest.java index 973c43f..400201d 100644 --- a/src/test/java/kyu7/AccumulTest.java +++ b/src/test/java/kyu7/AccumulTest.java @@ -21,4 +21,12 @@ public class AccumulTest { void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(kyu7.Accumul.class); } + + @Test + void implementationsShouldHandleEmptyAndMixedCaseInputConsistently() { + assertEquals("", accum("")); + assertEquals("", streamAccum("")); + assertEquals("R-Qq-Aaa", accum("RqA")); + assertEquals(streamAccum("RqA"), accum("RqA")); + } } diff --git a/src/test/java/kyu7/CountTheDigitTest.java b/src/test/java/kyu7/CountTheDigitTest.java index 803f7ec..73836dd 100644 --- a/src/test/java/kyu7/CountTheDigitTest.java +++ b/src/test/java/kyu7/CountTheDigitTest.java @@ -21,4 +21,11 @@ public class CountTheDigitTest { void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(kyu7.CountTheDigit.class); } + + @Test + void shouldSquareUsingLongArithmetic() { + assertEquals(54_139, nbDig(50_000, 2)); + assertThrows(IllegalArgumentException.class, () -> nbDig(-1, 2)); + assertThrows(IllegalArgumentException.class, () -> nbDig(10, 10)); + } } diff --git a/src/test/java/kyu7/DontGiveMeFiveTest.java b/src/test/java/kyu7/DontGiveMeFiveTest.java index c5112ff..b8e433a 100644 --- a/src/test/java/kyu7/DontGiveMeFiveTest.java +++ b/src/test/java/kyu7/DontGiveMeFiveTest.java @@ -21,4 +21,24 @@ public class DontGiveMeFiveTest { void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(kyu7.DontGiveMeFive.class); } + + @Test + void shouldCountWithoutIteratingAcrossTheWholeRange() { + assertEquals(8, dontGiveMeFive(1, 9)); + assertEquals(12, dontGiveMeFive(4, 17)); + assertEquals(9, dontGiveMeFive(-5, 5)); + assertEquals(1, dontGiveMeFive(Integer.MAX_VALUE, Integer.MAX_VALUE)); + assertThrows(IllegalArgumentException.class, () -> dontGiveMeFive(2, 1)); + assertEquals(1_680_985_958, dontGiveMeFive(Integer.MIN_VALUE, Integer.MAX_VALUE)); + + for (int start = -100; start <= 100; start++) { + int expected = 0; + for (int end = start; end <= 100; end++) { + if (!Integer.toString(end).contains("5")) { + expected++; + } + assertEquals(expected, dontGiveMeFive(start, end)); + } + } + } } diff --git a/src/test/java/kyu7/FindDivisorTest.java b/src/test/java/kyu7/FindDivisorTest.java index d8e4717..979bd53 100644 --- a/src/test/java/kyu7/FindDivisorTest.java +++ b/src/test/java/kyu7/FindDivisorTest.java @@ -21,4 +21,11 @@ public class FindDivisorTest { void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(kyu7.FindDivisor.class); } + + @Test + void shouldHandleSquaresAndTheLargestInt() { + assertEquals(9, numberOFindDivisorivisors(36)); + assertEquals(2, numberOFindDivisorivisors(Integer.MAX_VALUE)); + assertThrows(IllegalArgumentException.class, () -> numberOFindDivisorivisors(0)); + } } diff --git a/src/test/java/kyu7/GetSumTest.java b/src/test/java/kyu7/GetSumTest.java index bf7a1a1..f0ae2f8 100644 --- a/src/test/java/kyu7/GetSumTest.java +++ b/src/test/java/kyu7/GetSumTest.java @@ -21,4 +21,11 @@ public class GetSumTest { void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(kyu7.GetSum.class); } + + @Test + void shouldUseWideIntermediatesForBoundaryRanges() { + assertEquals(Integer.MIN_VALUE, getSum(Integer.MIN_VALUE, Integer.MAX_VALUE)); + assertEquals(3, getSum(-2, 3)); + assertThrows(ArithmeticException.class, () -> getSum(Integer.MAX_VALUE - 1, Integer.MAX_VALUE)); + } } diff --git a/src/test/java/kyu7/JadenCaseTest.java b/src/test/java/kyu7/JadenCaseTest.java index 565051a..97a33ce 100644 --- a/src/test/java/kyu7/JadenCaseTest.java +++ b/src/test/java/kyu7/JadenCaseTest.java @@ -21,4 +21,12 @@ public class JadenCaseTest { void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(kyu7.JadenCase.class); } + + @Test + void shouldCapitalizeWordsWithoutDestroyingWhitespace() { + assertEquals("How Can Mirrors Be Real", toJadenCase("How can mirrors be real")); + assertEquals(" Multiple Words\tStay ", toJadenCase(" multiple words\tstay ")); + assertNull(toJadenCase(null)); + assertNull(toJadenCase("")); + } } diff --git a/src/test/java/kyu7/MaxMultipleTest.java b/src/test/java/kyu7/MaxMultipleTest.java index d805774..de98ab3 100644 --- a/src/test/java/kyu7/MaxMultipleTest.java +++ b/src/test/java/kyu7/MaxMultipleTest.java @@ -21,4 +21,11 @@ public class MaxMultipleTest { void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(kyu7.MaxMultiple.class); } + + @Test + void shouldCalculateDirectlyForLargeBounds() { + assertEquals(1_999_999_998, maxMultiple(3, 2_000_000_000)); + assertEquals(15, maxMultiple(5, 17)); + assertThrows(IllegalArgumentException.class, () -> maxMultiple(0, 10)); + } } diff --git a/src/test/java/kyu7/MoneyTest.java b/src/test/java/kyu7/MoneyTest.java index 8e35d06..0445fb3 100644 --- a/src/test/java/kyu7/MoneyTest.java +++ b/src/test/java/kyu7/MoneyTest.java @@ -1,8 +1,10 @@ package kyu7; 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; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -24,4 +26,11 @@ void shouldCalculateYearsNeededForDesiredPrincipal( ) { assertEquals(expectedYears, Money.calculateYears(principal, interest, tax, desired)); } + + @Test + void shouldRejectInputsThatCanNeverReachTheTarget() { + assertThrows(IllegalArgumentException.class, () -> Money.calculateYears(1_000, 0.0, 0.18, 1_001)); + assertThrows(IllegalArgumentException.class, () -> Money.calculateYears(1_000, 0.05, 1.0, 1_001)); + assertThrows(IllegalArgumentException.class, () -> Money.calculateYears(Double.NaN, 0.05, 0.18, 1_001)); + } } diff --git a/src/test/java/kyu7/NbYearTest.java b/src/test/java/kyu7/NbYearTest.java index 5bf1d36..e45553a 100644 --- a/src/test/java/kyu7/NbYearTest.java +++ b/src/test/java/kyu7/NbYearTest.java @@ -21,4 +21,11 @@ public class NbYearTest { void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(kyu7.NbYear.class); } + + @Test + void shouldCalculateGrowthAndRejectAnUnreachableTarget() { + assertEquals(15, nbYear(1_500, 5.0, 100, 5_000)); + assertThrows(IllegalArgumentException.class, () -> nbYear(1_000, 0.0, 0, 1_001)); + assertThrows(IllegalArgumentException.class, () -> nbYear(1_000, Double.NaN, 1, 1_001)); + } } diff --git a/src/test/java/kyu7/NumberFunTest.java b/src/test/java/kyu7/NumberFunTest.java index 191f96d..4f2fccc 100644 --- a/src/test/java/kyu7/NumberFunTest.java +++ b/src/test/java/kyu7/NumberFunTest.java @@ -21,4 +21,12 @@ public class NumberFunTest { void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(kyu7.NumberFun.class); } + + @Test + void shouldHandleLongBoundariesWithoutFloatingPointMisclassification() { + assertEquals(1, findNextSquare(0)); + assertEquals(9_223_372_030_926_249_001L, findNextSquare(9_223_372_024_852_248_004L)); + assertEquals(-1, findNextSquare(Long.MAX_VALUE)); + assertThrows(ArithmeticException.class, () -> findNextSquare(9_223_372_030_926_249_001L)); + } } diff --git a/src/test/java/kyu7/ReverseWordsTest.java b/src/test/java/kyu7/ReverseWordsTest.java index 36d8a7b..fbc10c3 100644 --- a/src/test/java/kyu7/ReverseWordsTest.java +++ b/src/test/java/kyu7/ReverseWordsTest.java @@ -21,4 +21,15 @@ public class ReverseWordsTest { void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(kyu7.ReverseWords.class); } + + @Test + void shouldPreserveLeadingTrailingAndRepeatedWhitespace() { + assertEquals(" cba\t fed ", reverseWords(" abc\t def ")); + assertEquals("", reverseWords("")); + } + + @Test + void shouldRejectNullInput() { + assertThrows(IllegalArgumentException.class, () -> reverseWords(null)); + } } diff --git a/src/test/java/kyu7/RoundToTheNextMultipleOf5Test.java b/src/test/java/kyu7/RoundToTheNextMultipleOf5Test.java index 926087e..f23d82d 100644 --- a/src/test/java/kyu7/RoundToTheNextMultipleOf5Test.java +++ b/src/test/java/kyu7/RoundToTheNextMultipleOf5Test.java @@ -21,4 +21,12 @@ public class RoundToTheNextMultipleOf5Test { void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(kyu7.RoundToTheNextMultipleOf5.class); } + + @Test + void shouldRoundNegativeValuesUpAndReportUnrepresentableResults() { + assertEquals(0, roundToNext5(-1)); + assertEquals(-5, roundToNext5(-6)); + assertEquals(15, roundToNext5(12)); + assertThrows(ArithmeticException.class, () -> roundToNext5(Integer.MAX_VALUE)); + } } diff --git a/src/test/java/kyu7/SumOddNumbersPropertyTest.java b/src/test/java/kyu7/SumOddNumbersPropertyTest.java index 75382c3..211226d 100644 --- a/src/test/java/kyu7/SumOddNumbersPropertyTest.java +++ b/src/test/java/kyu7/SumOddNumbersPropertyTest.java @@ -6,8 +6,11 @@ import net.jqwik.api.constraints.IntRange; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static kyu7.SumOddNumbers.rowSumOddNumbers; +import org.junit.jupiter.api.Test; + @Tag("property") class SumOddNumbersPropertyTest { @@ -17,4 +20,10 @@ void rowSumOddNumbersMatchesCube(@ForAll @IntRange(min = 1, max = 25) int n) { int expected = n * n * n; assertEquals(expected, actual); } + + @Test + void shouldRejectInvalidAndUnrepresentableRows() { + assertThrows(IllegalArgumentException.class, () -> rowSumOddNumbers(0)); + assertThrows(ArithmeticException.class, () -> rowSumOddNumbers(1291)); + } } diff --git a/src/test/java/kyu7/TriangleTesterTest.java b/src/test/java/kyu7/TriangleTesterTest.java index 0ad68eb..74e3ce8 100644 --- a/src/test/java/kyu7/TriangleTesterTest.java +++ b/src/test/java/kyu7/TriangleTesterTest.java @@ -21,4 +21,10 @@ public class TriangleTesterTest { void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(kyu7.TriangleTester.class); } + + @Test + void shouldNotOverflowWhenSidesAreLarge() { + assertTrue(isTriangle(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE)); + assertFalse(isTriangle(Integer.MAX_VALUE, 1, Integer.MAX_VALUE - 1)); + } } diff --git a/src/test/java/leetcode/BestTimeToBuyAndSellStockTest.java b/src/test/java/leetcode/BestTimeToBuyAndSellStockTest.java index 3199c96..4aa91e5 100644 --- a/src/test/java/leetcode/BestTimeToBuyAndSellStockTest.java +++ b/src/test/java/leetcode/BestTimeToBuyAndSellStockTest.java @@ -21,4 +21,12 @@ public class BestTimeToBuyAndSellStockTest { void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(leetcode.BestTimeToBuyAndSellStock.class); } + + @Test + void shouldTrackTheLowestEarlierPriceInOnePass() { + assertEquals(5, maxProfit1(new int[]{7, 1, 5, 3, 6, 4})); + assertEquals(0, maxProfit1(new int[]{7, 6, 4, 3, 1})); + assertThrows(IllegalArgumentException.class, () -> maxProfit1(null)); + assertThrows(IllegalArgumentException.class, () -> maxProfit1(new int[]{1, -1, 2})); + } } diff --git a/src/test/java/leetcode/MergeIntervalsTest.java b/src/test/java/leetcode/MergeIntervalsTest.java index a346dc4..fd955da 100644 --- a/src/test/java/leetcode/MergeIntervalsTest.java +++ b/src/test/java/leetcode/MergeIntervalsTest.java @@ -38,5 +38,7 @@ void shouldRejectInvalidInput() { assertThrows(IllegalArgumentException.class, () -> merger.merge(null)); assertThrows(IllegalArgumentException.class, () -> merger.merge(new int[][]{null})); assertThrows(IllegalArgumentException.class, () -> merger.merge(new int[][]{{1}})); + assertThrows(IllegalArgumentException.class, () -> merger.merge(new int[][]{{2, 1}})); + assertThrows(IllegalArgumentException.class, () -> merger.merge(new int[][]{{1, 2, 3}})); } } diff --git a/src/test/java/other/AllMostPopularityKyu8Test.java b/src/test/java/other/AllMostPopularityKyu8Test.java index 831986e..4e569a3 100644 --- a/src/test/java/other/AllMostPopularityKyu8Test.java +++ b/src/test/java/other/AllMostPopularityKyu8Test.java @@ -37,6 +37,10 @@ void shouldCoverArrayHelpers() { assertArrayEquals(new int[0], AllMostPopularityKyu8.countPositivesSumNegatives(null)); assertArrayEquals(new int[]{0}, AllMostPopularityKyu8.digitize(0)); assertArrayEquals(new int[]{1, 3, 2, 5, 3}, AllMostPopularityKyu8.digitize(35_231)); + assertArrayEquals( + new int[]{8, 0, 8, 5, 7, 7, 4, 5, 8, 6, 3, 0, 2, 7, 3, 3, 2, 2, 9}, + AllMostPopularityKyu8.digitize(Long.MIN_VALUE) + ); } @Test @@ -67,6 +71,7 @@ void shouldCoverSearchAndCountingHelpers() { "1905, 20", "2000, 20", "2001, 21", + "2147483647, 21474837", "0, 0" }) void shouldCalculateCentury(int year, int century) { @@ -84,4 +89,22 @@ void shouldCoverRemainingSmallHelpers() { assertEquals(3, AllMostPopularityKyu8.basicMath("/", 12, 4)); assertThrows(IllegalArgumentException.class, () -> AllMostPopularityKyu8.basicMath("/", 1, 0)); } + + @Test + void shouldFailWhenAnIntegerResultCannotBeRepresented() { + assertThrows(ArithmeticException.class, () -> AllMostPopularityKyu8.opposite(Integer.MIN_VALUE)); + assertThrows(ArithmeticException.class, + () -> AllMostPopularityKyu8.sumR(new int[]{Integer.MAX_VALUE, 1})); + assertThrows(ArithmeticException.class, + () -> AllMostPopularityKyu8.sumArray( + new int[]{Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE, 0})); + assertThrows(ArithmeticException.class, + () -> AllMostPopularityKyu8.timeInSeconds(Integer.MAX_VALUE, 0, 0)); + assertThrows(ArithmeticException.class, + () -> AllMostPopularityKyu8.squareSum(new int[]{Integer.MAX_VALUE})); + assertThrows(ArithmeticException.class, + () -> AllMostPopularityKyu8.basicMath("+", Integer.MAX_VALUE, 1)); + assertThrows(IllegalArgumentException.class, () -> AllMostPopularityKyu8.abbrevName(null)); + assertThrows(IllegalArgumentException.class, () -> AllMostPopularityKyu8.removeFirstAndLastChar("x")); + } } diff --git a/src/test/java/other/HowManyNumbersTest.java b/src/test/java/other/HowManyNumbersTest.java index 1003373..ae5bb48 100644 --- a/src/test/java/other/HowManyNumbersTest.java +++ b/src/test/java/other/HowManyNumbersTest.java @@ -21,4 +21,16 @@ public class HowManyNumbersTest { void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(other.HowManyNumbers.class); } + + @Test + void shouldGenerateOnlyNonDecreasingDigitCombinations() { + other.HowManyNumbers numbers = new other.HowManyNumbers(); + + assertEquals(List.of(8L, 118L, 334L), numbers.findAll(10, 3)); + assertEquals(List.of(1L, 999L, 999L), numbers.findAll(27, 3)); + assertEquals(List.of(), numbers.findAll(84, 4)); + assertEquals(List.of(123L, 116999L, 566666L), numbers.findAll(35, 6)); + assertEquals(List.of(1L, 111111111111111111L, 111111111111111111L), numbers.findAll(18, 18)); + assertThrows(IllegalArgumentException.class, () -> numbers.findAll(10, 19)); + } } diff --git a/src/test/java/other/PermutationsTest.java b/src/test/java/other/PermutationsTest.java index 93bcdcf..72b6817 100644 --- a/src/test/java/other/PermutationsTest.java +++ b/src/test/java/other/PermutationsTest.java @@ -21,4 +21,16 @@ public class PermutationsTest { void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(other.Permutations.class); } + + @Test + void shouldResetResultsBetweenTopLevelInvocations() { + other.Permutations permutations = new other.Permutations(); + permutations.permutations(new int[]{1, 2}, 0); + assertEquals(2, permutations.result().size()); + + permutations.permutations(new int[]{3}, 0); + assertEquals(1, permutations.result().size()); + assertArrayEquals(new int[]{33}, permutations.result().get(0)); + assertThrows(IllegalArgumentException.class, () -> permutations.permutations(new int[]{1}, 1)); + } } diff --git a/src/test/java/other/PersistTest.java b/src/test/java/other/PersistTest.java index 84cdaa3..5592561 100644 --- a/src/test/java/other/PersistTest.java +++ b/src/test/java/other/PersistTest.java @@ -1,6 +1,7 @@ package other; 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.params.ParameterizedTest; @@ -23,4 +24,10 @@ class PersistTest { void shouldCountMultiplicativePersistence(long number, int expectedCount) { assertEquals(expectedCount, persist.persistence(number)); } + + @org.junit.jupiter.api.Test + void shouldUseLongForIntermediateDigitProducts() { + assertEquals(2, persist.persistence(8_999_999_999_999_999_999L)); + assertThrows(IllegalArgumentException.class, () -> persist.persistence(-1)); + } } diff --git a/src/test/java/other/SpinWordsTest.java b/src/test/java/other/SpinWordsTest.java index fed1218..f1c46db 100644 --- a/src/test/java/other/SpinWordsTest.java +++ b/src/test/java/other/SpinWordsTest.java @@ -21,4 +21,13 @@ public class SpinWordsTest { void smokeTestsShouldExecuteApi() { quality.SmokeMethodTestHarness.verify(other.SpinWords.class); } + + @Test + void shouldReverseLongWordsAndPreserveWhitespace() { + other.SpinWords spinWords = new other.SpinWords(); + + assertEquals("Hey wollef sroirraw", spinWords.spinWords("Hey fellow warriors")); + assertEquals(" emocleW\tto the elgnuj ", spinWords.spinWords(" Welcome\tto the jungle ")); + assertThrows(IllegalArgumentException.class, () -> spinWords.spinWords(null)); + } } diff --git a/src/test/java/quality/SmokeMethodTestHarness.java b/src/test/java/quality/SmokeMethodTestHarness.java index 7a96e58..0b931ef 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(summary.startsWith(clazz.getSimpleName()), () -> "Unexpected smoke summary: " + summary); + Assertions.assertTrue(failed < attempted, () -> "Every executable smoke path failed: " + summary); } private static Object resolveTargetInstance(Method method, Class clazz) throws Exception { @@ -146,7 +146,7 @@ private static Object defaultValueFor(Class type) { if (type.isArray()) { Class componentType = type.getComponentType(); if (componentType == int.class) { - return new int[]{1, 2, 3}; + return new int[10]; } if (componentType == long.class) { return new long[]{1L, 2L}; diff --git a/src/test/java/quality/SmokeMethodTestHarnessTest.java b/src/test/java/quality/SmokeMethodTestHarnessTest.java new file mode 100644 index 0000000..8fef2ae --- /dev/null +++ b/src/test/java/quality/SmokeMethodTestHarnessTest.java @@ -0,0 +1,35 @@ +package quality; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class SmokeMethodTestHarnessTest { + + @Test + void rejectsClassesWhoseEveryExecutablePathFails() { + assertThrows(AssertionError.class, () -> SmokeMethodTestHarness.verify(AlwaysFails.class)); + } + + @Test + void acceptsAClassWhenAtLeastOneExecutablePathSucceeds() { + assertDoesNotThrow(() -> SmokeMethodTestHarness.verify(PartiallyWorks.class)); + } + + private static final class AlwaysFails { + static void fail(int ignored) { + throw new IllegalStateException("expected failure"); + } + } + + private static final class PartiallyWorks { + static void fail(int ignored) { + throw new IllegalStateException("expected failure"); + } + + static int succeed(int value) { + return value; + } + } +} diff --git a/src/test/java/transactions/InMemoryValidationServiceTest.java b/src/test/java/transactions/InMemoryValidationServiceTest.java index 05cf848..164cd2a 100644 --- a/src/test/java/transactions/InMemoryValidationServiceTest.java +++ b/src/test/java/transactions/InMemoryValidationServiceTest.java @@ -8,6 +8,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -81,4 +82,60 @@ void shouldProcessTransactionsByIdWhenInputIsUnordered() { assertFalse(actual.getTransactions().get(1).status()); assertTrue(actual.getTransactions().get(2).status()); } + + @Test + void shouldRejectInvalidAmountsAndBalanceOverflowAndCascadeByOrder() { + List transactions = List.of( + createTransaction(1, 10, -1, TransactionType.BET), + createTransaction(2, 10, 1, TransactionType.WIN), + createTransaction(3, 20, 1, TransactionType.WIN) + ); + + ValidationResult invalidAmount = service.validate(transactions, 10); + assertFalse(invalidAmount.getTransactions().get(0).status()); + assertFalse(invalidAmount.getTransactions().get(1).status()); + assertTrue(invalidAmount.getTransactions().get(2).status()); + + ValidationResult overflow = service.validate( + List.of(createTransaction(1, 30, 1, TransactionType.WIN)), + Long.MAX_VALUE + ); + assertFalse(overflow.getTransactions().get(0).status()); + } + + @Test + void shouldInvalidateEarlierTransactionsAndRemoveTheirBalanceEffectsWhenOrderFailsLater() { + ValidationResult intrinsicFailure = service.validate(List.of( + createTransaction(1, 10, 50, TransactionType.BET), + createTransaction(2, 20, 50, TransactionType.BET), + createTransaction(3, 10, -1, TransactionType.WIN) + ), 50); + + assertFalse(intrinsicFailure.getTransactions().get(0).status()); + assertTrue(intrinsicFailure.getTransactions().get(1).status()); + assertFalse(intrinsicFailure.getTransactions().get(2).status()); + + ValidationResult balanceFailure = service.validate(List.of( + createTransaction(1, 30, 80, TransactionType.BET), + createTransaction(2, 40, 10, TransactionType.WIN), + createTransaction(3, 30, 31, TransactionType.BET) + ), 100); + + assertFalse(balanceFailure.getTransactions().get(0).status()); + assertTrue(balanceFailure.getTransactions().get(1).status()); + assertFalse(balanceFailure.getTransactions().get(2).status()); + } + + @Test + void shouldRejectNullEntriesAndExposeAnImmutableResult() { + List withNull = new ArrayList<>(); + withNull.add(null); + assertThrows(IllegalArgumentException.class, () -> service.validate(withNull, 0)); + + ValidationResult result = service.validate( + List.of(createTransaction(1, 1, 0, TransactionType.BET)), + 0 + ); + assertThrows(UnsupportedOperationException.class, () -> result.getTransactions().clear()); + } } From f97907962a7d64c08be161f9d62b84119d90c503 Mon Sep 17 00:00:00 2001 From: "Andrei.Ovcharenko" Date: Sat, 11 Jul 2026 10:32:21 +0300 Subject: [PATCH 2/3] ci: prime surefire before offline gate --- .github/workflows/maven.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 28b8499..8ef95f1 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -24,8 +24,8 @@ jobs: distribution: temurin java-version: 21 cache: maven - - name: Prime the exact Maven dependency set - run: mvn -B -ntp -DskipTests test --file pom.xml + - name: Prime the exact Maven test runtime + run: mvn -B -ntp -Dtest=quality.SmokeMethodTestHarnessTest test --file pom.xml - name: Run the strict offline test gate shell: pwsh run: .\scripts\run-offline-gate.ps1 -Goal test -StrictDependencies From cab92b4889a58c941db36630ada61c843a078ecb Mon Sep 17 00:00:00 2001 From: "Andrei.Ovcharenko" Date: Sat, 11 Jul 2026 10:33:43 +0300 Subject: [PATCH 3/3] ci: quote Maven selector on Windows --- .github/workflows/maven.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 8ef95f1..70ef064 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -25,7 +25,7 @@ jobs: java-version: 21 cache: maven - name: Prime the exact Maven test runtime - run: mvn -B -ntp -Dtest=quality.SmokeMethodTestHarnessTest test --file pom.xml + run: mvn -B -ntp "-Dtest=quality.SmokeMethodTestHarnessTest" test --file pom.xml - name: Run the strict offline test gate shell: pwsh run: .\scripts\run-offline-gate.ps1 -Goal test -StrictDependencies