diff --git a/README.md b/README.md index bd90ef0247..0f69ecd135 100644 --- a/README.md +++ b/README.md @@ -1 +1,49 @@ -# java-calculator-precourse \ No newline at end of file +# 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`만 허용, **공백 자체를 구분자로 지정하지 않음** +* 프로그램은 **한 번 입력을 처리하고 종료** diff --git a/src/main/java/calculator/Application.java b/src/main/java/calculator/Application.java index 573580fb40..5220e633b8 100644 --- a/src/main/java/calculator/Application.java +++ b/src/main/java/calculator/Application.java @@ -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); } -} +} \ No newline at end of file diff --git a/src/main/java/calculator/StringCalculator.java b/src/main/java/calculator/StringCalculator.java new file mode 100644 index 0000000000..5a71f945c4 --- /dev/null +++ b/src/main/java/calculator/StringCalculator.java @@ -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 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 split(ParseSpec spec) { + String s = spec.numbers; + List 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 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 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; + } + } +}