Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1차 과제 제출합니다. #1895

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0574ab7
test: 잘못된 구분자에 대한 에러 테스트
sihyunjojo Oct 20, 2024
f58e118
test: 여러 숫자 입력에 대한 잘못된 구분자에 대한 에러 테스트
sihyunjojo Oct 20, 2024
560f33a
test: 여러 숫자 입력에 대한 잘못된 구분자에 대한 에러 테스트
sihyunjojo Oct 20, 2024
e43ab2e
test: 음수 입력에 대한 에러 테스트
sihyunjojo Oct 20, 2024
0b0416c
test: 커스텀 구분자에 대한 성공 로직 테스트
sihyunjojo Oct 20, 2024
eb72ba0
test: 일반 성공 로직 테스트
sihyunjojo Oct 20, 2024
ccc3b3b
test: 입력 문자열의 시작이 부적절한 경우 테스트
sihyunjojo Oct 20, 2024
bae0fce
test: 커스텀 구분자 정의 시 '\n'이 누락의 경우 테스트
sihyunjojo Oct 20, 2024
1013019
test: 커스텀 구분자를 사용시, 값이 없을 시 테스트
sihyunjojo Oct 20, 2024
b44e995
test: 커스텀 구분자를 사용시, 하나의 숫자에 대한 테스트
sihyunjojo Oct 20, 2024
03b4d42
docs: 구현할 기능 목록 추가
sihyunjojo Oct 20, 2024
d78ddbe
test: 숫자가 커스텀 구분자로 사용되는 경우 에러 발생하는 테스트
sihyunjojo Oct 20, 2024
4119462
test: 커스텀 구분자가 없는 경우 성공 테스트
sihyunjojo Oct 20, 2024
f62953b
test: 글자 커스텀 구분자를 사용한 성공 로직 테스트
sihyunjojo Oct 20, 2024
a8b488f
test: 여러 커스텀 구분자를 쉼표로 구분하여 처리하는 성공 로직 테스트
sihyunjojo Oct 20, 2024
9b580d5
test: 여러 커스텀 구분자를 파이프로 구분하게 변경
sihyunjojo Oct 20, 2024
4702fff
test: 파이프(`|`)가 구분자 외에 다른 위치 사용에 대한 에러 테스트
sihyunjojo Oct 20, 2024
7152f05
test: 람다 표현식 변경
sihyunjojo Oct 20, 2024
dc56ec7
docs: 구현할 기능 목록 추가
sihyunjojo Oct 20, 2024
bda6522
refactor: 클래스 이름 변경
sihyunjojo Oct 20, 2024
1af4c3d
feat: 입출력 담당 클래스 생성
sihyunjojo Oct 20, 2024
32d634d
feat: 메시지 상수로 관리
sihyunjojo Oct 20, 2024
9757bdb
refactor: 메시지 상수화를 통한 테스트 코드 일관성 유지
sihyunjojo Oct 20, 2024
0824bb4
refactor: 테스트 코드 예제 수정
sihyunjojo Oct 21, 2024
17d448f
feat: add 코드 생성
sihyunjojo Oct 21, 2024
4c4f5f3
refactor: style 변경
sihyunjojo Oct 21, 2024
6e3c2d4
refactor: style 변경
sihyunjojo Oct 21, 2024
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
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,23 @@
# java-calculator-precourse
# java-calculator-precourse

## 구현할 기능 목록

