Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/main/java/com/thealgorithms/maths/ComplexNumberMultiply.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@ private static int[] parse(String num) {
return new int[] {real, imaginary};
}

/**
* Multiplies two complex numbers formatted as {@code "a+bi"}.
*
* <p>Input contract: both {@code num1} and {@code num2} must be non-null strings matching
* {@code -?\d+\+-?\d+i} with integer components (for example {@code "0+0i"} or
* {@code "1+-2i"}); otherwise {@link IllegalArgumentException} is thrown.
*
* <p>Output contract: pure function returning {@code "<real>+<imaginary>i"} where
* {@code real = a*c - b*d} and {@code imaginary = a*d + b*c}, preserving negative signs
* directly (for example {@code "0+-2i"}).
*
* @param num1 first complex number in {@code "a+bi"} form
* @param num2 second complex number in {@code "a+bi"} form
* @return product formatted as {@code "real+imaginaryi"}
* @throws IllegalArgumentException if either input is null or does not match {@code -?\d+\+-?\d+i}
*/
public static String multiply(String num1, String num2) {
int[] a = parse(num1);
int[] b = parse(num2);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package com.thealgorithms.maths;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.Test;

/**
* Contract regression tests for {@link ComplexNumberMultiply#multiply(String, String)}.
*
* <p>Source facts ({@code ComplexNumberMultiply.java}):
* <ul>
* <li>line 9: private constructor {@code ComplexNumberMultiply()} (utility class)</li>
* <li>lines 12-21: {@code parse(String)} requires non-null input matching
* {@code -?\d+\+-?\d+i} (line 13), throwing {@code IllegalArgumentException} on mismatch
* (line 14), splitting on {@code "\\+"} (line 17), and parsing {@code int} parts (lines 18-19)</li>
* <li>lines 39-47: {@code multiply(String, String)} computes
* {@code real = a[0] * b[0] - a[1] * b[1]} (line 43) and
* {@code imaginary = a[0] * b[1] + a[1] * b[0]} (line 44), returning
* {@code real + "+" + imaginary + "i"} (line 46)</li>
* </ul>
*
* <p>Expected values are independently calculated literals.
*/
public class ComplexNumberMultiplyContractTest {

private static void logCase(String id, String input, Object expected, Object actual) {
System.out.println("[R45-T011] " + id + " | input=" + input
+ " | expected=" + expected + " | actual=" + actual);
}

/**
* Smallest valid inputs: {@code "0+0i"} (zero complex number, minimal digits matching line 13)
* and unit elements {@code "1+0i"} and {@code "0+1i"}.
*/
@Test
public void smallestValidInputsMatchExpectedContract() {
String actualZero = ComplexNumberMultiply.multiply("0+0i", "0+0i");
logCase("smallest-zero", "(\"0+0i\", \"0+0i\")", "0+0i", actualZero);
assertEquals("0+0i", actualZero);

String actualRealUnit = ComplexNumberMultiply.multiply("1+0i", "1+0i");
logCase("smallest-real-unit", "(\"1+0i\", \"1+0i\")", "1+0i", actualRealUnit);
assertEquals("1+0i", actualRealUnit);

String actualImagUnit = ComplexNumberMultiply.multiply("0+1i", "0+1i");
logCase("smallest-imag-unit", "(\"0+1i\", \"0+1i\")", "-1+0i", actualImagUnit);
assertEquals("-1+0i", actualImagUnit);
}

/**
* Ordinary inputs with positive and negative components:
* (2 + 3i)(4 + 5i) = (8 - 15) + (10 + 12)i = -7 + 22i;
* (1 - 2i)(-3 + 4i) = (-3 + 8) + (4 + 6)i = 5 + 10i.
*/
@Test
public void ordinaryInputsMatchExpectedContract() {
String actualPos = ComplexNumberMultiply.multiply("2+3i", "4+5i");
logCase("ordinary-positive", "(\"2+3i\", \"4+5i\")", "-7+22i", actualPos);
assertEquals("-7+22i", actualPos);

String actualMixed = ComplexNumberMultiply.multiply("1+-2i", "-3+4i");
logCase("ordinary-mixed-signs", "(\"1+-2i\", \"-3+4i\")", "5+10i", actualMixed);
assertEquals("5+10i", actualMixed);
}

/**
* Repeated-value and domain boundary cases: repeated identical operands, multiplicative identity,
* zero absorption, pure imaginary operands, conjugate pairs, and negative-imaginary formatting
* ({@code "a+-bi"} per line 46).
*/
@Test
public void repeatedValuesAndDomainBoundariesMatchExpectedContract() {
String actualRepeatedPos = ComplexNumberMultiply.multiply("1+1i", "1+1i");
logCase("repeated-1+1i", "(\"1+1i\", \"1+1i\")", "0+2i", actualRepeatedPos);
assertEquals("0+2i", actualRepeatedPos);

String actualRepeatedNeg = ComplexNumberMultiply.multiply("-1+-1i", "-1+-1i");
logCase("repeated--1+-1i", "(\"-1+-1i\", \"-1+-1i\")", "0+2i", actualRepeatedNeg);
assertEquals("0+2i", actualRepeatedNeg);

String actualRepeatedNegI = ComplexNumberMultiply.multiply("0+-1i", "0+-1i");
logCase("repeated-0+-1i", "(\"0+-1i\", \"0+-1i\")", "-1+0i", actualRepeatedNegI);
assertEquals("-1+0i", actualRepeatedNegI);

String actualIdentity = ComplexNumberMultiply.multiply("1+0i", "-7+-9i");
logCase("boundary-identity", "(\"1+0i\", \"-7+-9i\")", "-7+-9i", actualIdentity);
assertEquals("-7+-9i", actualIdentity);

String actualZeroAbsorb = ComplexNumberMultiply.multiply("0+0i", "-7+-9i");
logCase("boundary-zero-absorb", "(\"0+0i\", \"-7+-9i\")", "0+0i", actualZeroAbsorb);
assertEquals("0+0i", actualZeroAbsorb);

String actualPureImag = ComplexNumberMultiply.multiply("0+3i", "0+4i");
logCase("boundary-pure-imag", "(\"0+3i\", \"0+4i\")", "-12+0i", actualPureImag);
assertEquals("-12+0i", actualPureImag);

String actualConjugate = ComplexNumberMultiply.multiply("3+4i", "3+-4i");
logCase("boundary-conjugate", "(\"3+4i\", \"3+-4i\")", "25+0i", actualConjugate);
assertEquals("25+0i", actualConjugate);

String actualMultiDigit = ComplexNumberMultiply.multiply("-12+15i", "10+-20i");
logCase("boundary-multi-digit", "(\"-12+15i\", \"10+-20i\")", "180+390i", actualMultiDigit);
assertEquals("180+390i", actualMultiDigit);
}

/**
* Invalid format and null-input contract (lines 13-15):
* {@code parse(String)} rejects {@code null} and any string not matching {@code -?\d+\+-?\d+i}
* with {@code IllegalArgumentException("Invalid complex number format: " + num)}.
*/
@Test
public void invalidInputsThrowIllegalArgumentExceptionWithExpectedMessage() {
IllegalArgumentException nullFirst = assertThrows(
IllegalArgumentException.class, () -> ComplexNumberMultiply.multiply(null, "1+1i"));
logCase("invalid-null-first", "(null, \"1+1i\")",
"IllegalArgumentException: Invalid complex number format: null", nullFirst.getMessage());
assertEquals("Invalid complex number format: null", nullFirst.getMessage());

IllegalArgumentException nullSecond = assertThrows(
IllegalArgumentException.class, () -> ComplexNumberMultiply.multiply("1+1i", null));
logCase("invalid-null-second", "(\"1+1i\", null)",
"IllegalArgumentException: Invalid complex number format: null", nullSecond.getMessage());
assertEquals("Invalid complex number format: null", nullSecond.getMessage());

String[] badInputs = {"1+1", "1-1i", " 1+1i", "", "a+bi", "+1i"};
for (String bad : badInputs) {
IllegalArgumentException ex = assertThrows(
IllegalArgumentException.class, () -> ComplexNumberMultiply.multiply(bad, "1+0i"));
String expectedMsg = "Invalid complex number format: " + bad;
logCase("invalid-format-" + bad, "(\"" + bad + "\", \"1+0i\")", expectedMsg, ex.getMessage());
assertEquals(expectedMsg, ex.getMessage());
}
}

/**
* Separately verifies absence of observable state mutation across repeated calls.
*/
@Test
public void repeatedCallsHaveNoObservableStateMutation() {
String a = new String("2+3i");
String b = new String("4+5i");
String first = ComplexNumberMultiply.multiply(a, b);
String second = ComplexNumberMultiply.multiply(a, b);
logCase("purity-repeated-call", "(\"2+3i\", \"4+5i\")", "-7+22i", first + " / " + second);
assertEquals("2+3i", a);
assertEquals("4+5i", b);
assertEquals("-7+22i", first);
assertEquals("-7+22i", second);
}
}
70 changes: 70 additions & 0 deletions work/R45-T011/check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Runner for task R45-T011: contract regression checks for ComplexNumberMultiply.

Runs the focused JUnit test through Maven, requires a nonzero test count,
saves inputs, expected versus actual results, and tool versions into work/R45-T011/,
and exits non-zero on any failure.
"""
import glob
import subprocess
import sys
import xml.etree.ElementTree as ET
from pathlib import Path

HERE = Path(__file__).resolve().parent
REPO = HERE.parent.parent
TEST_CLASS = "ComplexNumberMultiplyContractTest"
CMD = ["mvn", "-B", "test", f"-Dtest={TEST_CLASS}", "-Dsurefire.failIfNoSpecifiedTests=false"]


def tool_version(cmd):
try:
out = subprocess.run(cmd, capture_output=True, text=True, cwd=REPO)
return "\n".join(l for l in (out.stdout + out.stderr).splitlines()
if not l.startswith("Picked up")).strip()
except FileNotFoundError:
return f"{cmd[0]}: not found"


def main():
(HERE / "tool_versions.txt").write_text(
"java:\n" + tool_version(["java", "-version"]) + "\n\nmvn:\n"
+ tool_version(["mvn", "-v"]) + "\n\npython:\n" + sys.version + "\n")

print("Running:", " ".join(CMD))
proc = subprocess.run(CMD, capture_output=True, text=True, cwd=REPO)
combined_output = proc.stdout + proc.stderr
(HERE / "mvn_output.txt").write_text(combined_output)

comparisons = [line.strip() for line in combined_output.splitlines()
if line.strip().startswith("[R45-T011]")]
(HERE / "comparisons.txt").write_text(
("\n".join(comparisons) + "\n") if comparisons else "No comparison records captured.\n")

reports = glob.glob(str(REPO / "target/surefire-reports" / f"TEST-*{TEST_CLASS}.xml"))
if not reports:
(HERE / "result.txt").write_text(
f"command: {' '.join(CMD)}\nFAIL: no surefire report found\n")
print("FAIL: no surefire report found")
return 1

suite = ET.parse(reports[0]).getroot()
tests = int(suite.get("tests", 0))
failures = int(suite.get("failures", 0)) + int(suite.get("errors", 0))
skipped = int(suite.get("skipped", 0))
passed = proc.returncode == 0 and failures == 0 and tests > 0 and len(comparisons) > 0
status = "PASS" if passed else "FAIL"
summary = (
f"command: {' '.join(CMD)}\n"
f"tests={tests} failures/errors={failures} skipped={skipped} "
f"comparisons={len(comparisons)} mvn_exit={proc.returncode}\n"
f"status={status}\n"
)
(HERE / "result.txt").write_text(summary)
print(summary)

return 0 if passed else 1


if __name__ == "__main__":
sys.exit(main())
22 changes: 22 additions & 0 deletions work/R45-T011/comparisons.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[R45-T011] invalid-null-first | input=(null, "1+1i") | expected=IllegalArgumentException: Invalid complex number format: null | actual=Invalid complex number format: null
[R45-T011] invalid-null-second | input=("1+1i", null) | expected=IllegalArgumentException: Invalid complex number format: null | actual=Invalid complex number format: null
[R45-T011] invalid-format-1+1 | input=("1+1", "1+0i") | expected=Invalid complex number format: 1+1 | actual=Invalid complex number format: 1+1
[R45-T011] invalid-format-1-1i | input=("1-1i", "1+0i") | expected=Invalid complex number format: 1-1i | actual=Invalid complex number format: 1-1i
[R45-T011] invalid-format- 1+1i | input=(" 1+1i", "1+0i") | expected=Invalid complex number format: 1+1i | actual=Invalid complex number format: 1+1i
[R45-T011] invalid-format- | input=("", "1+0i") | expected=Invalid complex number format: | actual=Invalid complex number format:
[R45-T011] invalid-format-a+bi | input=("a+bi", "1+0i") | expected=Invalid complex number format: a+bi | actual=Invalid complex number format: a+bi
[R45-T011] invalid-format-+1i | input=("+1i", "1+0i") | expected=Invalid complex number format: +1i | actual=Invalid complex number format: +1i
[R45-T011] purity-repeated-call | input=("2+3i", "4+5i") | expected=-7+22i | actual=-7+22i / -7+22i
[R45-T011] smallest-zero | input=("0+0i", "0+0i") | expected=0+0i | actual=0+0i
[R45-T011] smallest-real-unit | input=("1+0i", "1+0i") | expected=1+0i | actual=1+0i
[R45-T011] smallest-imag-unit | input=("0+1i", "0+1i") | expected=-1+0i | actual=-1+0i
[R45-T011] repeated-1+1i | input=("1+1i", "1+1i") | expected=0+2i | actual=0+2i
[R45-T011] repeated--1+-1i | input=("-1+-1i", "-1+-1i") | expected=0+2i | actual=0+2i
[R45-T011] repeated-0+-1i | input=("0+-1i", "0+-1i") | expected=-1+0i | actual=-1+0i
[R45-T011] boundary-identity | input=("1+0i", "-7+-9i") | expected=-7+-9i | actual=-7+-9i
[R45-T011] boundary-zero-absorb | input=("0+0i", "-7+-9i") | expected=0+0i | actual=0+0i
[R45-T011] boundary-pure-imag | input=("0+3i", "0+4i") | expected=-12+0i | actual=-12+0i
[R45-T011] boundary-conjugate | input=("3+4i", "3+-4i") | expected=25+0i | actual=25+0i
[R45-T011] boundary-multi-digit | input=("-12+15i", "10+-20i") | expected=180+390i | actual=180+390i
[R45-T011] ordinary-positive | input=("2+3i", "4+5i") | expected=-7+22i | actual=-7+22i
[R45-T011] ordinary-mixed-signs | input=("1+-2i", "-3+4i") | expected=5+10i | actual=5+10i
71 changes: 71 additions & 0 deletions work/R45-T011/mvn_output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
[INFO] Scanning for projects...
[INFO] Loaded 24447 auto-discovered prefixes for remote repository central (prefixes-central.txt)
[INFO]
[INFO] -----------------------< com.thealgorithms:Java >-----------------------
[INFO] Building Java 1.0-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- jacoco:0.8.16:prepare-agent (default) @ Java ---
[INFO] Loaded 64 auto-discovered prefixes for remote repository apache.snapshots (prefixes-apache.snapshots.txt)
[INFO] argLine set to -javaagent:/Users/shyamlibakale/.m2/repository/org/jacoco/org.jacoco.agent/0.8.16/org.jacoco.agent-0.8.16-runtime.jar=destfile=/Users/shyamlibakale/TheAlgorithms-Java/TheAlgorithms-Java/target/jacoco.exec
[INFO]
[INFO] --- resources:3.5.0:resources (default-resources) @ Java ---
[INFO] skip non existing resourceDirectory /Users/shyamlibakale/TheAlgorithms-Java/TheAlgorithms-Java/src/main/resources
[INFO]
[INFO] --- compiler:3.16.0:compile (default-compile) @ Java ---
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 848 source files with javac [debug release 21] to target/classes
[INFO]
[INFO] --- resources:3.5.0:testResources (default-testResources) @ Java ---
[INFO] skip non existing resourceDirectory /Users/shyamlibakale/TheAlgorithms-Java/TheAlgorithms-Java/src/test/resources
[INFO]
[INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ Java ---
[INFO] Recompiling the module because of changed dependency.
[INFO] Compiling 797 source files with javac [debug release 21] to target/test-classes
[INFO]
[INFO] --- surefire:3.6.0:test (default-test) @ Java ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.thealgorithms.maths.ComplexNumberMultiplyContractTest
[R45-T011] invalid-null-first | input=(null, "1+1i") | expected=IllegalArgumentException: Invalid complex number format: null | actual=Invalid complex number format: null
[R45-T011] invalid-null-second | input=("1+1i", null) | expected=IllegalArgumentException: Invalid complex number format: null | actual=Invalid complex number format: null
[R45-T011] invalid-format-1+1 | input=("1+1", "1+0i") | expected=Invalid complex number format: 1+1 | actual=Invalid complex number format: 1+1
[R45-T011] invalid-format-1-1i | input=("1-1i", "1+0i") | expected=Invalid complex number format: 1-1i | actual=Invalid complex number format: 1-1i
[R45-T011] invalid-format- 1+1i | input=(" 1+1i", "1+0i") | expected=Invalid complex number format: 1+1i | actual=Invalid complex number format: 1+1i
[R45-T011] invalid-format- | input=("", "1+0i") | expected=Invalid complex number format: | actual=Invalid complex number format:
[R45-T011] invalid-format-a+bi | input=("a+bi", "1+0i") | expected=Invalid complex number format: a+bi | actual=Invalid complex number format: a+bi
[R45-T011] invalid-format-+1i | input=("+1i", "1+0i") | expected=Invalid complex number format: +1i | actual=Invalid complex number format: +1i
[R45-T011] purity-repeated-call | input=("2+3i", "4+5i") | expected=-7+22i | actual=-7+22i / -7+22i
[R45-T011] smallest-zero | input=("0+0i", "0+0i") | expected=0+0i | actual=0+0i
[R45-T011] smallest-real-unit | input=("1+0i", "1+0i") | expected=1+0i | actual=1+0i
[R45-T011] smallest-imag-unit | input=("0+1i", "0+1i") | expected=-1+0i | actual=-1+0i
[R45-T011] repeated-1+1i | input=("1+1i", "1+1i") | expected=0+2i | actual=0+2i
[R45-T011] repeated--1+-1i | input=("-1+-1i", "-1+-1i") | expected=0+2i | actual=0+2i
[R45-T011] repeated-0+-1i | input=("0+-1i", "0+-1i") | expected=-1+0i | actual=-1+0i
[R45-T011] boundary-identity | input=("1+0i", "-7+-9i") | expected=-7+-9i | actual=-7+-9i
[R45-T011] boundary-zero-absorb | input=("0+0i", "-7+-9i") | expected=0+0i | actual=0+0i
[R45-T011] boundary-pure-imag | input=("0+3i", "0+4i") | expected=-12+0i | actual=-12+0i
[R45-T011] boundary-conjugate | input=("3+4i", "3+-4i") | expected=25+0i | actual=25+0i
[R45-T011] boundary-multi-digit | input=("-12+15i", "10+-20i") | expected=180+390i | actual=180+390i
[R45-T011] ordinary-positive | input=("2+3i", "4+5i") | expected=-7+22i | actual=-7+22i
[R45-T011] ordinary-mixed-signs | input=("1+-2i", "-3+4i") | expected=5+10i | actual=5+10i
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.045 s -- in com.thealgorithms.maths.ComplexNumberMultiplyContractTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO]
[INFO] --- jacoco:0.8.16:report (generate-code-coverage-report) @ Java ---
[INFO] Loading execution data file /Users/shyamlibakale/TheAlgorithms-Java/TheAlgorithms-Java/target/jacoco.exec
[INFO] Analyzed bundle 'Java' with 1073 classes
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 5.763 s
[INFO] Finished at: 2026-11-11T16:30:48+05:30
[INFO] ------------------------------------------------------------------------
3 changes: 3 additions & 0 deletions work/R45-T011/result.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
command: mvn -B test -Dtest=ComplexNumberMultiplyContractTest -Dsurefire.failIfNoSpecifiedTests=false
tests=5 failures/errors=0 skipped=0 comparisons=22 mvn_exit=0
status=PASS
14 changes: 14 additions & 0 deletions work/R45-T011/tool_versions.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
java:
openjdk version "21.0.13" 2026-10-20 LTS
OpenJDK Runtime Environment Temurin-21.0.13+9 (build 21.0.13+9-LTS)
OpenJDK 64-Bit Server VM Temurin-21.0.13+9 (build 21.0.13+9-LTS, mixed mode, sharing)

mvn:
Apache Maven 3.10.0 (c43a36b8d67be7e0805a411bc0898af1a51f5472)
Maven home: /opt/homebrew/Cellar/maven/3.10.0/libexec
Java version: 27.0.1.0.3, vendor: Homebrew, runtime: /opt/homebrew/Cellar/openjdk/27.0.1.0.3/libexec/openjdk.jdk/Contents/Home
Default locale: en_IN, platform encoding: UTF-8, time zone: Asia/Kolkata
OS name: "mac os x", version: "27.0", arch: "aarch64", family: "mac"

python:
3.14.7 (v3.14.7:823f0323ee6, Aug 5 2026, 07:07:01) [Clang 21.0.0 (clang-2100.1.1.101)]