Skip to content
Open
50 changes: 49 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,49 @@
# java-calculator-precourse
# java-calculator-precourse

# 기능 목록

## 범위

* 입력: `camp.nextstep.edu.missionutils.Console.readLine()` 한 줄
* 출력 형식: **`결과 : <합계>`** (공백/콜론 위치 정확히 일치)
* JDK 21, `Application.main()` 시작, `System.exit()` 금지, `build.gradle` 수정 금지

---

## 기능 목록 (구현 순서 권장)

1. **입력 안내 & 입력**

* `덧셈할 문자열을 입력해 주세요.` 출력 후 한 줄 입력 수신
* 입력을 trim()한 결과가 비어 있으면 `0`
2. **커스텀 구분자 감지**

* 포맷: `//<구분자>\n<숫자들>` (개행은 실제 `\n` 또는 리터럴 `\\n` 모두 허용)
* `//`와 개행 사이 **단일 문자**만 허용(다문자/다중 구분자 불가)
3. **토큰 분리**

* 커스텀 구분자 존재 시: **해당 문자로만** 분리(기본 구분자 비활성화)
* 커스텀 구분자 없음: 기본 구분자 `,` 또는 `:` 로 분리
* 각 토큰 `trim()` 처리
4. **유효성 검사**

* **양의 정수(> 0)만 허용**
* **0, 음수, 실수(소수점), 숫자 아님, 중간 빈 토큰** ⇒ `IllegalArgumentException`
* 커스텀 구분자가 숫자/공백/개행이면 ⇒ `IllegalArgumentException`
5. **합계 계산, 출력**

* 모든 유효 토큰 합산 후 정확한 포맷으로 출력
6. **예외 전파**

* 잘못된 입력 시 IllegalArgumentException 발생 → catch 하지 않고 전파
* `System.exit()` 호출 금지, 스택트레이스 출력도 불필요

---

## 구현 결정(Assumptions)

* 커스텀 구분자: **단일 문자만 허용**, 다문자(`//st\n`)·다중 지정 미지원
* 커스텀 사용 시 기본 구분자(, :) **혼용 불가** (`//;\n1;2,3` ⇒ 예외)
* 숫자 형식: **양의 정수**만 허용(**0/음수/실수 불가**)
* 공백은 토큰 양끝 `trim`만 허용, **공백 자체를 구분자로 지정하지 않음**
* 프로그램은 **한 번 입력을 처리하고 종료**
8 changes: 6 additions & 2 deletions src/main/java/calculator/Application.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package calculator;
import camp.nextstep.edu.missionutils.Console;

public class Application {
public static void main(String[] args) {
// TODO: 프로그램 구현
System.out.println("덧셈할 문자열을 입력해 주세요.");
String input = Console.readLine(); // 한 줄 입력
int result = StringCalculator.add(input); // 예외는 전파(잡지 않음)
System.out.println("결과 : " + result);
}
}
}
127 changes: 127 additions & 0 deletions src/main/java/calculator/StringCalculator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package calculator;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

public final class StringCalculator {

private StringCalculator() { }

public static int add(String input) {
if (input == null || input.trim().isEmpty()) {
return 0; // 공백만 입력 포함
}
String s = input.trim();

ParseSpec spec = parseSpec(s); // 커스텀 구분자 헤더 파싱
validateCustomDelimiter(spec); // 커스텀 구분자 유효성 검사

if (spec.numbers.trim().isEmpty()) {
return 0;
}

List<String> tokens = split(spec); // 토큰 분리 및 trim
validateTokens(tokens); // 토큰 유효성 검사

return sum(tokens); // 합산
}

// --- helper: header parsing ---
private static ParseSpec parseSpec(String s) {
if (!s.startsWith("//")) {
return new ParseSpec(false, '\0', s);
}

// 실제 개행 '\n' 또는 리터럴 "\\n" 모두 허용
int idx = s.indexOf('\n');
int sepLen = 1; // 구분 문자열 길이
if (idx < 0) {
idx = s.indexOf("\\n");
sepLen = 2;
}
if (idx < 0) {
throw new IllegalArgumentException("custom delimiter format must contain newline");
}

String header = s.substring(2, idx);
if (header.length() != 1) {
throw new IllegalArgumentException("custom delimiter must be single character");
}
char d = header.charAt(0);
String numbers = s.substring(idx + sepLen);
return new ParseSpec(true, d, numbers);
}

// --- helper: custom delimiter validation ---
private static void validateCustomDelimiter(ParseSpec spec) {
if (!spec.hasCustom) {
return;
}
char d = spec.delimiter;
if (Character.isDigit(d) || Character.isWhitespace(d) || d == '\n' || d == '\r') {
throw new IllegalArgumentException("invalid custom delimiter");
}
}

// --- helper: splitting ---
private static List<String> split(ParseSpec spec) {
String s = spec.numbers;
List<String> out = new ArrayList<>();
if (spec.hasCustom) {
String regex = Pattern.quote(String.valueOf(spec.delimiter));
for (String t : s.split(regex, -1)) {
out.add(t.trim());
}
} else {
for (String t : s.split("[,:]", -1)) {
out.add(t.trim());
}
}
return out;
}

// --- helper: token validation ---
private static void validateTokens(List<String> tokens) {
for (String t : tokens) {
if (t.isEmpty()) {
throw new IllegalArgumentException("empty token");
}
// 정수만 허용(부호/소수점/문자 불가)
if (!t.chars().allMatch(Character::isDigit)) {
throw new IllegalArgumentException("non-integer: " + t);
}
int v;
try {
v = Integer.parseInt(t);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("invalid integer: " + t);
}
if (v <= 0) {
throw new IllegalArgumentException("only positive integers (>0) allowed: " + t);
}
}
}

// --- helper: summation ---
private static int sum(List<String> tokens) {
int acc = 0;
for (String t : tokens) {
acc += Integer.parseInt(t);
}
return acc;
}

// --- data holder ---
private static final class ParseSpec {
final boolean hasCustom;
final char delimiter;
final String numbers;

ParseSpec(boolean hasCustom, char delimiter, String numbers) {
this.hasCustom = hasCustom;
this.delimiter = delimiter;
this.numbers = numbers;
}
}
}