| 번호 | 기능 설명 |
|----|---------------------------------------------------|
| 1 | 기본 구분자(쉼표, 콜론) 외의 구분자(공백, 특수 문자 포함)가 입력될 경우 예외 처리 |
| 2 | 여러 숫자 입력에서 잘못된 구분자가 포함된 경우 예외 처리 |
| 3 | 입력 값에 영어 또는 한글이 구분자로 포함된 경우 예외 처리 |
| 4 | 음수가 입력된 경우 예외 처리 |
| 5 | 입력 문자열이 숫자 또는 "//"로 시작하지 않으면 예외 처리 |
| 6 | 커스텀 구분자가 정의될 때 `\n`이 없을 경우 예외 처리 |
| 7 | 쉼표와 콜론을 혼합 구분자로 사용했을 때 정상적으로 합산 결과 반환 |
| 8 | 커스텀 구분자를 사용한 경우 정상적으로 합산 결과 반환 |
| 9 | 커스텀 구분자만 있을 때 값이 없으면 0 반환 |
| 10 | 커스텀 구분자를 사용하고 숫자가 하나만 들어온 경우 그 숫자 반환 |
| 11 | 쉼표, 콜론 외에도 여러 커스텀 구분자를 파이프로 구분하여 사용 가능 |
| 12 | 커스텀 구분자에 쉼표가 포함되었을 경우 예외 처리 |
| 13 | 커스텀 구분자가 비어 있을 때 기본 구분자만 사용하여 정상적인 합산 결과 반환 |
| 14 | 커스텀 구분자에 숫자가 포함된 경우 예외 처리 |
| 15 | 파이프(`\|`)가 구분자 이외의 위치에서 사용되면 예외 처리 |
| 16 | 입력을 받아오는 기능 |
| 17 | 결과를 출력하는 기능 |
5 changes: 4 additions & 1 deletion src/main/java/calculator/Application.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package calculator;

import calculator.io.CalculatorIO;

public class Application {

public static void main(String[] args) {
// TODO: 프로그램 구현
CalculatorIO.add();
}
}
22 changes: 22 additions & 0 deletions src/main/java/calculator/global/instance/Messages.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package calculator.global.instance;

public class Messages {

public static final String DESCRIPTION = """
문자열 덧셈 계산기입니다.
쉼표(,) 또는 콜론(:)을 구분자로 가지는 문자열을 전달하는 경우 구분자를 기준으로 분리한 각 숫자의 합을 반환한다.
앞의 기본 구분자(쉼표, 콜론) 외에 커스텀 구분자를 지정할 수 있다. 커스텀 구분자는 문자열 앞부분의 "//"와 "\\n" 사이에 위치하는 문자를 커스텀 구분자로 사용한다.
또한 커스텀 구분자를 여러 개 사용하고 싶으면 | 를 이용해서 구분해서 등록해주세요.
(단, | 는 커스텀 구분자로 사용이 불가능합니다.)
""";
public static final String INPUT_PROMPT = "덧셈할 문자열을 입력해 주세요.";
public static final String RESULT = "결과 : ";

public static final String INVALID_DELIMITER_ERROR = "구분자가 적절하지 않습니다";
public static final String INVALID_CHARACTER_ERROR = "글자는 들어올 수 없습니다";
public static final String NEGATIVE_NUMBER_ERROR = "음수는 허용되지 않습니다";
public static final String INVALID_STARTING_CHARACTER_ERROR = "잘못된 형식입니다. 숫자 또는 '//'로 시작해야 합니다.";
public static final String MISSING_NEWLINE_AFTER_CUSTOM_DELIMITER_ERROR = "잘못된 형식입니다. 커스텀 구분자 뒤에 '\\n'이 있어야 합니다.";
public static final String NUMBER_AS_CUSTOM_DELIMITER_ERROR = "숫자는 커스텀 구분자로 사용할 수 없습니다";
public static final String PIPE_MISUSED_AS_DELIMITER_ERROR = "커스텀 구분자 등록의 형식이 잘못되었습니다. 파이프(`|`)는 커스텀 구분자를 구분하는 용도로만 사용할 수 있습니다";
}
23 changes: 23 additions & 0 deletions src/main/java/calculator/io/CalculatorIO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package calculator.io;

import static calculator.global.instance.Messages.DESCRIPTION;
import static calculator.global.instance.Messages.INPUT_PROMPT;
import static calculator.global.instance.Messages.RESULT;

import calculator.service.CalculatorService;
import camp.nextstep.edu.missionutils.Console;

public class CalculatorIO {

public static void add() {
System.out.println(DESCRIPTION);
System.out.println(INPUT_PROMPT);
try {
String stringInput = Console.readLine(); // 사용자 입력 받기
int result = CalculatorService.add(stringInput); // 덧셈 계산
System.out.println(RESULT + result);
} finally {
Console.close(); // Scanner 자원 해제
}
}
}
75 changes: 75 additions & 0 deletions src/main/java/calculator/service/CalculatorService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package calculator.service;

import calculator.global.instance.Messages;

