From b74ee47f7dd555f9d9446e15588180dd8c06c989 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 20 Oct 2025 01:12:51 +0900 Subject: [PATCH 01/12] =?UTF-8?q?docs(readme):=20=EC=84=A4=EA=B3=84=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B8=B0=EB=8A=A5=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 학습 목표, 전체적인 개요, 설계, 기능 목록과 설계 과정에서 고민한 문제해결 과정을 README.md에 작성하였습니다. --- README.md | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 13420b29..b5cd4f76 100644 --- a/README.md +++ b/README.md @@ -1 +1,56 @@ -# javascript-calculator-precourse \ No newline at end of file +# javascript-calculator-precourse +# 학습 목표 +- `개발 환경`에 익숙해진다. (git, vscode, node) +- `문자열 덧셈 계산기 문제`를 해결한다. +# 개요 +### 개발 환경 +- 미션 저장소를 포크하여 개발을 진행할 개인 저장소를 만든다. +- `git clone`으로 개인 저장소에 대한 로컬 저장소를 만든다. +- 하나의 기능을 단위로 커밋을 생성한다. (add -> commit -> push) + 커밋 메세지는 AngularJS Git Commit Message Conventions를 기준으로 작성한다. +### 문자열 덧셈 계산기 +`입력받은 문자열에서 구분자를 기준으로 숫자를 분리해 덧셈 기능을 제공하는 계산기` +- 쉼표(,) 또는 콜론(:)을 구분자로 가지는 문자열을 전달하는 경우 구분자를 기준으로 분리한 각 숫자의 합을 반환한다. + 예: "" => 0, "1,2" => 3, "1,2,3" => 6, "1,2:3" => 6 +- 앞의 기본 구분자(쉼표, 콜론) 외에 커스텀 구분자를 지정할 수 있다. 커스텀 구분자는 문자열 앞부분의 "//"와 "\n" 사이에 위치하는 문자를 커스텀 구분자로 사용한다. +- 예를 들어 "//;\n1;2;3"과 같이 값을 입력할 경우 커스텀 구분자는 세미콜론(;)이며, 결과 값은 6이 반환되어야 한다. +사용자가 잘못된 값을 입력할 경우 "[ERROR]"로 시작하는 메시지와 함께 Error를 발생시킨 후 애플리케이션은 종료되어야 한다. +# 설계 +## 흐름 +```mermaid +flowchart LR + A[입력] --> B[파싱] --> C[검증] --> D[연산] --> E[출력] +``` +## 기능 목록(커밋 기준) +- [ ]입력 + - [ ] @woowacourse/mission-utils에서 제공하는 Console API를 사용하여 입력을 받는다. + - [ ] 문자열의 좌우 공백을 없앤다. +- [ ]파싱 + - [ ] 구분자 식별 + - `기본 구분자`(`,`, `:`) + - 문자열 앞부분의 "//"와 "\n"가 존재하면 그 사이의 문자를 `커스텀 구분자`로 추가한다. + - 숫자 추출 + - 구분자를 통해 문자열의 숫자를 추출한다. +- [ ]검증 & 예외처리 + - [ ] 숫자들이 대해 0이나 양의 정수인지 검증한다. (다른 문자, 음수, 소수 x) + - [ ] 구분자의 공백이 아닌 길이가 1인 하나의 문자인지 검증한다. + - [ ] 구분자가 숫자가 아님을 검증한다. + - [ ] 커스텀 구분자가 하나임을 검증한다.(여러 번 지정 불가능) +- [ ]연산(덧셈) + - [ ] 숫자들을 모두 더한다. +- [ ]출력 + - [ ] @woowacourse/mission-utils에서 제공하는 Console API를 사용하여 출력한다. + ``` + 결과 : 6 + ``` + + +# 문제해결과정 +### 설계 단계 +> 파싱과 검증의 순서 +- 해결해야하는 문제: 파싱과 검증의 순서를 설계하는 과정에서, 검증해야할 예외들 중에 빈문자열과 같이 파싱을 하지 않아도 할 수 있는 부분들을 어떻게 처리할 지 고민이 되었습니다. +- 어떻게 해결헀는지: `사전 검증`과 파싱 후`구조 검증`으로 나누면 불필요한 연산을 줄이고 안정적으로 처리할 수 있을 것이라고 생각합니다. +> 커스텀 구분자의 다양한 반례 +- 해결해야하는 문제 : 커스텀 구분자에 대한 제한에 따라 다양한 예외들이 나오게 됩니다. 구분자의 종류, 개수에 따라 다양한 상황(`.` 허용시 소수문제, `//`허용시 구분자의 모호성)이 발생합니다. +- 어떻게 해결했는지: 명확성과 안전을 위해서 엄격한 제한을 두도록 합니다. 소수 입력을 제한하여 공백과 숫자를 제외한 문자를 공백으로 허용합니다. 그리고 문자는 하나만 가능합니다. +### 구현 단계 From 221c1bb3586d45da94db0e116a99ca247af0e68f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 20 Oct 2025 17:13:07 +0900 Subject: [PATCH 02/12] =?UTF-8?q?feat(app):=20mission-utils=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=EC=9E=85=EB=A0=A5=20=ED=95=A8=EC=88=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 비동기 입력을 받는 getInputUsingWoowaMissionUtils를 App 클래스에 추가하였습니다. 재사용성과 테스트를 고려하여 입력을 받을 때 질문으로 출력되는 문자열을 인자로 받도록하였습니다. App 클래스의 run() 함수에서 해당 입력함수를 실행하고 trim 함수로 좌우 공백을 지워주도록 했습니다. \n 입력에 대한 테스트 케이스를 통해 원하는 실행을 확인했습니다. --- README.md | 14 +++++++------- __tests__/InputTest.js | 30 ++++++++++++++++++++++++++++++ src/App.js | 10 +++++++++- 3 files changed, 46 insertions(+), 8 deletions(-) create mode 100644 __tests__/InputTest.js diff --git a/README.md b/README.md index b5cd4f76..6b1afd7e 100644 --- a/README.md +++ b/README.md @@ -22,23 +22,23 @@ flowchart LR A[입력] --> B[파싱] --> C[검증] --> D[연산] --> E[출력] ``` ## 기능 목록(커밋 기준) -- [ ]입력 - - [ ] @woowacourse/mission-utils에서 제공하는 Console API를 사용하여 입력을 받는다. - - [ ] 문자열의 좌우 공백을 없앤다. -- [ ]파싱 +- [x] 입력 + - [x] @woowacourse/mission-utils에서 제공하는 Console API를 사용하여 입력을 받는다. + - [x] 문자열의 좌우 공백을 없앤다. +- [ ] 파싱 - [ ] 구분자 식별 - `기본 구분자`(`,`, `:`) - 문자열 앞부분의 "//"와 "\n"가 존재하면 그 사이의 문자를 `커스텀 구분자`로 추가한다. - 숫자 추출 - 구분자를 통해 문자열의 숫자를 추출한다. -- [ ]검증 & 예외처리 +- [ ] 검증 & 예외처리 - [ ] 숫자들이 대해 0이나 양의 정수인지 검증한다. (다른 문자, 음수, 소수 x) - [ ] 구분자의 공백이 아닌 길이가 1인 하나의 문자인지 검증한다. - [ ] 구분자가 숫자가 아님을 검증한다. - [ ] 커스텀 구분자가 하나임을 검증한다.(여러 번 지정 불가능) -- [ ]연산(덧셈) +- [ ] 연산(덧셈) - [ ] 숫자들을 모두 더한다. -- [ ]출력 +- [ ] 출력 - [ ] @woowacourse/mission-utils에서 제공하는 Console API를 사용하여 출력한다. ``` 결과 : 6 diff --git a/__tests__/InputTest.js b/__tests__/InputTest.js new file mode 100644 index 00000000..c8b72fe9 --- /dev/null +++ b/__tests__/InputTest.js @@ -0,0 +1,30 @@ +import App from "../src/App.js"; +import { MissionUtils } from "@woowacourse/mission-utils"; + +const mockQuestions = (inputs) => { + MissionUtils.Console.readLineAsync = jest.fn(); + + MissionUtils.Console.readLineAsync.mockImplementation(() => { + const input = inputs.shift(); + return Promise.resolve(input); + }); +}; + +describe("문자열 계산기", () => { + test("입력이 들어오는 지 확인", async () => { + const inputs = ["1,2,3"]; + mockQuestions(inputs); + const app = new App(); + const testingInput = await app.getInputUsingWoowaMissionUtils("덧셈할 문자열을 입력해 주세요."); + + expect(testingInput).toBe("1,2,3"); + }); + test("좌우 공백이 존재하는 입력이 그대로 들어오는 지 확인", async () => { + const inputs = [" 1,2,3 "]; + mockQuestions(inputs); + const app = new App(); + const testingInput = await app.getInputUsingWoowaMissionUtils("덧셈할 문자열을 입력해 주세요."); + + expect(testingInput).toBe(" 1,2,3 "); + }); +}); diff --git a/src/App.js b/src/App.js index 091aa0a5..12cfdae8 100644 --- a/src/App.js +++ b/src/App.js @@ -1,5 +1,13 @@ +import { Console } from "@woowacourse/mission-utils"; + class App { - async run() {} + async run() { + const rawInput = await this.getInputUsingWoowaMissionUtils("덧셈할 문자열을 입력해 주세요.").trim(); // 계산해야할 문자열 + } + + async getInputUsingWoowaMissionUtils(questionString) { // @woowacourse/mission-utils의 Console.readLineAsync 함수로 비동기 입력 받기 + return await Console.readLineAsync(questionString); + } } export default App; From 5ede6f66a9a68a945983b4a22d02a1ce884b24d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 20 Oct 2025 18:01:09 +0900 Subject: [PATCH 03/12] =?UTF-8?q?feat(app):=20=EB=B9=88=20=EB=AC=B8?= =?UTF-8?q?=EC=9E=90=EC=97=B4=20=EA=B2=80=EC=A6=9D=20=ED=95=A8=EC=88=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 파싱하기 전에 빈 문자열에 대해 검증하는 함수를 작성했습니다. --- __tests__/InputTest.js | 8 +++++++- src/App.js | 7 ++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/__tests__/InputTest.js b/__tests__/InputTest.js index c8b72fe9..42fff215 100644 --- a/__tests__/InputTest.js +++ b/__tests__/InputTest.js @@ -10,7 +10,7 @@ const mockQuestions = (inputs) => { }); }; -describe("문자열 계산기", () => { +describe("문자열 계산기 입력", () => { test("입력이 들어오는 지 확인", async () => { const inputs = ["1,2,3"]; mockQuestions(inputs); @@ -27,4 +27,10 @@ describe("문자열 계산기", () => { expect(testingInput).toBe(" 1,2,3 "); }); + test("입력된 값이 비어있는지 검증", async () => { + const app = new App(); + expect(() => app.checkStringIsEmpty(null)).toThrow("[ERORR] 아무것도 입력되지 않았습니다."); + expect(() => app.checkStringIsEmpty(" ")).toThrow("[ERORR] 아무것도 입력되지 않았습니다."); + expect(() => app.checkStringIsEmpty(undefined)).toThrow("[ERORR] 아무것도 입력되지 않았습니다."); + }); }); diff --git a/src/App.js b/src/App.js index 12cfdae8..b4a310bc 100644 --- a/src/App.js +++ b/src/App.js @@ -2,12 +2,17 @@ import { Console } from "@woowacourse/mission-utils"; class App { async run() { - const rawInput = await this.getInputUsingWoowaMissionUtils("덧셈할 문자열을 입력해 주세요.").trim(); // 계산해야할 문자열 + const rawInput = (await this.getInputUsingWoowaMissionUtils("덧셈할 문자열을 입력해 주세요.")).trim(); // 계산해야할 문자열 + this.checkStringIsEmpty(rawInput, "[ERROR] 아무것도 입력되지 않았습니다."); // 빈 입력값에 대한 사전검증 } async getInputUsingWoowaMissionUtils(questionString) { // @woowacourse/mission-utils의 Console.readLineAsync 함수로 비동기 입력 받기 return await Console.readLineAsync(questionString); } + + checkStringIsEmpty(str, errMsg) { // 내용과 상관없는 빈 string 자체에 대한 검증 함수 + if(!str || str.trim() === "") throw new Error(errMsg); + } } export default App; From 667fde3e2244b46c7716e4d0c35f57abc386fe36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 20 Oct 2025 21:15:49 +0900 Subject: [PATCH 04/12] =?UTF-8?q?feat(parser):=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80=20=EA=B5=AC=EB=B6=84=EC=9E=90=20=EC=B6=94=EC=B6=9C=20?= =?UTF-8?q?=ED=95=A8=EC=88=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 입력값을 파싱하기 위한 parser 클래스를 추가하고, 정규표현식을 사용해 커스텀 구분자를 추출하는 extractCustomDelimiterStrictly 함수를 추가했습니다. --- README.md | 4 ++-- __tests__/InputTest.js | 7 ++++--- __tests__/ParserTest.js | 25 +++++++++++++++++++++++++ src/Parser.js | 21 +++++++++++++++++++++ 4 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 __tests__/ParserTest.js create mode 100644 src/Parser.js diff --git a/README.md b/README.md index 6b1afd7e..1ee009f2 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,10 @@ flowchart LR - [x] @woowacourse/mission-utils에서 제공하는 Console API를 사용하여 입력을 받는다. - [x] 문자열의 좌우 공백을 없앤다. - [ ] 파싱 - - [ ] 구분자 식별 + - [x] 구분자 식별 - `기본 구분자`(`,`, `:`) - 문자열 앞부분의 "//"와 "\n"가 존재하면 그 사이의 문자를 `커스텀 구분자`로 추가한다. - - 숫자 추출 + - [ ] 숫자 추출 - 구분자를 통해 문자열의 숫자를 추출한다. - [ ] 검증 & 예외처리 - [ ] 숫자들이 대해 0이나 양의 정수인지 검증한다. (다른 문자, 음수, 소수 x) diff --git a/__tests__/InputTest.js b/__tests__/InputTest.js index 42fff215..6faa02fa 100644 --- a/__tests__/InputTest.js +++ b/__tests__/InputTest.js @@ -28,9 +28,10 @@ describe("문자열 계산기 입력", () => { expect(testingInput).toBe(" 1,2,3 "); }); test("입력된 값이 비어있는지 검증", async () => { + const errMsg = "[ERROR] 아무것도 입력되지 않았습니다."; const app = new App(); - expect(() => app.checkStringIsEmpty(null)).toThrow("[ERORR] 아무것도 입력되지 않았습니다."); - expect(() => app.checkStringIsEmpty(" ")).toThrow("[ERORR] 아무것도 입력되지 않았습니다."); - expect(() => app.checkStringIsEmpty(undefined)).toThrow("[ERORR] 아무것도 입력되지 않았습니다."); + expect(() => app.checkStringIsEmpty(null, errMsg)).toThrow("[ERROR] 아무것도 입력되지 않았습니다."); + expect(() => app.checkStringIsEmpty(" ", errMsg)).toThrow("[ERROR] 아무것도 입력되지 않았습니다."); + expect(() => app.checkStringIsEmpty(undefined, errMsg)).toThrow("[ERROR] 아무것도 입력되지 않았습니다."); }); }); diff --git a/__tests__/ParserTest.js b/__tests__/ParserTest.js new file mode 100644 index 00000000..7a60d1b7 --- /dev/null +++ b/__tests__/ParserTest.js @@ -0,0 +1,25 @@ +import Parser from "../src/Parser" +describe("Parser", () => { + test("정상적인 커스텀 구분자 추출 확인", () => { + const parser = new Parser() + const deli = parser.extractCustomDelimiterStrictly("//&\\n123") + expect(deli).toEqual("&"); + }); + // test("비정상적인 커스텀 구분자(2자 이상) 에러 발생 확인", () => { + // const parser = new Parser() + // expect(() => + // parser.extractCustomDelimiterStrictly("//&#\\n123") + // ).toThrow("[ERROR]"); + // }); + // test("비정상적인 커스텀 구분자(숫자) 에러 발생 확인", () => { + // const parser = new Parser() + // expect(() => + // parser.extractCustomDelimiterStrictly("//3\\n123") + // ).toThrow("[ERROR]"); + // }); + // test("커스텀 구분자가 존재하지 않는 경우 에러를 발생하지 않아야 한다.", () => { + // const parser = new Parser() + // const deli = parser.extractCustomDelimiterStrictly("1,2,3") + // expect(deli).toEqual(null); + // }); +}) \ No newline at end of file diff --git a/src/Parser.js b/src/Parser.js new file mode 100644 index 00000000..53ae0b80 --- /dev/null +++ b/src/Parser.js @@ -0,0 +1,21 @@ +class Parser { + constructor(expression) { + // 기본 구분자와 커스텀 구분자 사이에 구별이 필요하지 않기 때문에 Map 보다는 중복을 제거할 수 있는 Set가 더 적합하다고 판단했습니다. + this.delimiterSet = new Set([",", ":"]); + } + + extractCustomDelimiterStrictly(expr) { // 커스텀 구분자를 추출한다(1.구분자는 하나의 문자이며, 2.하나의 종류만 존재한다.) + const matchedDelimiterArray = expr.match(/\/\/(.*)\\n/); + return matchedDelimiterArray[1]; + } + + updateDelimiterSet(customDeli) { + this.delimiterSet.add(customDeli); + } + parseExpressionToNumberList(expression) { + const customDelimiter = this.extractCustomDelimiterStrictly(expression); + if(customDelimiter) this.updateDelimiterSet(customDelimiter); + } +} + +export default Parser; \ No newline at end of file From 094e88cea79a8648b7905926e29bd40a77c9bf48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 20 Oct 2025 21:41:07 +0900 Subject: [PATCH 05/12] =?UTF-8?q?feat(parser):=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80=20=EA=B5=AC=EB=B6=84=EC=9E=90=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 추출한 커스텀 구분자에 대해서 구분자의 길이(1자), 개수(1개), 숫자 불가능을 검증하는 함수를 작성했습니다. --- README.md | 6 ++--- __tests__/ParserTest.js | 52 +++++++++++++++++++++++++++-------------- src/Parser.js | 16 +++++++++---- 3 files changed, 49 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 1ee009f2..907d9825 100644 --- a/README.md +++ b/README.md @@ -33,9 +33,9 @@ flowchart LR - 구분자를 통해 문자열의 숫자를 추출한다. - [ ] 검증 & 예외처리 - [ ] 숫자들이 대해 0이나 양의 정수인지 검증한다. (다른 문자, 음수, 소수 x) - - [ ] 구분자의 공백이 아닌 길이가 1인 하나의 문자인지 검증한다. - - [ ] 구분자가 숫자가 아님을 검증한다. - - [ ] 커스텀 구분자가 하나임을 검증한다.(여러 번 지정 불가능) + - [x] 구분자의 공백이 아닌 길이가 1인 하나의 문자인지 검증한다. + - [x] 구분자가 숫자가 아님을 검증한다. + - [x] 커스텀 구분자가 하나임을 검증한다.(여러 번 지정 불가능) - [ ] 연산(덧셈) - [ ] 숫자들을 모두 더한다. - [ ] 출력 diff --git a/__tests__/ParserTest.js b/__tests__/ParserTest.js index 7a60d1b7..c23dd688 100644 --- a/__tests__/ParserTest.js +++ b/__tests__/ParserTest.js @@ -1,25 +1,41 @@ import Parser from "../src/Parser" -describe("Parser", () => { +describe("Parser 구분자 추출", () => { test("정상적인 커스텀 구분자 추출 확인", () => { const parser = new Parser() const deli = parser.extractCustomDelimiterStrictly("//&\\n123") expect(deli).toEqual("&"); }); - // test("비정상적인 커스텀 구분자(2자 이상) 에러 발생 확인", () => { - // const parser = new Parser() - // expect(() => - // parser.extractCustomDelimiterStrictly("//&#\\n123") - // ).toThrow("[ERROR]"); - // }); - // test("비정상적인 커스텀 구분자(숫자) 에러 발생 확인", () => { - // const parser = new Parser() - // expect(() => - // parser.extractCustomDelimiterStrictly("//3\\n123") - // ).toThrow("[ERROR]"); - // }); - // test("커스텀 구분자가 존재하지 않는 경우 에러를 발생하지 않아야 한다.", () => { - // const parser = new Parser() - // const deli = parser.extractCustomDelimiterStrictly("1,2,3") - // expect(deli).toEqual(null); - // }); + test("비정상적인 커스텀 구분자(2자 이상) 에러 발생 확인", () => { + const parser = new Parser() + expect(() => + parser.extractCustomDelimiterStrictly("//&#\\n123") + ).toThrow("[ERROR]"); + }); + test("비정상적인 커스텀 구분자(숫자) 에러 발생 확인", () => { + const parser = new Parser() + expect(() => + parser.extractCustomDelimiterStrictly("//3\\n123") + ).toThrow("[ERROR]"); + }); + test("비정상적인 커스텀 구분자(구분자가 여러개) 에러 발생 확인", () => { + const parser = new Parser() + expect(() => + parser.extractCustomDelimiterStrictly("//%\\n1:2//&\\n3") + ).toThrow("[ERROR]"); + }); + test("커스텀 구분자가 존재하지 않는 경우 에러를 발생하지 않아야 한다.", () => { + const parser = new Parser() + const deli = parser.extractCustomDelimiterStrictly("1,2,3") + expect(deli).toEqual(null); + }); + test("구분자 리스트 추가 확인", () => { + const parser = new Parser(); + parser.updateDelimiterSet("&"); + expect(parser.delimiterSet).toStrictEqual(new Set([",", ":", "&"])); + }) + test("커스텀 구분자 추출 후, 구분자 리스트 추가 확인", () => { + const parser = new Parser(); + parser.parseExpressionToNumberList("//&\\n1&2&3"); + expect(parser.delimiterSet).toStrictEqual(new Set([",", ":", "&"])); + }) }) \ No newline at end of file diff --git a/src/Parser.js b/src/Parser.js index 53ae0b80..233d113f 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -4,11 +4,19 @@ class Parser { this.delimiterSet = new Set([",", ":"]); } - extractCustomDelimiterStrictly(expr) { // 커스텀 구분자를 추출한다(1.구분자는 하나의 문자이며, 2.하나의 종류만 존재한다.) - const matchedDelimiterArray = expr.match(/\/\/(.*)\\n/); - return matchedDelimiterArray[1]; - } + extractCustomDelimiterStrictly(expr) { // 커스텀 구분자를 추출(1.구분자는 하나의 문자이며, 2.하나의 종류만 존재한다.) + const matchedDelimiterArray = expr.match(/\/\/(.*)\\n/); // 정규식으로 커스텀 구분자 필터 + if(!matchedDelimiterArray) return null // 1) 사용자가 커스텀 구분자를 지정하지 않은 경우 그대로 return + const requestedDelimiter = matchedDelimiterArray[1]; // 사용자가 요청한 구분자 추출 + // 커스텀 구분자 포맷을 가진 경우만 검증 + this.validateCustomDelimiterExpression(expr, requestedDelimiter); + return requestedDelimiter; // 2) 커스텀 구분자가 있는 경우 추출하여 해당 구분자 반환 + } + validateCustomDelimiterExpression(origin, target) { + if(!origin.startsWith("//") || target.length != 1 || !isNaN(target)) + throw new Error("[ERROR]"); + } updateDelimiterSet(customDeli) { this.delimiterSet.add(customDeli); } From 0dc9f4567f8d4c46094359b3eae781ea56ca2bf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 20 Oct 2025 22:24:46 +0900 Subject: [PATCH 06/12] =?UTF-8?q?feat(parser):=20=EC=88=AB=EC=9E=90=20?= =?UTF-8?q?=EC=B6=94=EC=B6=9C=20=ED=95=A8=EC=88=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- __tests__/ParserTest.js | 31 ++++++++++++++++++++++++++++--- src/Parser.js | 18 ++++++++++++++---- 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 907d9825..cb17dc65 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,11 @@ flowchart LR - [x] 입력 - [x] @woowacourse/mission-utils에서 제공하는 Console API를 사용하여 입력을 받는다. - [x] 문자열의 좌우 공백을 없앤다. -- [ ] 파싱 +- [x] 파싱 - [x] 구분자 식별 - `기본 구분자`(`,`, `:`) - 문자열 앞부분의 "//"와 "\n"가 존재하면 그 사이의 문자를 `커스텀 구분자`로 추가한다. - - [ ] 숫자 추출 + - [x] 숫자 추출 - 구분자를 통해 문자열의 숫자를 추출한다. - [ ] 검증 & 예외처리 - [ ] 숫자들이 대해 0이나 양의 정수인지 검증한다. (다른 문자, 음수, 소수 x) diff --git a/__tests__/ParserTest.js b/__tests__/ParserTest.js index c23dd688..a95f622f 100644 --- a/__tests__/ParserTest.js +++ b/__tests__/ParserTest.js @@ -30,12 +30,37 @@ describe("Parser 구분자 추출", () => { }); test("구분자 리스트 추가 확인", () => { const parser = new Parser(); - parser.updateDelimiterSet("&"); + parser.delimiterSet.add("&"); expect(parser.delimiterSet).toStrictEqual(new Set([",", ":", "&"])); - }) + }); test("커스텀 구분자 추출 후, 구분자 리스트 추가 확인", () => { const parser = new Parser(); parser.parseExpressionToNumberList("//&\\n1&2&3"); expect(parser.delimiterSet).toStrictEqual(new Set([",", ":", "&"])); - }) + }); +}); + +describe("Parser 숫자 분리", () => { + test("기본 구분자 숫자 분리(단일)", () => { + const parser = new Parser(); + const result = parser.splitByDelimitersToNumbers("1,2,3", parser.delimiterSet); + expect(result).toStrictEqual([1, 2, 3]) + }); + test("기본 구분자 숫자 분리(혼합)", () => { + const parser = new Parser(); + const result = parser.splitByDelimitersToNumbers("1,2:3", parser.delimiterSet); + expect(result).toStrictEqual([1, 2, 3]) + }); + test("커스텀구분자 숫자 분리(단일)", () => { + const parser = new Parser(); + parser.delimiterSet.add("("); + const result = parser.splitByDelimitersToNumbers("1(2(3", parser.delimiterSet); + expect(result).toStrictEqual([1, 2, 3]) + }); + test("커스텀구분자 숫자 분리(기본 구분자와 혼합)", () => { + const parser = new Parser(); + parser.delimiterSet.add("("); + const result = parser.splitByDelimitersToNumbers("1:2(3", parser.delimiterSet); + expect(result).toStrictEqual([1, 2, 3]) + }); }) \ No newline at end of file diff --git a/src/Parser.js b/src/Parser.js index 233d113f..c2a4e937 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -13,16 +13,26 @@ class Parser { this.validateCustomDelimiterExpression(expr, requestedDelimiter); return requestedDelimiter; // 2) 커스텀 구분자가 있는 경우 추출하여 해당 구분자 반환 } + validateCustomDelimiterExpression(origin, target) { if(!origin.startsWith("//") || target.length != 1 || !isNaN(target)) throw new Error("[ERROR]"); } - updateDelimiterSet(customDeli) { - this.delimiterSet.add(customDeli); + + splitByDelimitersToNumbers(rawNumbers, delimiterSet) { // 구분자와 섞인 숫자 뭉치를 분리 + let numberArray= [rawNumbers]; + delimiterSet.forEach(deli => { + numberArray = numberArray.flatMap(el => el.split(deli)); + }); + numberArray = numberArray.map(Number); + return numberArray; } + parseExpressionToNumberList(expression) { - const customDelimiter = this.extractCustomDelimiterStrictly(expression); - if(customDelimiter) this.updateDelimiterSet(customDelimiter); + const customDelimiter = this.extractCustomDelimiterStrictly(expression); // 커스텀 구분자 추출 + if(customDelimiter) this.delimiterSet.add(customDelimiter); // 커스텀 구분자 추가 + const exprBody = expression.slice(5); // 커스텀 헤더 제거하여 숫자본체만 분리 + const parsedNumberList = this.splitByDelimitersToNumbers(exprBody, this.delimiterSet); // 구분자로 각 숫자 분리 } } From 1274c883d1b25a81fc87d0fd6f97d2af92422e25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 20 Oct 2025 22:35:40 +0900 Subject: [PATCH 07/12] =?UTF-8?q?feat(parser):=20=EC=88=AB=EC=9E=90=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=ED=95=A8=EC=88=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- __tests__/ParserTest.js | 6 ++++++ src/Parser.js | 14 ++++++++++++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index cb17dc65..80ef4737 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,8 @@ flowchart LR - 문자열 앞부분의 "//"와 "\n"가 존재하면 그 사이의 문자를 `커스텀 구분자`로 추가한다. - [x] 숫자 추출 - 구분자를 통해 문자열의 숫자를 추출한다. -- [ ] 검증 & 예외처리 - - [ ] 숫자들이 대해 0이나 양의 정수인지 검증한다. (다른 문자, 음수, 소수 x) +- [x] 검증 & 예외처리 + - [x] 숫자들이 대해 0이나 양의 정수인지 검증한다. (다른 문자, 음수, 소수 x) - [x] 구분자의 공백이 아닌 길이가 1인 하나의 문자인지 검증한다. - [x] 구분자가 숫자가 아님을 검증한다. - [x] 커스텀 구분자가 하나임을 검증한다.(여러 번 지정 불가능) diff --git a/__tests__/ParserTest.js b/__tests__/ParserTest.js index a95f622f..d25c44ff 100644 --- a/__tests__/ParserTest.js +++ b/__tests__/ParserTest.js @@ -63,4 +63,10 @@ describe("Parser 숫자 분리", () => { const result = parser.splitByDelimitersToNumbers("1:2(3", parser.delimiterSet); expect(result).toStrictEqual([1, 2, 3]) }); + test("잘못된 숫자 검증(소수, 음수)", () => { + const parser = new Parser(); + expect(() => parser.splitByDelimitersToNumbers("1:2.4:3", parser.delimiterSet)).toThrow("[ERROR]") + expect(() => parser.splitByDelimitersToNumbers("1:-3:3", parser.delimiterSet)).toThrow("[ERROR]") + }); + }) \ No newline at end of file diff --git a/src/Parser.js b/src/Parser.js index c2a4e937..67dbcdd2 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -21,18 +21,28 @@ class Parser { splitByDelimitersToNumbers(rawNumbers, delimiterSet) { // 구분자와 섞인 숫자 뭉치를 분리 let numberArray= [rawNumbers]; - delimiterSet.forEach(deli => { + delimiterSet.forEach(deli => { // 각 구분자로 분리 numberArray = numberArray.flatMap(el => el.split(deli)); }); - numberArray = numberArray.map(Number); + numberArray = numberArray.map(num => { // string -> num과 검증 + num = Number(num); + this.validateBusinessRuleNumber(num); + return num; + }); return numberArray; } + validateBusinessRuleNumber(num) { // 0을 포함한 양의 정수 + if(!Number.isInteger(num) || num < 0) + throw new Error("[ERROR]") + } + parseExpressionToNumberList(expression) { const customDelimiter = this.extractCustomDelimiterStrictly(expression); // 커스텀 구분자 추출 if(customDelimiter) this.delimiterSet.add(customDelimiter); // 커스텀 구분자 추가 const exprBody = expression.slice(5); // 커스텀 헤더 제거하여 숫자본체만 분리 const parsedNumberList = this.splitByDelimitersToNumbers(exprBody, this.delimiterSet); // 구분자로 각 숫자 분리 + return parsedNumberList; } } From 9c64cc3951abe5704d8052d5dfbcb152ad432db6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 20 Oct 2025 22:44:05 +0900 Subject: [PATCH 08/12] =?UTF-8?q?feat(app):=20Parser=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit App에서 입력값을 Parser을 활용해 가공하는 연결 코드를 작성했습니다. --- __tests__/ApplicationTest.js | 9 +++++++++ src/App.js | 7 ++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/__tests__/ApplicationTest.js b/__tests__/ApplicationTest.js index 7c6962dd..c427572a 100644 --- a/__tests__/ApplicationTest.js +++ b/__tests__/ApplicationTest.js @@ -41,3 +41,12 @@ describe("문자열 계산기", () => { await expect(app.run()).rejects.toThrow("[ERROR]"); }); }); + +describe("Parser, App 클래스 통합 테스트", () => { + test("입력값 파싱", async () => { + const inputs = "//;\\n1;2;3"; + const app = new App(); + const integrateResult = app.parser.parseExpressionToNumberList(inputs); + expect(integrateResult).toStrictEqual([1,2,3]) + }); +}); diff --git a/src/App.js b/src/App.js index b4a310bc..33e41d99 100644 --- a/src/App.js +++ b/src/App.js @@ -1,9 +1,14 @@ import { Console } from "@woowacourse/mission-utils"; +import Parser from "./Parser"; class App { + constructor() { + this.parser = new Parser(); + } async run() { const rawInput = (await this.getInputUsingWoowaMissionUtils("덧셈할 문자열을 입력해 주세요.")).trim(); // 계산해야할 문자열 - this.checkStringIsEmpty(rawInput, "[ERROR] 아무것도 입력되지 않았습니다."); // 빈 입력값에 대한 사전검증 + this.checkStringIsEmpty(rawInput, "[ERROR] 아무것도 입력되지 않았습니다."); // 빈 입력값에 대한 사전검증 + const parsedNumbers = this.parser.parseExpressionToNumberList(rawInput); } async getInputUsingWoowaMissionUtils(questionString) { // @woowacourse/mission-utils의 Console.readLineAsync 함수로 비동기 입력 받기 From 0afaadd9279d6e31ac6abbb0f2f5cc278b218947 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 20 Oct 2025 22:53:05 +0900 Subject: [PATCH 09/12] =?UTF-8?q?feat(app):=20=EC=88=AB=EC=9E=90=20?= =?UTF-8?q?=EB=88=84=EC=82=B0=20=ED=95=A8=EC=88=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- __tests__/ApplicationTest.js | 7 +++++++ src/App.js | 5 +++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 80ef4737..1c7aa0c2 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,8 @@ flowchart LR - [x] 구분자의 공백이 아닌 길이가 1인 하나의 문자인지 검증한다. - [x] 구분자가 숫자가 아님을 검증한다. - [x] 커스텀 구분자가 하나임을 검증한다.(여러 번 지정 불가능) -- [ ] 연산(덧셈) - - [ ] 숫자들을 모두 더한다. +- [x] 연산(덧셈) + - [x] 숫자들을 모두 더한다. - [ ] 출력 - [ ] @woowacourse/mission-utils에서 제공하는 Console API를 사용하여 출력한다. ``` diff --git a/__tests__/ApplicationTest.js b/__tests__/ApplicationTest.js index c427572a..d0e448f8 100644 --- a/__tests__/ApplicationTest.js +++ b/__tests__/ApplicationTest.js @@ -49,4 +49,11 @@ describe("Parser, App 클래스 통합 테스트", () => { const integrateResult = app.parser.parseExpressionToNumberList(inputs); expect(integrateResult).toStrictEqual([1,2,3]) }); + test("입력값 파싱", async () => { + const inputs = "//;\\n1;2;3"; + const app = new App(); + const integrateResult = app.parser.parseExpressionToNumberList(inputs); + const answer = app.accumulateNumbers(integrateResult); + expect(answer).toStrictEqual(6) + }); }); diff --git a/src/App.js b/src/App.js index 33e41d99..54848fdc 100644 --- a/src/App.js +++ b/src/App.js @@ -9,6 +9,7 @@ class App { const rawInput = (await this.getInputUsingWoowaMissionUtils("덧셈할 문자열을 입력해 주세요.")).trim(); // 계산해야할 문자열 this.checkStringIsEmpty(rawInput, "[ERROR] 아무것도 입력되지 않았습니다."); // 빈 입력값에 대한 사전검증 const parsedNumbers = this.parser.parseExpressionToNumberList(rawInput); + const calculatedValue = this.accumulateNumbers(parsedNumbers); } async getInputUsingWoowaMissionUtils(questionString) { // @woowacourse/mission-utils의 Console.readLineAsync 함수로 비동기 입력 받기 @@ -18,6 +19,10 @@ class App { checkStringIsEmpty(str, errMsg) { // 내용과 상관없는 빈 string 자체에 대한 검증 함수 if(!str || str.trim() === "") throw new Error(errMsg); } + + accumulateNumbers(numberArray) { + return numberArray.reduce((acc, cur) => acc + cur) + } } export default App; From 132a75bb220b774c1d88e30bea56f44a0f1fafb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 20 Oct 2025 23:05:20 +0900 Subject: [PATCH 10/12] =?UTF-8?q?feat(app):=20=EA=B2=B0=EA=B3=BC=20?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5=20=ED=95=A8=EC=88=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit missionutils의 Console.print를 사용해 계산의 결과를 출력합니다. --- README.md | 4 ++-- src/App.js | 5 +++++ src/Parser.js | 11 +++++++---- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1c7aa0c2..f659866b 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,8 @@ flowchart LR - [x] 커스텀 구분자가 하나임을 검증한다.(여러 번 지정 불가능) - [x] 연산(덧셈) - [x] 숫자들을 모두 더한다. -- [ ] 출력 - - [ ] @woowacourse/mission-utils에서 제공하는 Console API를 사용하여 출력한다. +- [x] 출력 + - [x] @woowacourse/mission-utils에서 제공하는 Console API를 사용하여 출력한다. ``` 결과 : 6 ``` diff --git a/src/App.js b/src/App.js index 54848fdc..37b4982b 100644 --- a/src/App.js +++ b/src/App.js @@ -10,6 +10,7 @@ class App { this.checkStringIsEmpty(rawInput, "[ERROR] 아무것도 입력되지 않았습니다."); // 빈 입력값에 대한 사전검증 const parsedNumbers = this.parser.parseExpressionToNumberList(rawInput); const calculatedValue = this.accumulateNumbers(parsedNumbers); + this.printOutputUsingWoowaMissionUtils("결과 : ", calculatedValue) } async getInputUsingWoowaMissionUtils(questionString) { // @woowacourse/mission-utils의 Console.readLineAsync 함수로 비동기 입력 받기 @@ -23,6 +24,10 @@ class App { accumulateNumbers(numberArray) { return numberArray.reduce((acc, cur) => acc + cur) } + + printOutputUsingWoowaMissionUtils(outMsg, output) { + Console.print(outMsg + output); + } } export default App; diff --git a/src/Parser.js b/src/Parser.js index 67dbcdd2..d8e2d5ec 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -38,10 +38,13 @@ class Parser { } parseExpressionToNumberList(expression) { - const customDelimiter = this.extractCustomDelimiterStrictly(expression); // 커스텀 구분자 추출 - if(customDelimiter) this.delimiterSet.add(customDelimiter); // 커스텀 구분자 추가 - const exprBody = expression.slice(5); // 커스텀 헤더 제거하여 숫자본체만 분리 - const parsedNumberList = this.splitByDelimitersToNumbers(exprBody, this.delimiterSet); // 구분자로 각 숫자 분리 + let targetExpr = expression + const customDelimiter = this.extractCustomDelimiterStrictly(targetExpr); // 커스텀 구분자 추출 + if(customDelimiter) { + this.delimiterSet.add(customDelimiter); // 커스텀 구분자 추가 + targetExpr = targetExpr.slice(5); // 커스텀 헤더 제거하여 숫자본체만 분리 + } + const parsedNumberList = this.splitByDelimitersToNumbers(targetExpr, this.delimiterSet); // 구분자로 각 숫자 분리 return parsedNumberList; } } From 296c2a850438b037d8fe32fd5c12de33a5e125c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 20 Oct 2025 23:11:36 +0900 Subject: [PATCH 11/12] =?UTF-8?q?refactor(app,parser):=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EB=A9=94=EC=84=B8=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Parser.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Parser.js b/src/Parser.js index d8e2d5ec..08fb61da 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -16,7 +16,7 @@ class Parser { validateCustomDelimiterExpression(origin, target) { if(!origin.startsWith("//") || target.length != 1 || !isNaN(target)) - throw new Error("[ERROR]"); + throw new Error("[ERROR] 잘못된 구분자입니다."); } splitByDelimitersToNumbers(rawNumbers, delimiterSet) { // 구분자와 섞인 숫자 뭉치를 분리 @@ -34,7 +34,7 @@ class Parser { validateBusinessRuleNumber(num) { // 0을 포함한 양의 정수 if(!Number.isInteger(num) || num < 0) - throw new Error("[ERROR]") + throw new Error("[ERROR] 옳지 않은 숫자입니다.(0과 양의 정수만 가능)") } parseExpressionToNumberList(expression) { From 94c273d7fe1de8459a362deccd2f363401f7883b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=84=B1=EC=A4=80?= Date: Mon, 20 Oct 2025 23:26:16 +0900 Subject: [PATCH 12/12] =?UTF-8?q?docs(readme):=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EB=8B=A8=EA=B3=84=20=EB=AC=B8=EC=A0=9C=EB=93=A4=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index f659866b..bef36c37 100644 --- a/README.md +++ b/README.md @@ -54,3 +54,12 @@ flowchart LR - 해결해야하는 문제 : 커스텀 구분자에 대한 제한에 따라 다양한 예외들이 나오게 됩니다. 구분자의 종류, 개수에 따라 다양한 상황(`.` 허용시 소수문제, `//`허용시 구분자의 모호성)이 발생합니다. - 어떻게 해결했는지: 명확성과 안전을 위해서 엄격한 제한을 두도록 합니다. 소수 입력을 제한하여 공백과 숫자를 제외한 문자를 공백으로 허용합니다. 그리고 문자는 하나만 가능합니다. ### 구현 단계 +> 테스트 용이성 +- 해결해야하는 문제: 미션 전반에 거쳐 테스트를 해가며 안정적인 개발을 해보고 싶었습니다. +- 어떻게 해결했는지: 최근에 함수형 패러다임에 대해서 공부를 하고 있습니다. 그래서 run()과 같은 중심 함수에 사이드 이펙트 몰아두고 다른 함수들은 언제나 같은 입력에 대해서 같은 결과를 보여주는 순수함수를 구현하려고 노력했습니다. 그리고 기능들을 함수로 최대한 쪼개보았습니다. 많이 부족하지만 이러한 노력들이 실제로 테스트를 진행할 때 도움이 되었습니다. +> 구분자로 숫자 분리 +- 해결해야하는 문제: 직관적으로 이해하기 쉬운 forEach를 통해서 각 구분자로 숫자를 분리하려고 하는데, map을 사용하니 다음과 같은 오류가 발생헀습니다. +```jsx +TypeError: el.split is not a function +``` +- 어떻게 해결했는지: 1차원 배열로 만드는 게 핵심이라고 생각해, mdn 탐색을 통해서 `flatMap`을 알게 되었습니다. 이를 통해 평탄화를 할 수 있었습니다. \ No newline at end of file