Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,32 @@
# javascript-calculator-precourse
# javascript-calculator-precourse

## 문자열 덧셈 계산기

### 구현할 기능 목록

1. 입력 기능 작성
2. 아래 해당되는 구분자로 문자열을 분리하는 함수 생성. ""일 경우 0을 반환
- 기본 구분자(쉼표, 콜론)
- 커스텀 구분자("//"와 "\n" 사이에 위치하는 문자)
3. 구분자로 분리된 문자열을 숫자로 변환하여 더하는 함수 생성
4. 입력 형식에 맞지 않을 경우 "[ERROR]"로 Error 처리
- 유효하지 않은 구분자
- 쉼표, 콜론, "//"와 "\n" 사이에 위치하는 문자가 아닌 경우
- 음수 값
5. 발견된 추가 케이스 구현
- 기본 구분자와 커스텀 구분자가 혼용되어 있는 경우
- 공백을 포함한 경우
- 단일 숫자인 경우
- 연속된 구분자일 경우
- 잘못된 커스텀 구분자 형식
- ERROR
- 숫자가 아닌 문자 포함
- 숫자가 없는 커스텀 구분자 형식
- 구분자 없음

### 발생 문제 정리

- 기본 구분자 이후 커스텀 구분자를 찾게 했는데, 내 코드에선 기본 구분자와 커스텀 구분자가 혼용되어 있는 경우 기본 구분자에서 바로 return을 하기에 커스텀 구분자를 확인하지 못함.
- 커스텀 구분자인지 먼저 확인 후 없으면 기본 구문자 확인으로 변경
- 내가 작성한 테스트 입력들이 `\n`(actual newline)과 `\\n`(backslash + n 문자열) 두 가지 형태여서 오류가 섞여서 발생
- `/^\/\/(.+?)(?:\\n|\n)/`을 사용하여 두 케이스 모두 대응으로 변경
105 changes: 104 additions & 1 deletion __tests__/ApplicationTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,115 @@ describe("문자열 계산기", () => {
});
});

test("예외 테스트", async () => {
test("예외 테스트 - 음수", async () => {
const inputs = ["-1,2,3"];
mockQuestions(inputs);

const app = new App();

await expect(app.run()).rejects.toThrow("[ERROR]");
});

test("기본 구분자와 커스텀 구분자를 혼용하여 사용", async () => {
const inputs = ["//;\n1,2;3"];
mockQuestions(inputs);

const logSpy = getLogSpy();
const outputs = ["결과 : 6"];

const app = new App();
await app.run();

outputs.forEach((output) => {
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(output));
});
});

test("공백 포함", async () => {
const inputs = [",2"];
mockQuestions(inputs);

const logSpy = getLogSpy();
const outputs = ["결과 : 2"];

const app = new App();
await app.run();

outputs.forEach((output) => {
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(output));
});
});

test("공백 포함2", async () => {
const inputs = ["1,2,"];
mockQuestions(inputs);

const logSpy = getLogSpy();
const outputs = ["결과 : 3"];

const app = new App();
await app.run();

outputs.forEach((output) => {
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(output));
});
});

test("단일 숫자", async () => {
const inputs = ["1"];
mockQuestions(inputs);
3;
const logSpy = getLogSpy();
const outputs = ["결과 : 1"];

const app = new App();
await app.run();

outputs.forEach((output) => {
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(output));
});
});

test("연속된 구분자", async () => {
const inputs = ["1,,3"];
mockQuestions(inputs);

const logSpy = getLogSpy();
const outputs = ["결과 : 4"];

const app = new App();
await app.run();

outputs.forEach((output) => {
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining(output));
});
});

test("예외테스트 - 잘못된 커스텀 구분자 형식 - 구분자 없음", async () => {
const inputs = ["//\n1,2,3"];
mockQuestions(inputs);
const app = new App();
await expect(app.run()).rejects.toThrow("[ERROR]");
});

test("예외테스트 - 잘못된 커스텀 구분자 형식 - 숫자 없음", async () => {
const inputs = ["//;\n"];
mockQuestions(inputs);
const app = new App();
await expect(app.run()).rejects.toThrow("[ERROR]");
});

test("예외테스트 - 숫자가 아닌 문자 포함", async () => {
const inputs = ["1,2,abc"];
mockQuestions(inputs);
const app = new App();
await expect(app.run()).rejects.toThrow("[ERROR]");
});

test("예외테스트 - 유효하지 않은 구분자", async () => {
const inputs = ["1@3"];
mockQuestions(inputs);
const app = new App();
await expect(app.run()).rejects.toThrow("[ERROR]");
});
});
93 changes: 92 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,96 @@
import { MissionUtils } from "@woowacourse/mission-utils";

class App {
async run() {}
static DEFAULT_DELIMITERS = /,|:/;
static CUSTOM_DELIMITER_PATTERN = /^\/\/(.+?)(?:\\n|\n)/;
static ERROR_MESSAGES = {
INVALID_DELIMITER: "[ERROR] 유효하지 않은 구분자가 포함되어 있습니다.",
INVALID_NUMBER: "[ERROR] 유효하지 않은 숫자가 포함되어 있습니다.",
};

async run() {
try {
const input = await this.getUserInput();
const result = this.getDelimiters(input);
this.printResult(result);
} catch (error) {
this.printError(error);
throw error;
}
}

async getUserInput() {
return await MissionUtils.Console.readLineAsync(
"덧셈할 문자열을 입력해 주세요.\n"
);
}

// 쉼표, 콜론, 커스텀 구분자로 문자열을 분리하는 함수
getDelimiters(str) {
// 공백 문자열 처리
if (str.trim() === "") return 0;

if (str.startsWith("//")) {
// 커스텀 구분자 형식 확인
const customDelimiterMatch = str.match(App.CUSTOM_DELIMITER_PATTERN);

if (!customDelimiterMatch) {
throw new Error(App.ERROR_MESSAGES.INVALID_DELIMITER);
}

const customDelimiter = customDelimiterMatch[1];
let numbersPart = str.slice(customDelimiterMatch[0].length);

// 숫자 부분이 비어있으면 에러
if (numbersPart.trim() === "") {
throw new Error(App.ERROR_MESSAGES.INVALID_DELIMITER);
}

// 모든 구분자를 쉼표로 통일
numbersPart = numbersPart.replaceAll(customDelimiter, ",");
numbersPart = numbersPart.replaceAll(":", ",");

return this.sumNumbers(numbersPart.split(","));
}

const trimmed = str.trim();
if (!/^[\d,:]+$/.test(trimmed)) {
throw new Error(App.ERROR_MESSAGES.INVALID_DELIMITER);
}

// 기본 구문자 처리
if (str.match(App.DEFAULT_DELIMITERS)) {
return this.sumNumbers(str.split(App.DEFAULT_DELIMITERS));
}
if (!/^\d+$/.test(trimmed)) {
throw new Error(App.ERROR_MESSAGES.INVALID_DELIMITER);
}

return this.sumNumbers([trimmed]);
}

// 구분자로 분리된 문자열을 숫자로 변환하여 더하는 함수
sumNumbers(stringNumbers) {
return stringNumbers.reduce((acc, num) => {
// 빈 문자열 처리
if (num.trim() === "") return acc;
const parsed = parseInt(num.trim(), 10);

// 음수 처리
if (isNaN(parsed) || parsed < 0) {
throw new Error(App.ERROR_MESSAGES.INVALID_NUMBER);
}
return acc + parsed;
}, 0);
}

printResult(result) {
MissionUtils.Console.print(`결과 : ${result}`);
}

printError(error) {
MissionUtils.Console.print(error.message);
}
}

export default App;