diff --git a/build.gradle b/build.gradle index f6efa25b..509ac291 100644 --- a/build.gradle +++ b/build.gradle @@ -10,11 +10,10 @@ repositories { } dependencies { - testImplementation platform('org.junit:junit-bom:5.9.1') - testImplementation('org.junit.jupiter:junit-jupiter') 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') } diff --git a/src/main/java/Calculator.java b/src/main/java/Calculator.java deleted file mode 100644 index 6b06ec43..00000000 --- a/src/main/java/Calculator.java +++ /dev/null @@ -1,31 +0,0 @@ -import java.util.function.BiFunction; - -public class Calculator { - public static int calculate(BiFunction func, int num1, int num2) { - try { - return func.apply(num1, num2); - } catch (ArithmeticException e) { - throw new ArithmeticException("연산 결과가 자료형 int의 범위를 벗어났습니다."); - } - } - - public static int add(int num1, int num2) { - return calculate(Math::addExact, num1, num2); - } - - public static int sub(int num1, int num2) { - return calculate(Math::subtractExact, num1, num2); - } - - public static int mul(int num1, int num2) { - return calculate(Math::multiplyExact, num1, num2); - } - - public static int div(int num1, int num2) { - if (num2 == 0) { - throw new ArithmeticException("0으로 나눌 수 없습니다."); - } - - return num1 / num2; - } -} \ No newline at end of file diff --git a/src/main/java/StringCalculator.java b/src/main/java/StringCalculator.java deleted file mode 100644 index 4dfc37b5..00000000 --- a/src/main/java/StringCalculator.java +++ /dev/null @@ -1,58 +0,0 @@ -public class StringCalculator { - public static int calculate(String str) { - String delimeter = "[,|:]"; - String[] tokens; - int res = 0; - - if (str == null || str.isBlank()) { - return 0; - } - - String customDelimter = findCustomDelimeter(str); - - if (customDelimter != null) { - delimeter = customDelimter; - tokens = str.substring(str.indexOf("\n") + 1).split(delimeter); - } - - else { - tokens = str.split(delimeter); - } - - return sum(tokens); - } - - public static String findCustomDelimeter(String str) { - int idx = 0; - int startIdx = str.indexOf("//"); - int endIdx = str.indexOf("\n"); - - if ((startIdx == -1) != (endIdx == -1) || (startIdx + 2 == endIdx)) { // 커스텀 구분자 형식인 // 과 \n 중 하나만 존재하거나 구분자가 없는 경우 - throw new RuntimeException("커스텀 구분자 형식에 맞지 않는 문자열을 전달했습니다."); - } - - else if ((startIdx != -1)) { // 커스텀 구분자 형식인 경우 - return str.substring(startIdx + 2, endIdx); - } - - else { // 디폴트 구분자 형식인 경우 - return null; - } - } - - public static int sum(String[] tokens) { - int res = 0; - - for (String token : tokens) { - try { - res = Math.addExact(res, Integer.parseInt(token)); - } catch (NumberFormatException e) { - throw new RuntimeException("숫자 이외의 값 또는 음수를 전달할 수 없습니다."); - } catch (ArithmeticException e) { - throw new ArithmeticException("연산 결과가 자료형 int의 범위를 벗어났습니다."); - } - } - - return res; - } -} diff --git a/src/main/java/domain/Calculator.java b/src/main/java/domain/Calculator.java new file mode 100644 index 00000000..46de3fde --- /dev/null +++ b/src/main/java/domain/Calculator.java @@ -0,0 +1,25 @@ +package domain; + +import exception.ErrorMessage; + +public class Calculator { + + public int add(int num1, int num2) { + return Math.addExact(num1, num2); + } + + public int sub(int num1, int num2) { + return Math.subtractExact(num1, num2); + } + + public int mul(int num1, int num2) { + return Math.multiplyExact(num1, num2); + } + + public int div(int num1, int num2) { + if (num2 == 0) + throw new ArithmeticException(ErrorMessage.DIVIDE_BY_ZERO); + + return num1 / num2; + } +} diff --git a/src/main/java/domain/StringCalculator.java b/src/main/java/domain/StringCalculator.java new file mode 100644 index 00000000..728e5eb0 --- /dev/null +++ b/src/main/java/domain/StringCalculator.java @@ -0,0 +1,52 @@ +package domain; + +import exception.ErrorMessage; +import util.DelimiterUtil; +import util.ParseUtil; + +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; + +public class StringCalculator { + + public int calculate(String str) { + if (str == null) { + throw new RuntimeException(ErrorMessage.NULL_STRING); + } + + if (str.isBlank()) + return 0; + + String customDelimiter = DelimiterUtil.findCustomDelimiter(str); + String strNumbers = extractNumber(str, customDelimiter); + String[] tokens = splitTokens(strNumbers, customDelimiter); + List numbers = ParseUtil.parseNumber(tokens); + + return sum(numbers); + } + + private String extractNumber(String str, String customDelimiter) { + if (customDelimiter != null) { + return str.substring(str.indexOf("\n") + 1); + } + + return str; + } + + private String[] splitTokens(String strNumbers, String customDelimiter) { + String delimiterRegex = "[,|:]"; + + if (customDelimiter == null) { + return strNumbers.split(delimiterRegex); + } + + return strNumbers.split(Pattern.quote(customDelimiter)); + } + + private int sum(List numbers) { + return numbers.stream() + .mapToInt(Integer::intValue) + .sum(); + } +} diff --git a/src/main/java/exception/ErrorMessage.java b/src/main/java/exception/ErrorMessage.java new file mode 100644 index 00000000..d612613d --- /dev/null +++ b/src/main/java/exception/ErrorMessage.java @@ -0,0 +1,14 @@ +package exception; + +public class ErrorMessage { + + public static final String NULL_STRING = "문자열에 null 값이 전달되었습니다."; + public static final String CUSTOM_DELIMITER_NOT_FOUND = "커스텀 구분자를 찾을 수 없습니다."; + public static final String NEGATIVE_NUMBER_NOT_ALLOWED = "음수는 처리할 수 없습니다."; + public static final String INVALID_STRING = "문자열은 처리할 수 없습니다."; + public static final String DIVIDE_BY_ZERO = "0으로 나눌 수 없습니다."; + + private ErrorMessage() { + throw new RuntimeException(); + } +} diff --git a/src/main/java/util/DelimiterUtil.java b/src/main/java/util/DelimiterUtil.java new file mode 100644 index 00000000..8e3f090f --- /dev/null +++ b/src/main/java/util/DelimiterUtil.java @@ -0,0 +1,21 @@ +package util; + +import exception.ErrorMessage; + +public class DelimiterUtil { + + public static String findCustomDelimiter(String str) { + int startDelimiterIdx = str.indexOf("//"); + int endDelimiterIdx = str.indexOf("\n"); + + if (startDelimiterIdx == -1 && endDelimiterIdx == -1) { + return null; + } + + if ((startDelimiterIdx == -1) ^ (endDelimiterIdx == -1) || (startDelimiterIdx + 2 == endDelimiterIdx)) { + throw new RuntimeException(ErrorMessage.CUSTOM_DELIMITER_NOT_FOUND); + } + + return str.substring(startDelimiterIdx + 2, endDelimiterIdx); + } +} diff --git a/src/main/java/util/ParseUtil.java b/src/main/java/util/ParseUtil.java new file mode 100644 index 00000000..cf5086e3 --- /dev/null +++ b/src/main/java/util/ParseUtil.java @@ -0,0 +1,29 @@ +package util; + +import exception.ErrorMessage; + +import java.util.Arrays; +import java.util.List; + +public class ParseUtil { + + public static List parseNumber(String[] tokens) { + return Arrays.stream(tokens) + .map(ParseUtil::parseTokenToInt) + .toList(); + } + + public static int parseTokenToInt(String token) { + try { + int number = Integer.parseInt(token.trim()); + + if (number < 0) { + throw new RuntimeException(ErrorMessage.NEGATIVE_NUMBER_NOT_ALLOWED); + } + + return number; + } catch (NumberFormatException ex) { + throw new RuntimeException(ErrorMessage.INVALID_STRING); + } + } +} diff --git a/src/test/java/CalculatorTest.java b/src/test/java/CalculatorTest.java index 7c85e553..199afe17 100644 --- a/src/test/java/CalculatorTest.java +++ b/src/test/java/CalculatorTest.java @@ -1,66 +1,83 @@ -import org.junit.jupiter.api.DisplayName; +import domain.Calculator; +import exception.ErrorMessage; import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.*; -@DisplayName("계산기 기능 테스트 클래스") public class CalculatorTest { + + private final Calculator calculator = new Calculator(); + + @Test + public void 덧셈_테스트() { + final int num1 = 1; + final int num2 = 2; + final int actual = calculator.add(num1, num2); + + assertThat(actual).isEqualTo(num1 + num2); + } + @Test - @DisplayName("더하기 연산 테스트") - public void testAdd() { - int num1 = 1, num2 = 2; + public void 뺄셈_테스트() { + final int num1 = 3; + final int num2 = 2; + final int actual = calculator.sub(num1, num2); - assertThat(3).isEqualTo(Calculator.add(num1, num2)); + assertThat(actual).isEqualTo(num1 - num2); } @Test - @DisplayName("빼기 연산 테스트") - public void testSub() { - int num1 = 1, num2 = 2; + public void 곱셈_테스트() { + final int num1 = 3; + final int num2 = 2; + final int actual = calculator.mul(num1, num2); - assertThat(-1).isEqualTo(Calculator.sub(num1, num2)); + assertThat(actual).isEqualTo(num1 * num2); } @Test - @DisplayName("곱하기 연산 테스트") - public void testMul() { - int num1 = 10, num2 = 2; + public void 나눗셈_테스트() { + final int num1 = 4; + final int num2 = 2; + final int actual = calculator.div(num1, num2); - assertThat(20).isEqualTo(Calculator.mul(num1, num2)); + assertThat(actual).isEqualTo(num1 / num2); } @Test - @DisplayName("나누기 연산 테스트") - public void testDiv() { - int num1 = 1, num2 = 2; + public void 덧셈_오버플로우_예외_발생() { + final int num1 = Integer.MAX_VALUE; + final int num2 = Integer.MAX_VALUE; - assertThat(0).isEqualTo(Calculator.div(num1, num2)); + assertThatThrownBy(() -> calculator.add(num1, num2)) + .isInstanceOf(ArithmeticException.class); } @Test - @DisplayName("나누기 연산에서 나누는 값이 0인지 테스트") - public void testDivideByZero() { - int num1 = 2, num2 = 0; + public void 뺄셈_오버플로우_예외_발생() { + final int num1 = Integer.MIN_VALUE; + final int num2 = Integer.MAX_VALUE; - assertThatThrownBy(() -> { - Calculator.div(num1, num2); - }).isInstanceOf(ArithmeticException.class); + assertThatThrownBy(() -> calculator.sub(num1, num2)) + .isInstanceOf(ArithmeticException.class); } @Test - @DisplayName("연산 후 int 범위를 벗어나는 지 테스트") - public void testOverflow() { - assertThatThrownBy(() -> { - Calculator.add(Integer.MAX_VALUE, 1); - }).isInstanceOf(ArithmeticException.class); - - assertThatThrownBy(() -> { - Calculator.sub(Integer.MIN_VALUE, 1); - }).isInstanceOf(ArithmeticException.class); - - assertThatThrownBy(() -> { - Calculator.mul(Integer.MAX_VALUE, 2); - }).isInstanceOf(ArithmeticException.class); + public void 곱셈_오버플로우_예외_발생() { + final int num1 = Integer.MAX_VALUE; + final int num2 = Integer.MAX_VALUE; + + assertThatThrownBy(() -> calculator.mul(num1, num2)) + .isInstanceOf(ArithmeticException.class); + } + + @Test + public void 나눗셈_0으로_나누면_예외_발생() { + final int num1 = Integer.MAX_VALUE; + final int num2 = 0; + + assertThatThrownBy(() -> calculator.div(num1, num2)) + .isInstanceOf(ArithmeticException.class) + .hasMessageContaining(ErrorMessage.DIVIDE_BY_ZERO); } } diff --git a/src/test/java/StringCalculatorTest.java b/src/test/java/StringCalculatorTest.java index eb350cf3..8cd40401 100644 --- a/src/test/java/StringCalculatorTest.java +++ b/src/test/java/StringCalculatorTest.java @@ -1,49 +1,80 @@ -import org.junit.jupiter.api.DisplayName; +import domain.StringCalculator; +import exception.ErrorMessage; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.NullSource; import org.junit.jupiter.params.provider.ValueSource; +import util.DelimiterUtil; +import util.ParseUtil; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.util.List; + +import static org.assertj.core.api.Assertions.*; public class StringCalculatorTest { + + private final StringCalculator stringCalculator = new StringCalculator(); + @ParameterizedTest - @DisplayName("문자열 계산기 기능 테스트") - @ValueSource(strings = {"1:2:3", "1,2,3", "3:2,1", "3,2:1"}) - public void testDefaultDelimeter(String str) { - assertThat(StringCalculator.calculate(str)).isEqualTo(6); + @ValueSource(strings = { + "1,2,3:4", + "1 , 2: 3,4 " + }) + public void 기본_구분자_문자열_계산기_테스트(String str) { + int actual = stringCalculator.calculate(str); + + assertThat(actual).isEqualTo(10); } @ParameterizedTest - @ValueSource(strings = {"", " ", " "}) - @DisplayName("공백을 줬을 경우 테스트") - public void testEmptyValue(String value) { - assertThat(StringCalculator.calculate(value)).isEqualTo(0); + @ValueSource(strings = { + "//+\n1+2+3+4", + "// \n1 2 3 4", + "//||\n1||2||3||4" + }) + public void 커스텀_구분자_문자열_계산기_테스트(String str) { + int actual = stringCalculator.calculate(str); + + assertThat(actual).isEqualTo(10); } @ParameterizedTest - @ValueSource(strings = {"//;\n1;2;3", "//!!\n1!!2!!3"}) - @DisplayName("커스텀 구분자 테스트") - public void testCustomDelimeter(String value) { - assertThat(StringCalculator.calculate(value)).isEqualTo(6); + @ValueSource(strings = { + "//1;2;3;4", + "\n1;2;3;4", + "//\n1;2;3;4" + }) + public void 커스텀_구분자_형식에_맞지_않은_경우_예외_발생(String str) { + assertThatThrownBy(() -> stringCalculator.calculate(str)) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining(ErrorMessage.CUSTOM_DELIMITER_NOT_FOUND); } @ParameterizedTest - @ValueSource(strings = {"/;\n1;2;3", "/;\t1;2;3", "//;;\n1;2;3", "// n1 2 3"}) - @DisplayName("커스텀 구분자 형식에 맞지 않을 경우 예외 발생 테스트") - public void testCustomDelimeterFormatError(String value) { - assertThatThrownBy(() -> StringCalculator.calculate(value)).isInstanceOf(RuntimeException.class); + @NullSource + public void 문자열이_널값인_경우_예외_발생(String str) { + assertThatThrownBy(() -> stringCalculator.calculate(str)) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining(ErrorMessage.NULL_STRING); } - @Test - @DisplayName("커스텀 구분자에 아무것도 주지 않은 상황 테스트") - public void testEmptyCustomDelimeter() { - assertThatThrownBy(() -> StringCalculator.calculate("//\n1;2;3")).isInstanceOf(RuntimeException.class); + @ParameterizedTest + @ValueSource(strings = { + "", + " " + }) + public void 문자열이_비어있는_경우_결과값으로_0리턴(String str) { + int actual = stringCalculator.calculate(str); + + assertThat(actual).isEqualTo(0); } @Test - @DisplayName("계산 결과가 int 범위를 벗어나는 경우 예외 발생 테스트") - public void testReusltOverflow() { - assertThatThrownBy(() -> StringCalculator.calculate("2147483647:2:3")).isInstanceOf(ArithmeticException.class); + public void 구분자_없이_숫자만_입력한_경우() { + String str = "1"; + int actual = stringCalculator.calculate(str); + + assertThat(actual).isEqualTo(1); } -} \ No newline at end of file +} diff --git a/src/test/java/UtilTest.java b/src/test/java/UtilTest.java new file mode 100644 index 00000000..3367452d --- /dev/null +++ b/src/test/java/UtilTest.java @@ -0,0 +1,52 @@ +import exception.ErrorMessage; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import util.DelimiterUtil; +import util.ParseUtil; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class UtilTest { + @ParameterizedTest + @CsvSource({ + "'//+\n', '+'", + "'// \n', ' '", + "'//||\n', '||'" + }) + public void 커스텀_구분자_파싱_테스트(String str, String expected) { + String customDelimiter = DelimiterUtil.findCustomDelimiter(str); + + assertThat(customDelimiter).isEqualTo(expected); + } + + @Test + public void 문자열_리스트_정수_리스트로_변환() { + String[] tokens = {"1", "2", "3", "4"}; + + List actual = ParseUtil.parseNumber(tokens); + + assertThat(actual).containsExactly(1, 2, 3, 4); + } + + @Test + public void 문자열_리스트_정수_리스트로_변환_음수가_포함되면_예외_발생() { + String[] tokens = {"1", "2", "-3", "4"}; + + assertThatThrownBy(() -> ParseUtil.parseNumber(tokens)) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining(ErrorMessage.NEGATIVE_NUMBER_NOT_ALLOWED); + } + + @Test + public void 문자열_리스트_정수_리스트로_변환_문자가_포함되면_예외_발생() { + String[] tokens = {"a1", "b", " ", "4"}; + + assertThatThrownBy(() -> ParseUtil.parseNumber(tokens)) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining(ErrorMessage.INVALID_STRING); + } +}