diff --git a/README.md b/README.md index 1e3356ca..19ae9a9f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,30 @@ # java-calculator 계산기 미션 저장소 + +## level1 +### 간단한 사칙연산 계산기 구현 + +요구사항 +- 인자 2개를 받아 사칙연산을 할 수 있는 계산기를 구현한다. +- 사칙연산과 매칭되는 4개의 메서드를 제공한다. +- 계산된 결과는 정수를 반환한다. + +## level2 +### 구현한 사칙연산 계산기 테스트 + +요구사항 +- 구현한 초간단 계산기가 예상한대로 동작하는지 Junit5를 활용하여 테스트를 자동화한다. + + +## level3 +### 문자열 계산기 구현 +요구사항 +- 쉼표(,) 또는 콜론(:)을 구분자로 가지는 문자열을 전달하는 경우 구분자를 기준으로 분리한 각 숫자의 합을 반환 (예: “” => 0, "1,2" => 3, "1,2,3" => 6, “1,2:3” => 6) +- 앞의 기본 구분자(쉼표, 콜론)외에 커스텀 구분자를 지정할 수 있다. 커스텀 구분자는 문자열 앞부분의 “//”와 “\n” 사이에 위치하는 문자를 커스텀 구분자로 사용한다. 예를 들어 “//;\n1;2;3”과 같이 값을 입력할 경우 커스텀 구분자는 세미콜론(;)이며, 결과 값은 6이 반환되어야 한다. +- 문자열 계산기에 숫자 이외의 값 또는 음수를 전달하는 경우 RuntimeException 예외를 throw한다. + +## level4 +### 문자열 계산기 구현 리팩토링 +- 기존 JUnit5로 작성되어 있던 단위 테스트를 AssertJ로 리팩터링한다. +JUnit5에서 제공하는 기능과 AssertJ에서 제공하는 기능을 사용해보고, 어떠한 차이가 있는지 경험한다. diff --git a/build.gradle b/build.gradle index 87254a3a..239f9e78 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,9 @@ repositories { dependencies { testImplementation platform('org.junit:junit-bom:5.9.1') + testImplementation platform('org.assertj:assertj-bom:3.25.1') testImplementation('org.junit.jupiter:junit-jupiter') + testImplementation('org.assertj:assertj-core') } test { diff --git a/src/main/java/Calculator.java b/src/main/java/Calculator.java index 987781dc..31095187 100644 --- a/src/main/java/Calculator.java +++ b/src/main/java/Calculator.java @@ -1,17 +1,17 @@ public class Calculator { - int add(int a, int b) { + public int add(int a, int b) { return a + b; } - int subtract(int a, int b) { + public int subtract(int a, int b) { return a - b; } - int multiply(int a, int b) { + public int multiply(int a, int b) { return a * b; } - int divide(int a, int b) { + public int divide(int a, int b) { if (b == 0) { throw new ArithmeticException("0으로 나눌 수 없습니다."); } diff --git a/src/main/java/StringCalculator.java b/src/main/java/StringCalculator.java new file mode 100644 index 00000000..52509e07 --- /dev/null +++ b/src/main/java/StringCalculator.java @@ -0,0 +1,68 @@ + +public class StringCalculator { + + public int calculate(String input) { + + if (input == null || input.trim().isEmpty()) { + return 0; + } + + // 지정 구분자 + String separator = "[,;]"; + String numbers = input; + + /* + * 커스텀 구분자 체크 + * 1. 입력받은 문자열이 //로 시작하면 \n 으로 끝나는 사이의 값을 구분자로 지정한다. + * 2. 커스텀 구분자가 정규 표현식으로 사용되는 문자일 경우 이스케이프 처리한다. + */ + if (input.startsWith("//")) { + int newSeparatorIndex = input.indexOf("\n"); + + String customSeparator = input.substring(2, newSeparatorIndex).trim(); + + // 정규식 특수문자 앞에 /를 붙인다. [\\\\^$.|?*+()\\[\\]{}]는 정규식 특수문자를 모두 찾는 패턴, \\\\는 자바에서 \를 앞에 붙여주는 표현 + customSeparator = customSeparator.replaceAll("[\\\\^$.|?*+()\\[\\]{}]", "\\\\$0"); + + separator = customSeparator + "|,|;"; + + numbers = input.substring(newSeparatorIndex + 1).trim(); + } + + if (numbers.isEmpty()) { + return 0; + } + + String[] numbersArray = numbers.split(separator); + + int sum = 0; + + for (String number : numbersArray) { + if (!isNumeric(number)) { + throw new RuntimeException("숫자가 아닌 값이 포함되어있습니다."); + } + + sum += Integer.parseInt(number); + } + + return sum; + } + + private boolean isNumeric(String number) { + if (number == null || number.trim().isEmpty()) { + return false; + } + + for (int i = 0; i < number.length(); i++) { + if (i == 0 && number.charAt(i) == '-') { + return false; + } + + if (!Character.isDigit(number.charAt(i))) { + return false; + } + } + + return true; + } +} diff --git a/src/test/java/CalculatorAssertTest.java b/src/test/java/CalculatorAssertTest.java new file mode 100644 index 00000000..9b425ce6 --- /dev/null +++ b/src/test/java/CalculatorAssertTest.java @@ -0,0 +1,58 @@ +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@DisplayName("Calculator Assert Test") +public class CalculatorAssertTest { + + private Calculator calculator = new Calculator(); + + @Test + @DisplayName("add") + void add_test() { + int a = 1; + int b = 2; + int expected = a + b; + + int actual = calculator.add(1, 2); + + assertEquals(expected, actual); + } + + @Test + @DisplayName("subtract") + void subtract_test() { + int a = 1; + int b = 2; + int expected = 1 - 2; + + int actual = calculator.subtract(a, b); + + assertEquals(expected, actual); + } + + @Test + @DisplayName("multiply") + void multiply_test() { + int a = 1; + int b = 2; + int expected = a * b; + + int actual = calculator.multiply(a, b); + + assertEquals(expected, actual); + } + + @Test + @DisplayName("divide") + void divide_test() { + int a = 1; + int b = 2; + int expected = a / b; + + int actual = calculator.divide(a, b); + + assertEquals(expected, actual); + } +} diff --git a/src/test/java/CalculatorJUnit5Test.java b/src/test/java/CalculatorJUnit5Test.java new file mode 100644 index 00000000..97e19e0c --- /dev/null +++ b/src/test/java/CalculatorJUnit5Test.java @@ -0,0 +1,64 @@ +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Calculator junit5 Test") +public class CalculatorJUnit5Test { + + private Calculator calculator = new Calculator(); + + @Test + @DisplayName("add") + void add_test() { + int a = 1; + int b = 2; + int expected = a + b; + + int actual = calculator.add(1, 2); + + if (actual != expected) { + throw new RuntimeException("expected: <" + expected + "> but was: <" + actual + ">"); + } + } + + @Test + @DisplayName("subtract") + void subtract_test() { + int a = 1; + int b = 2; + int expected = 1 - 2; + + int actual = calculator.subtract(a, b); + + if (actual != expected) { + throw new RuntimeException("expected: <" + expected + "> but was: <" + actual + ">"); + } + } + + @Test + @DisplayName("multiply") + void multiply_test() { + int a = 1; + int b = 2; + int expected = a * b; + + int actual = calculator.multiply(a, b); + + if (actual != expected) { + throw new RuntimeException("expected: <" + expected + "> but was: <" + actual + ">"); + } + } + + @Test + @DisplayName("divide") + void divide_test() { + int a = 1; + int b = 2; + int expected = a / b; + + int actual = calculator.divide(a, b); + + if (actual != expected) { + throw new RuntimeException("expected: <" + expected + "> but was: <" + actual + ">"); + } + } +} diff --git a/src/test/java/StringCalculatorAssertTest.java b/src/test/java/StringCalculatorAssertTest.java new file mode 100644 index 00000000..0cd8b24b --- /dev/null +++ b/src/test/java/StringCalculatorAssertTest.java @@ -0,0 +1,46 @@ +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("String Calculator Assert Test") +public class StringCalculatorAssertTest { + + private final StringCalculator calculator = new StringCalculator(); + + @Test + @DisplayName("빈 문자열을 입력하면 0을 반환한다.") + void input_empty_string() { + String input = ""; + + // 인자를 비교하는 다양한 방법 + assertEquals(calculator.calculate(input), 0); + assertThat(calculator.calculate(input)).isZero(); + assertThat(calculator.calculate(input)).isEqualTo(0); + } + + @Test + @DisplayName("숫자만 입력하면 숫자를 그대로 리턴한다.") + void input_only_numbers() { + String input = "123"; + + assertThat(calculator.calculate(input)).isEqualTo(123); + } + + @Test + @DisplayName("기본 구분자가 포함된 문자열을 입력하면 숫자만 분리해 합한다.") + void input_numbers_with_basic_separator() { + String input = "1;2,3"; + + assertThat(calculator.calculate(input)).isEqualTo(6); + } + + @Test + @DisplayName("커스텀 구분자가 포함된 문자열을 입력하면 숫자만 분리해 합한다.") + void input_numbers_with_custom_separator() { + String input = "//*\n1;2,3"; + + assertThat(calculator.calculate(input)).isEqualTo(6); + } +}