diff --git a/Strategy.md b/Strategy.md new file mode 100644 index 0000000..ad4c690 --- /dev/null +++ b/Strategy.md @@ -0,0 +1,16 @@ + +메시지 + +1. 입력을 받아라 +2. 구분자를 뽑아내라 - inner class +3. 구분자에 따라 쪼개라 +4. 덧셈해라 + + +구분자로 쪼개는 애 > Splitter +구분자 뽑아내는 애 > Splitter.delimiter + +커스텀 구분자가 있는지 없는지 판단하는 애? 서비스레이어에서하자 + + +덧셈하는 > Calculator diff --git a/src/main/java/calculator/empty.txt b/src/main/java/calculator/empty.txt deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/domain/Calculator.java b/src/main/java/domain/Calculator.java new file mode 100644 index 0000000..838ad08 --- /dev/null +++ b/src/main/java/domain/Calculator.java @@ -0,0 +1,14 @@ +package domain; + +import domain.dto.Operand; + +import java.util.List; + +public class Calculator { + public long calculate(List operands) { + return operands.stream() + .map(Operand::new) + .reduce(new Operand(), Operand::sum) + .getValue(); + } +} diff --git a/src/main/java/domain/dto/Operand.java b/src/main/java/domain/dto/Operand.java new file mode 100644 index 0000000..6c40d23 --- /dev/null +++ b/src/main/java/domain/dto/Operand.java @@ -0,0 +1,52 @@ +package domain.dto; + +public class Operand { + private final long value; + + public Operand(){ + this.value = 0; + } + + public Operand(final String operand) { + try { + long parsedOperand = Long.parseLong(operand); + checkNegative(parsedOperand); + this.value = parsedOperand; + } catch (NumberFormatException ne) { + throw new IllegalArgumentException(String.format("%s 는 Long 형으로 파싱될 수 없습니다.", operand)); + } + } + + private Operand(long value) { + this.value = value; + } + + private void checkNegative(long parsedOperand) { + if (parsedOperand < 0) { + throw new IllegalArgumentException(String.format("음수인 %d 값은 들어올 수 없습니다", parsedOperand)); + } + } + + public long getValue() { + return this.value; + } + + public Operand sum(Operand operand) { + return new Operand(this.value + operand.value); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Operand operand = (Operand) o; + + return value == operand.value; + } + + @Override + public int hashCode() { + return (int) (value ^ (value >>> 32)); + } +} diff --git a/src/main/java/service/CalculatorService.java b/src/main/java/service/CalculatorService.java new file mode 100644 index 0000000..9516027 --- /dev/null +++ b/src/main/java/service/CalculatorService.java @@ -0,0 +1,27 @@ +package service; + +import domain.Calculator; +import support.Splitter; + +import java.util.List; + +public class CalculatorService { + private final Calculator calculator; + + public CalculatorService(Calculator calculator) { + this.calculator = calculator; + } + + public long calculate(final String formula) { + if (isNullOrEmpty(formula)) { + return 0; + } + + return calculator.calculate(Splitter.split(formula.trim())); + } + + private boolean isNullOrEmpty(String formula) { + return formula == null || formula.isEmpty(); + } + +} diff --git a/src/main/java/support/Splitter.java b/src/main/java/support/Splitter.java new file mode 100644 index 0000000..7cc4f58 --- /dev/null +++ b/src/main/java/support/Splitter.java @@ -0,0 +1,46 @@ +package support; + +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +public class Splitter { + private static final String DEFAULT_DELIMITER_REGEX = "[,:]"; + private static final String NUMERIC_REGEX = "-?\\d+(\\.\\d+)?"; + + private static final int CUSTOM_DELIMITER_INDEX = 1; + private static final int REAL_FORMULA_INDEX = 2; + + private static final Pattern CUSTOM_DELIMITER_PATTERN = Pattern.compile("//(.*)₩n(.*)"); + + private Splitter() { + } + + public static List split(final String formula) { + Matcher customDelimiterMatcher = CUSTOM_DELIMITER_PATTERN.matcher(formula); + + if (customDelimiterMatcher.find()) { + return split(customDelimiterMatcher); + } + + return Arrays.asList(formula.split(DEFAULT_DELIMITER_REGEX)); + } + + private static List split(Matcher customDelimiterMatcher) { + String realFormula = customDelimiterMatcher.group(REAL_FORMULA_INDEX); + String customDelimiter = customDelimiterMatcher.group(CUSTOM_DELIMITER_INDEX); + + validateDelimiter(customDelimiter); + + return Arrays.asList(realFormula.split(customDelimiter)); + } + + private static void validateDelimiter(final String customDelimiter) { + if (customDelimiter.matches(NUMERIC_REGEX) || customDelimiter.isEmpty()) { + throw new IllegalArgumentException(String.format("%s 는(은) 올바르지 않은 커스텀구분자입니다.", customDelimiter)); + } + } + +} diff --git a/src/test/java/domain/CalculatorTest.java b/src/test/java/domain/CalculatorTest.java new file mode 100644 index 0000000..70e47d6 --- /dev/null +++ b/src/test/java/domain/CalculatorTest.java @@ -0,0 +1,52 @@ +package domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +class CalculatorTest { + + Calculator calculator = new Calculator(); + + @DisplayName("calculator에 음수가 아닌 숫자가 들어가면 정상적으로 더해서 반환한다.") + @Test + void calculate() { + //given + List formula = Arrays.asList("1", "2", "3"); + + //when + double result = calculator.calculate(formula); + + //then + assertThat(result).isEqualTo(6); + } + + @DisplayName("숫자가 아닌 값이나 음수가 들어가면 Exception이 발생한다") + @MethodSource("exceptionCase") + @ParameterizedTest + void calculateThrow(List formula, String exceptionMessage) { + //given + //when + //then + assertThatThrownBy(() -> calculator.calculate(formula)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(exceptionMessage); + } + + static Stream exceptionCase() { + return Stream.of( + Arguments.of(Arrays.asList("1", "@", "3"), String.format("%s 는 Long 형으로 파싱될 수 없습니다.", "@")), + Arguments.of(Arrays.asList("1", "-2", "3"), String.format("음수인 %d 값은 들어올 수 없습니다", -2)) + ); + } + +} diff --git a/src/test/java/domain/dto/OperandTest.java b/src/test/java/domain/dto/OperandTest.java new file mode 100644 index 0000000..9a73fdd --- /dev/null +++ b/src/test/java/domain/dto/OperandTest.java @@ -0,0 +1,42 @@ +package domain.dto; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +class OperandTest { + + @DisplayName("음수나 문자가 들어가면 익셉션을 던져준다") + @MethodSource("negativeAndString") + @ParameterizedTest + void negative(String input, String exceptionMessage){ + assertThatThrownBy(() -> new Operand(input)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(exceptionMessage); + } + + static Stream negativeAndString(){ + return Stream.of( + Arguments.of("-5", "음수인 -5 값은 들어올 수 없습니다"), + Arguments.of("abc", "abc 는 Long 형으로 파싱될 수 없습니다.") + ); + } + + @DisplayName("Operand 끼리 sum 정상 동작 수행 확인") + @Test + void sum(){ + Operand operand = new Operand("5"); + Operand operand1 = new Operand("1"); + + assertThat(operand.sum(operand1)).isEqualTo(new Operand("6")); + } + + +} \ No newline at end of file diff --git a/src/test/java/service/CalculatorServiceTest.java b/src/test/java/service/CalculatorServiceTest.java new file mode 100644 index 0000000..47cdb37 --- /dev/null +++ b/src/test/java/service/CalculatorServiceTest.java @@ -0,0 +1,70 @@ +package service; + +import domain.Calculator; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import java.util.stream.Stream; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class CalculatorServiceTest { + + CalculatorService calculatorService = new CalculatorService(new Calculator()); + + @DisplayName(",과 : 로 구분하거나 커스텀 구분자를 설정한 정상계산 테스트") + @MethodSource("validFormula") + @ParameterizedTest + void validFormula(String formula, long expectedValue) { + assertThat(calculatorService.calculate(formula)).isEqualTo(expectedValue); + } + + static Stream validFormula() { + return Stream.of( + Arguments.of("//o₩n1o2o3", 6L), + Arguments.of("//abc₩n1abc2abc3abc", 6L), + Arguments.of("//@₩n3@1@7@1", 12L), + Arguments.of("1,2:3", 6L) + ); + } + + @DisplayName("숫자 하나를 문자열로 입력할 경우 해당 숫자를 반환한다.") + @Test + void formulaWithoutDelimiter() { + String formula = "12345"; + + assertThat(calculatorService.calculate(formula)).isEqualTo(Long.parseLong(formula)); + } + + @DisplayName("올바르지 않은 수식 테스트") + @MethodSource("invalidFormula") + @ParameterizedTest + void inValidFormula(String formula, String exceptionMessage) { + assertThatThrownBy(() -> calculatorService.calculate(formula)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(exceptionMessage); + } + + static Stream invalidFormula() { + return Stream.of( + Arguments.of("//₩n1o2o3", " 는(은) 올바르지 않은 커스텀구분자입니다."), + Arguments.of("//abc", "//abc 는 Long 형으로 파싱될 수 없습니다."), + Arguments.of("1abc2abc3abc", "1abc2abc3abc 는 Long 형으로 파싱될 수 없습니다."), + Arguments.of("//o₩n1*2", "1*2 는 Long 형으로 파싱될 수 없습니다.") + ); + } + + @DisplayName("빈 문자열 또는 null 값이 들어가면 0 을 반환해야 한다.") + @ParameterizedTest + @NullAndEmptySource + void emptyNull(String formula) { + assertThat(calculatorService.calculate((formula))).isEqualTo(0); + } + +} diff --git a/src/test/java/support/SplitterTest.java b/src/test/java/support/SplitterTest.java new file mode 100644 index 0000000..b77e209 --- /dev/null +++ b/src/test/java/support/SplitterTest.java @@ -0,0 +1,73 @@ +package support; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +class SplitterTest { + + @DisplayName("커스텀 구분자가 없을 때 제대로 쪼개지는 지") + @CsvSource({"'1,2,3'", "'1:2:3'", "'1,2:3'"}) + @ParameterizedTest + void split(String formula) { + //given + //when + List splitedFormula = Splitter.split(formula); + + //then + assertThat(splitedFormula).isEqualTo(Arrays.asList(formula.split("[,:]"))); + } + + + @DisplayName("커스텀 구분자가 있을 때 제대로 쪼개지는 지") + @MethodSource("customDelimiterFormat") + @ParameterizedTest + void splitByCustomDelimiter(String formula, List expectedValue) { + //given + //when + List splitedFormula = Splitter.split(formula); + + //then + assertThat(splitedFormula).isEqualTo(expectedValue); + } + + static Stream customDelimiterFormat() { + return Stream.of( + Arguments.of("//o₩n1o2o3", Arrays.asList("1", "2", "3")), + Arguments.of("//abc₩n1abc2abc3abc", Arrays.asList("1", "2", "3")) + ); + } + + + + @DisplayName("// \n 사이에 구분자가 없을 때 에러를 던져주는 지") + @MethodSource("exceptionCase") + @ParameterizedTest + void splitCustomDelimiterThrow(String formula, String exceptionMessage){ + //given + //when + //then + assertThatThrownBy(() -> Splitter.split(formula)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(exceptionMessage); + } + + static Stream exceptionCase(){ + return Stream.of( + Arguments.of("//₩n1o2o3", " 는(은) 올바르지 않은 커스텀구분자입니다."), + Arguments.of("//1₩n11213", "1 는(은) 올바르지 않은 커스텀구분자입니다.") + ); + } + + +}