public class CalculatorService {

public static int add(String input) {
if (input == null || input.isEmpty()) {
return 0; // 빈 문자열 또는 null이면 0 반환
}

// 입력에서 \\n을 실제 개행 문자로 변환
input = input.replace("\\n", "\n");

String delimiter = ",|:"; // 기본 구분자 쉼표와 콜론
if (input.startsWith("//")) {
int delimiterIndex = input.indexOf("\n"); // 실제 개행 문자를 찾음
if (delimiterIndex == -1) {
throw new IllegalArgumentException(Messages.MISSING_NEWLINE_AFTER_CUSTOM_DELIMITER_ERROR);
}
String customDelimiter = input.substring(2, delimiterIndex); // 커스텀 구분자 추출
if (!customDelimiter.isEmpty()) {
delimiter = extractCustomDelimiters(customDelimiter); // 커스텀 구분자가 있는 경우 추출
}
input = input.substring(delimiterIndex + 1); // 개행 뒤의 숫자 부분만 남김
}

if (input.isEmpty()) {
return 0; // 숫자가 없으면 0 반환
}

// 입력 값을 구분자로 분리한 후 계산
String[] numbers = input.split(delimiter);
int sum = 0;

for (String number : numbers) {
if (!number.trim().isEmpty()) { // 빈 값이 아닌 경우만 처리
int num = parsePositiveInt(number.trim()); // 양수만 허용
sum += num;
}
}

return sum;
}

private static String extractCustomDelimiters(String delimiterPart) {
if (delimiterPart.contains("|")) {
String[] customDelimiters = delimiterPart.split("\\|");
for (int i = 0; i < customDelimiters.length; i++) {
customDelimiters[i] = escapeSpecialChars(customDelimiters[i]); // 특수 문자를 이스케이프
}
return String.join("|", customDelimiters);
} else {
return escapeSpecialChars(delimiterPart); // 단일 구분자인 경우도 이스케이프 처리
}
}

// 정규식 메타 문자를 이스케이프 처리하는 메소드
private static String escapeSpecialChars(String delimiter) {
return delimiter.replaceAll("([\\[\\]{}()*+?^$\\\\.|])", "\\\\$1");
}

// 양수만 허용하고 숫자가 아니면 예외 처리
private static int parsePositiveInt(String number) {
try {
int num = Integer.parseInt(number);
if (num < 0) {
throw new IllegalArgumentException(Messages.NEGATIVE_NUMBER_ERROR);
}
return num;
} catch (NumberFormatException e) {
throw new IllegalArgumentException(Messages.INVALID_CHARACTER_ERROR);
}
}
}
213 changes: 213 additions & 0 deletions src/test/java/calculator/service/CalculatorServiceTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
package calculator.service;

import static calculator.global.instance.Messages.INVALID_CHARACTER_ERROR;
import static calculator.global.instance.Messages.MISSING_NEWLINE_AFTER_CUSTOM_DELIMITER_ERROR;
import static calculator.global.instance.Messages.NEGATIVE_NUMBER_ERROR;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;

@DisplayName("계산기 로직 테스트")
class CalculatorServiceTest {

@DisplayName("구분자가 잘못 들어 왔을 시, 에러 발생")
@ParameterizedTest
@ValueSource(strings = {"1;2", // 세미콜론은 기본 구분자가 아님
"1|2", // 파이프는 기본 구분자가 아님
"1 2", // 공백은 기본 구분자가 아님
"1/2" // 슬래시는 기본 구분자가 아님
})
void add_invalidDelimiter_throwsException(String input) {
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> CalculatorService.add(input));

assertEquals(INVALID_CHARACTER_ERROR, exception.getMessage());
}

@DisplayName("여러 숫자 입력에 대한 구분자가 잘못 들어 왔을 시, 에러 발생")
@ParameterizedTest
@CsvSource({"'1,2;3,4'", // 첫 번째 잘못된 구분자 (세미콜론)
"'1,2,3;4'", // 두 번째 잘못된 구분자 (세미콜론)
"'1;2,3,4'", // 첫 번째 잘못된 구분자 (세미콜론)
"'1,2:3|4'", // 네 번째 잘못된 구분자 (파이프)
"'1:2,3/4'", // 네 번째 잘못된 구분자 (슬래시)
"'9:10,11/12'", // 네 번째 잘못된 구분자 (슬래시), 네 개의 숫자
"'9:10,11/12,13'", // 네 번째 잘못된 구분자 (슬래시), 네 개의 숫자
})
void add_validAndInvalidMixedDelimiters_throwsException(String input) {
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> CalculatorService.add(input));

