diff --git a/README.md b/README.md index 13420b29..43fe18c0 100644 --- a/README.md +++ b/README.md @@ -1 +1,31 @@ -# javascript-calculator-precourse \ No newline at end of file +# 1주차 미션: 문자열 덧셈 계산기 + +### 기능 요구 사항 + +--- + +입력한 문자열에서 숫자를 추출하여 더하는 계산기를 구현한다. + +- 쉼표(,) 또는 콜론(:)을 구분자로 가지는 문자열을 전달하는 경우 구분자를 기준으로 분리한 각 숫자의 합을 반환한다. + + - 예: "" => 0, "1,2" => 3, "1,2,3" => 6, "1,2:3" => 6 + +- 앞의 기본 구분자(쉼표, 콜론) 외에 커스텀 구분자를 지정할 수 있다. 커스텀 구분자는 문자열 앞부분의 "//"와 "\n" 사이에 위치하는 문자를 커스텀 구분자로 사용한다. + + - 예를 들어 "//;\n1;2;3"과 같이 값을 입력할 경우 커스텀 구분자는 세미콜론(;)이며, 결과 값은 6이 반환되어야 한다. + +- 사용자가 잘못된 값을 입력할 경우 "[ERROR]"로 시작하는 메시지와 함께 `Error`를 발생시킨 후 애플리케이션은 종료되어야 한다. + +--- + +### 기능 구현 목록 + +--- + +- [ ] 입력 후 정규 표현식을 통해 커스텀 구분자를 확인하는 로직 +- [ ] 구분자와 숫자를 구분하여 합산 구하는 로직 + +- [ ] 입력 전후 요구 사항에 맞게 결과 출력하는 로직 +- [ ] 에러 발생 시 에러 메시지 출력하는 로직 + +--- diff --git a/__tests__/ApplicationTest.js b/__tests__/ApplicationTest.js index 7c6962dd..726eebf5 100644 --- a/__tests__/ApplicationTest.js +++ b/__tests__/ApplicationTest.js @@ -17,6 +17,7 @@ const getLogSpy = () => { }; describe("문자열 계산기", () => { + // 정답 케이스 test("커스텀 구분자 사용", async () => { const inputs = ["//;\\n1"]; mockQuestions(inputs); @@ -32,7 +33,68 @@ describe("문자열 계산기", () => { }); }); - test("예외 테스트", async () => { + 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("16진수 덧셈", async () => { + const inputs = ["0x01,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("커스텀 구분자 x와 16진수", async () => { + const inputs = ["//x\\n0x01,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 = ["//-\\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 = ["-1,2,3"]; mockQuestions(inputs); @@ -40,4 +102,30 @@ describe("문자열 계산기", () => { await expect(app.run()).rejects.toThrow("[ERROR]"); }); + + test("구분자 마무리", async () => { + const inputs = ["1,2,3,"]; + mockQuestions(inputs); + + const app = new App(); + + await expect(app.run()).rejects.toThrow("[ERROR]"); + }); + + test("숫자 커스텀 구분자", async () => { + const inputs = ["//1\\n1,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 app = new App(); + + await expect(app.run()).rejects.toThrow("[ERROR]"); + }); }); diff --git a/src/App.js b/src/App.js index 091aa0a5..3ff759ac 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,20 @@ +import { Console } from "@woowacourse/mission-utils"; +import { + getCustomSeparator, + getExpression, + executeExpression, +} from "./function.js"; + class App { - async run() {} + async run() { + const input = await Console.readLineAsync("덧셈할 문자열을 입력해 주세요."); + + const separators = getCustomSeparator(input); + const expressions = getExpression(input); + const result = executeExpression(expressions, separators); + + Console.print(`결과 : ${result}`); + } } export default App; diff --git a/src/function.js b/src/function.js new file mode 100644 index 00000000..229fd712 --- /dev/null +++ b/src/function.js @@ -0,0 +1,61 @@ +const pattern = /\/\/(.*)\\n/; + +function hasCustomSeparator(input) { + // 커스텀 구분자를 선언하는 데 이용되는 패턴 선언 + return pattern.test(input); +} + +export function getCustomSeparator(input) { + let separators = [":", ","]; + if (hasCustomSeparator(input)) { + const customSeparators = input.match(pattern)[1]; + isLegelSeparator(customSeparators); + for (const separator of customSeparators) { + separators.push(separator); + } + } + return separators; +} + +export function getExpression(input) { + if (hasCustomSeparator(input)) { + return input.replace(pattern, ""); + } else { + return input; + } +} + +export function executeExpression(expressions, separators) { + let stack = ""; + let result = 0; + + for (const char of expressions) { + if (separators.includes(char)) { + number = Number(stack); + isLegalNumber(number); + result += number; + stack = ""; + } else stack += char; + } + + if (stack === "") throw new Error("[ERROR] 구분자로 마무리될 수 없어요."); + + number = Number(stack); + isLegalNumber(number); + result += Number(stack); + + return result; +} + +function isLegalNumber(number) { + if (number < 0) throw new Error("[ERROR] 양수를 입력해주세요."); + if (Number.isNaN(number)) + throw new Error("[ERROR] 올바른 숫자를 입력해주세요."); +} + +function isLegelSeparator(separators) { + if (separators.length < 1) + throw new Error("[ERROR] 구분자를 올바르게 입력해주세요."); + if (/.*\d.*/.test(separators)) + throw new Error("[ERROR] 숫자를 구분자로 이용할 수 없어요."); +}