assertEquals(INVALID_CHARACTER_ERROR, exception.getMessage());
}

@DisplayName("구분자 이외의 글자 입력이 들어오면, 에러 발생")
@ParameterizedTest
@CsvSource({"'1a2,3'", // 영어 'a'가 구분자로 들어온 경우
"'1,2b3'", // 영어 'b'가 구분자로 들어온 경우
"'1가2:3'", // 한글 '가'가 구분자로 들어온 경우
"'1,나2,3'", // 한글 '나'가 구분자로 들어온 경우
"'1,2c3,4'", // 영어 'c'가 구분자로 들어온 경우
"'1,2,3한4'", // 한글 '한'이 구분자로 들어온 경우
"'1d2:3,4'" // 영어 'd'가 구분자로 들어온 경우
})
void add_invalidCharacterDelimiter_throwsException(String input) {

IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> CalculatorService.add(input));

assertEquals(INVALID_CHARACTER_ERROR, exception.getMessage());
}

@DisplayName("음수 입력이 들어오면, 에러 발생")
@ParameterizedTest
@ValueSource(strings = {"1,-2,3", // 음수가 두 번째 위치
"-1,2,3", // 음수가 첫 번째 위치
"1,2,-3", // 음수가 세 번째 위치
"-1,-2,3", // 음수가 첫 번째와 두 번째 위치
"1,-2:-3" // 음수가 두 번째와 세 번째 위치, 콜론과 쉼표 혼합
})
void add_negativeNumber_throwsException(String input) {
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> CalculatorService.add(input));

assertEquals(NEGATIVE_NUMBER_ERROR, exception.getMessage());
}

@DisplayName("입력 문자열의 시작이 잘못되면, 에러 발생")
@ParameterizedTest
@ValueSource(strings = {"a1,2,3", // 첫 글자가 문자
"#1,2,3", // 첫 글자가 특수 문자
"@//;\n1;2;3", // 커스텀 구분자가 아닌 특수 문자로 시작
";1,2,3" // 잘못된 특수 문자로 시작
})
void add_invalidStartingCharacter_throwsException(String input) {
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> CalculatorService.add(input));

assertEquals(INVALID_CHARACTER_ERROR, exception.getMessage());
}

@DisplayName("커스텀 구분자 정의 시 '\\n'이 누락되면, IllegalArgumentException 발생")
@ParameterizedTest
@ValueSource(strings = {"//;1;2;3", // '\n'이 없고 바로 숫자가 나옴
"//|1|2|3", // '\n'이 없고 바로 숫자가 나옴
"//.123", // '\n'이 없이 바로 숫자가 나옴
"//#1#2" // '\n'이 없고 바로 숫자가 나옴
})
void add_missingNewlineAfterCustomDelimiter_throwsException(String input) {
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> CalculatorService.add(input));

assertEquals(MISSING_NEWLINE_AFTER_CUSTOM_DELIMITER_ERROR, exception.getMessage());
}

@DisplayName("숫자가 커스텀 구분자로 사용되면 IllegalArgumentException 발생")
@ParameterizedTest
@ValueSource(strings = {"//1\n1,2,3", // 숫자 '1'이 커스텀 구분자로 사용됨
"//2\n2,3,4", // 숫자 '2'가 커스텀 구분자로 사용됨
"//3\n3:4:5", // 숫자 '3'이 커스텀 구분자로 사용됨
})
void add_withNumberAsCustomDelimiter_throwsException(String input) {
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> CalculatorService.add(input));

assertEquals(INVALID_CHARACTER_ERROR, exception.getMessage());
}

@DisplayName("파이프(`|`)가 구분자 외에 다른 위치에서 사용되면 IllegalArgumentException 발생")
@ParameterizedTest
@ValueSource(strings = {"//|\n1,2,3", // | 는 커스텀 구분자가 될 수 없음
"//||\n2,3,4", // | 는 커스텀 구분자가 될 수 없음
"//|||\n3:4:5", // | 는 커스텀 구분자가 될 수 없음
})
void add_withNumberOrPipeMisusedAsCustomDelimiter_throwsException(String input) {
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> CalculatorService.add(input));

assertEquals(INVALID_CHARACTER_ERROR, exception.getMessage());
}

@DisplayName("커스텀 구분자에 대한 성공 로직 테스트")
@ParameterizedTest
@CsvSource({"'//;\n1;2;3', 6", // 세미콜론을 커스텀 구분자로 사용
"'//@\n2@3@4', 9", // @를 커스텀 구분자로 사용
"'//#\n1#2#3#4', 10", // #을 커스텀 구분자로 사용
"'//.\n5.6.7', 18" // 점(.)을 커스텀 구분자로 사용
})
void add_withCustomDelimiters_returnsSum(String input, int expectedSum) {
int result = CalculatorService.add(input);
assertEquals(expectedSum, result);
}

@DisplayName("구분자에 대한 성공 로직 테스트")
@ParameterizedTest
@CsvSource({"'1,2,3', 6", // 쉼표 구분자
"'1:2:3', 6", // 콜론 구분자
"'1,2:3', 6", // 쉼표와 콜론 혼합 구분자
})
void add_withValidInput_returnsSum(String input, int expectedSum) {
int result = CalculatorService.add(input);
assertEquals(expectedSum, result);
}

@DisplayName("커스텀 구분자를 사용하더라도 값이 없으면 0을 반환")
@ParameterizedTest
@ValueSource(strings = {"//;\n", // 커스텀 구분자만 있고 숫자가 없음
"//#\n", // 커스텀 구분자만 있고 숫자가 없음
"//@\n", // 커스텀 구분자만 있고 숫자가 없음
})
void add_customDelimiterWithNoValues_returnsZero(String input) {
int result = CalculatorService.add(input);
assertEquals(0, result);
}

@DisplayName("커스텀 구분자를 사용하고 숫자가 하나만 들어왔을 때 해당 숫자를 반환")
@ParameterizedTest
@CsvSource({"'//;\n5', 5", // 세미콜론 구분자와 숫자 5
"'//#\n15', 15", // 샵 구분자와 숫자 15
"'//@\n20', 20", // 앳 구분자와 숫자 20
"'//.\n25', 25" // 점 구분자와 숫자 25
})
void add_customDelimiterWithSingleNumber_returnsNumber(String input, int expected) {
int result = CalculatorService.add(input);
assertEquals(expected, result);
}

@DisplayName("커스텀 구분자가 없는 경우 성공 로직 테스트")
@ParameterizedTest
@CsvSource({"'//\n1,2,3', 6", // 커스텀 구분자가 없고 쉼표 구분자 사용
"'//\n1:2:3', 6", // 커스텀 구분자가 없고 콜론 구분자 사용
"'//\n4,5:6', 15" // 커스텀 구분자가 없고 쉼표와 콜론 혼합 사용
})
void add_withEmptyCustomDelimiter_returnsSum(String input, int expectedSum) {
int result = CalculatorService.add(input);
assertEquals(expectedSum, result);
}

@DisplayName("글자 커스텀 구분자를 사용한 성공 로직 테스트")
@ParameterizedTest
@CsvSource({"'//a\n1a2a3', 6", // 'a'를 커스텀 구분자로 사용
"'//ㄱ\n1ㄱ2ㄱ3', 6", // 'ㄱ'를 커스텀 구분자로 사용
"'//가\n1가2가3', 6" // '가'를 커스텀 구분자로 사용
})
void add_withCustomLetterDelimiters_returnsSum(String input, int expectedSum) {
int result = CalculatorService.add(input);
assertEquals(expectedSum, result);
}

@DisplayName("여러 커스텀 구분자를 파이프로 구분하여 처리하는 성공 로직 테스트")
@ParameterizedTest
@CsvSource({"'//#|*|%\n5#6*7%8', 26", // 해시, 별표, 퍼센트를 커스텀 구분자로 사용
"'//@|!|^\n9@10!11^12', 42", // 앳, 느낌표, 캐럿을 커스텀 구분자로 사용
"'//가|나|다\n1가2나3다4', 10" // 한글 구분자 (가, 나, 다) 사용
})
void add_withMultipleCustomDelimitersUsingPipe_returnsSum(String input, int expectedSum) {
int result = CalculatorService.add(input);
assertEquals(expectedSum, result);
}
}