diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..3c8cb92f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +.idea +.gradle +build \ No newline at end of file diff --git a/README.md b/README.md index e89d6ccc6..4748c93ae 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,93 @@ -# java_calculator -자바 계산기 구현 미션 Repository입니다. +## 📌 계산기 TDD, OOP로 만들기 +### 다양한 표현 수식에 대한 사칙 연산을 수행하는 콘솔 기반의 계산기를 TDD와 OOP를 기반으로 구현해내는 프로젝트 +- 메뉴 입력(0. 종료, 1. 조회, 2. 계산)을 받는 기능 +- 이전 계산 이력에 대한 결과를 순서대로 조회하는 기능 +- 수식(중위 표현식)을 입력 받아 계산 결과 값을 출력하는 기능 +- 다양한 오류 처리 (메뉴 입력 오류, 표현 수식 오류) -## 이곳은 공개 Repo입니다. -1. 여러분의 포트폴리오로 사용하셔도 됩니다. -2. 때문에 이 repo를 fork한 뒤 -3. 여러분의 개인 Repo에 작업하며 -4. 이 Repo에 PR을 보내어 멘토의 코드 리뷰와 피드백을 받으세요. -## Branch 명명 규칙 -1. 여러분 repo는 알아서 해주시고 😀(본인 레포니 main으로 하셔두 되져) -2. prgrms-be-devcourse/spring-board 레포로 PR시 branch는 gituser_id을 적어주세요 :) -- base repo : `여기repo` base : `username` ← head repo : `여러분repo` compare : `main`또는 `github_id` -- 이 규칙은 멘토+팀원들과 정하여 진행해주세요 :) -- 참고 : [Github 위치 및 피드백 기준 가이드](https://www.notion.so/backend-devcourse/Github-e1a0908a6bbf4aeaa5a62981499bb215) -### 과제를 통해 기대하는 역량 +
+ +## 👩‍💻 요구 사항과 구현 내용 +### 전체적인 클래스 관계 구조도 + -- 깃허브를 통한 코드리뷰를 경험해보자 -- 기본적인 테스트 코드 작성 및 활용하는 능력해보자 -- 스스로 OOP를 생각하고 코드로 옮길 수 있는 능력해보자 -### 요구사항 +### 기능 구현 + +- 콘솔로 구현입니다.(스윙으로 구현하시는 분들 계실까봐) - 객체지향적인 코드로 계산기 구현하기 - - [ ] 더하기 - - [ ] 빼기 - - [ ] 곱하기 - - [ ] 나누기 - - [ ] 우선순위(사칙연산) -- [ ] 테스트 코드 구현하기 -- [ ] 계산 이력을 맵으로 데이터 저장기능 만들기(인메모리 DB) -- (선택) 정규식 사용 + - [x] 더하기 + - [x] 빼기 + - [x] 곱하기 + - [x] 나누기 + - [x] 우선순위(사칙연산) +- [x] 테스트 코드 구현하기 +- [x] 계산 이력을 맵으로 데이터 저장기능 만들기 + - 애플리케이션이 동작하는 동안 데이터베이스 외에 데이터를 저장할 수 있는 방법을 고안해보세요. +- [x] 정규식 사용 + + + + +## ✅ 피드백 반영사항 + +
+ 첫번째 PR 반영사항 + +### 1. CalculatorApp +- 전략 패턴을 사용하여 CalculatorApp에서 구체적인 객체를 생성하여 `Calculator`에 주입합니다. +```java + CalculatorConsole console = new CalculatorConsole(); + + new Calculator( + new PostfixCalculator(), + new InfixToPostfixConverter(), + console, + console, + new CalculationRepository() + ).run(); +``` + +### 2. 다양한 형태의 Converter 구현 +- 다양한 수식 간의 변환을 가능하게 하는 `ExpressionConverter` 인터페이스로 추상화를 하고, 현재 계산기 프로그램에서는 중위 표현식을 후위 표현식으로 변환하는 `InfixToPosfixConverter` 클래스로 구현하여 동적으로 적절한 컨버터가 선택되도록 하였습니다. +- `convert()` 메서드에서 String 형태의 expression을 변환하여 피연산자와 연산자의 리스트 형태의 ArrayList 타입으로 수식을 변환합니다. + +### 3. Calculator 추상화 +- 컨버터도 추상화할 수 있으면, 구체적인 표현식을 적절히 계산하여 계산 결과 값을 도출하는 계산기도 추상화할 수 있다고 생각하였습니다. +- 따라서 `calculate()` 추상 메소드를 포함하는 `BasicCalculator` 인터페이스로 추상화하고, 구체적으로 후위 표현 수식을 계산하는 `PostfixCalculator` 클래스로 구체화하였습니다. +- 마찬가지로 runnable한 Application에서 동작할 때, 계산 대상인 표현 수식에 따라 동적으로 적절한 Calculator가 선택됩니다. + +### 4. Validation +- 기존에 Calculator 내에서 메뉴 선택, 표현 수식을 비롯한 입력에 대한 값 검증을 하던 `validateChoiceInput()`, `validateExpression()`의 메서드는 각각 Menu 클래스 내부, `CalculatorValidator` 클래스 내부로 이동하였습니다. +- 메서드 명 또한 valid 여부에 따라 boolean 타입을 반환한다는 점에서, 이를 잘 드러낼 수 있느 `isValidInputMenu()`, `isValidExpression()` 으로 변경하였습니다. + +### 5. CalculationRepository - ConcurrentHashMap과 Atomic Variable을 통한 ID값 관리로 멀티 쓰레드 환경에서의 동시성 문제 고려 +- 계산 이력을 저장하기 위해서 Map을 사용한 이유는, 추후 데이터베이스로의 확장 가능성을 생각했을 때, 각 레코드 별로 고유 PK ID값을 통해 CRUD를 편리하게 하는 것을 고려하여 각 계산 결과 객체 값에 대하여 Key 값을 Integer타입으로, 계산 결과 (CalculationResult)를 Value로 저장하도록 구현하였습니다. +- 기존에는 맵의 Key 값으로, 맵의 크기를 기반으로 id 값을 결정하였고 이는 데이터 삭제 로직으로 확장되는 경우를 고려하지 못했습니다. +- 쓰기 작업(put)에서 Lock을 통해 멀티 쓰레드 환경의 동시성 문제를 해결할 수 있는 ConccurrentHashMap을 이용하였습니다. +- 마찬가지로 유니크 아이디의 경우, 맵의 Key의 타입으로 AtomicInteger을 사용하였습니다. + +### 6. ParameterizedTest 기반 유닛 테스트 +- 단위 테스트 코드들을 작성하였고, ParameterizedTest를 통해 다양한 입력에 대한 테스트를 수행하였습니다. +- [ ] InfixToPostfixConverter 테스트에서 Failed 1개 발생 +- [ ] 계산기 통합 테스트 Failed + +### 7. 상수 관리 +- 여러 클래스에서 사용되는 상수를 따로 하나의 클래스에서 관리하는 것은 객체 지향적이지 않다는 피드백을 바탕으로 각 클래스에서 사용하는 상수들은 클래스 내부로 옮겼습니다. + +### 8. toString의 쓰임 +- 디버그나 로깅 목적으로 toString이 사용된 다는 것을 새로 알게 되었고, +- `CalculationRepository`에 객체 저장 시에 `CalculationResult` 자체를 매개 변수로 넣어주도록 변경하였습니다. + +### 9. Operation +- 사칙 연산을 수행하는 `Operation` 객체를 싱글톤으로 생성하도록 LazyHolder의 방식으로 구현하였습니다. +- [ ] Operation의 Converter, Calculator에서 모두 Operation을 필요로 한다는 점 -> static하게 사용할 수 있는 방법이 없을까? 고민하고 있습니다.. ! +- [ ] 사칙 연산을 수행해내기 위해서는, String(+,-,*,/)과 Operator Enum 객체들을 Map으로 저장하는 `OperatorMap` 을 초기화하는 `Operation` 클래스의 객체가 필수적으로 생성되어야 하는데, Converter과 Calculator 모두 Operation에 의존성을 띄고 있어 설계 리팩토링을 진행해야 할 것 같습니다.. + +
### 실행결과(콘솔) ``` diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..71e44b8b7 --- /dev/null +++ b/build.gradle @@ -0,0 +1,34 @@ +plugins { + id 'java' +} + +group 'org.example' +version '1.0-SNAPSHOT' +sourceCompatibility = '17' + + +repositories { + mavenCentral() +} +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + + +dependencies { + + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.2' + + implementation 'org.assertj:assertj-core:3.24.2' + + implementation group: 'org.projectlombok', name: 'lombok', version: '1.18.28' + +} + +tasks.test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..41dfb8790 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 000000000..1b6c78733 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..107acd32c --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 000000000..948943cf9 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'javacalculator' + diff --git a/src/main/java/CalculatorApp.java b/src/main/java/CalculatorApp.java new file mode 100644 index 000000000..7b20dbe6e --- /dev/null +++ b/src/main/java/CalculatorApp.java @@ -0,0 +1,19 @@ +import calculator.Calculator; +import calculator.CalculatorConsole; +import calculator.model.calculator.PostfixCalculator; +import calculator.model.converter.InfixToPostfixConverter; +import calculator.repository.CalculationRepository; + +public class CalculatorApp { + public static void main(String[] args) { + CalculatorConsole console = new CalculatorConsole(); + + new Calculator( + new PostfixCalculator(), + new InfixToPostfixConverter(), + console, + console, + new CalculationRepository() + ).run(); + } +} diff --git a/src/main/java/calculator/Calculator.java b/src/main/java/calculator/Calculator.java new file mode 100644 index 000000000..b9dd25a42 --- /dev/null +++ b/src/main/java/calculator/Calculator.java @@ -0,0 +1,67 @@ +package calculator; + +import calculator.model.menu.Menu; +import calculator.model.calculator.CalculationResult; +import calculator.model.ExpressionConverter; +import calculator.io.Input; +import calculator.io.Output; +import calculator.model.BasicCalculator; +import calculator.repository.CalculationRepository; +import calculator.model.validator.CalculatorValidator; + +import java.util.List; + + +public class Calculator implements Runnable { + private final BasicCalculator calculator; + private final ExpressionConverter expressionConverter; + private final Input input; + private final Output output; + private final CalculationRepository calculationRepository; + + private static final String MENU_INPUT_ERROR = "메뉴가 올바르게 입력되지 않았습니다."; + private static final String INVALID_INPUT_EXPRESSION = "올바른 수식 표현이 아닙니다."; + private static final char REQUEST_EXIT = '0'; + private static final char REQUEST_VIEW_CALCULATION_RESULT = '1'; + private static final char REQUEST_CALCULATION = '2'; + private static final String CHOICE_PROMPT = "\n선택 : "; + private static final int FIRST_INDEX = 0; + + public Calculator(BasicCalculator calculator, ExpressionConverter expressionConverter, Input input, Output output, CalculationRepository calculationRepository) { + this.calculator = calculator; + this.expressionConverter = expressionConverter; + this.input = input; + this.output = output; + this.calculationRepository = calculationRepository; + } + + + @Override + public void run() { + boolean isProgramRunnable = true; + while (isProgramRunnable) { + output.putMenu(); + String inputMenu = input.getChoice(CHOICE_PROMPT); + if (!Menu.isValidMenuInput(inputMenu)) { + output.inputError(MENU_INPUT_ERROR); + continue; + } + + switch (inputMenu.charAt(FIRST_INDEX)) { + case REQUEST_EXIT -> isProgramRunnable = false; + case REQUEST_VIEW_CALCULATION_RESULT -> output.showCalculationResult(calculationRepository.findAll()); + case REQUEST_CALCULATION -> { + String expression = input.getExpression(); + if (!CalculatorValidator.isValidExpression(expression)) { + output.inputError(INVALID_INPUT_EXPRESSION); + continue; + } + List convertedExpression = expressionConverter.convert(expression); + Integer result = calculator.calculate(convertedExpression); + output.showResult(result.toString()); + calculationRepository.save(new CalculationResult(expression, result)); + } + } + } + } +} diff --git a/src/main/java/calculator/CalculatorConsole.java b/src/main/java/calculator/CalculatorConsole.java new file mode 100644 index 000000000..3e3f28009 --- /dev/null +++ b/src/main/java/calculator/CalculatorConsole.java @@ -0,0 +1,49 @@ +package calculator; + +import calculator.model.menu.Menu; +import calculator.io.Input; +import calculator.io.Output; +import calculator.model.calculator.CalculationResult; + +import java.util.Arrays; +import java.util.Map; +import java.util.Scanner; + +public class CalculatorConsole implements Input, Output { + private final static Scanner scanner = new Scanner(System.in); + + @Override + public void putMenu() { + Arrays.stream(Menu.values()) + .forEach(m -> System.out.println(m.toString())); + } + + @Override + public void showCalculationResult(Map result) { + System.out.println(); + result.forEach((key, value) -> System.out.println(value)); + System.out.println(); + } + + @Override + public void inputError(String errorResponse) { + System.out.println(errorResponse + "\n"); + } + + @Override + public void showResult(String calculationResult) { + System.out.println(calculationResult + "\n"); + } + + @Override + public String getChoice(String prompt) { + System.out.print(prompt); + return scanner.nextLine(); + } + + @Override + public String getExpression() { + System.out.println(); + return scanner.nextLine(); + } +} diff --git a/src/main/java/calculator/io/Input.java b/src/main/java/calculator/io/Input.java new file mode 100644 index 000000000..b5b284604 --- /dev/null +++ b/src/main/java/calculator/io/Input.java @@ -0,0 +1,6 @@ +package calculator.io; + +public interface Input { + String getChoice(String s); + String getExpression(); +} diff --git a/src/main/java/calculator/io/Output.java b/src/main/java/calculator/io/Output.java new file mode 100644 index 000000000..223ac8a70 --- /dev/null +++ b/src/main/java/calculator/io/Output.java @@ -0,0 +1,12 @@ +package calculator.io; + +import calculator.model.calculator.CalculationResult; + +import java.util.Map; + +public interface Output { + void putMenu(); + void showCalculationResult(Map result); + void inputError(String errorResponse); + void showResult(String calculationResult); +} diff --git a/src/main/java/calculator/model/BasicCalculator.java b/src/main/java/calculator/model/BasicCalculator.java new file mode 100644 index 000000000..96d20b3b6 --- /dev/null +++ b/src/main/java/calculator/model/BasicCalculator.java @@ -0,0 +1,7 @@ +package calculator.model; + +import java.util.List; + +public interface BasicCalculator { + Integer calculate(List expression); +} diff --git a/src/main/java/calculator/model/ExpressionConverter.java b/src/main/java/calculator/model/ExpressionConverter.java new file mode 100644 index 000000000..1cf24d96d --- /dev/null +++ b/src/main/java/calculator/model/ExpressionConverter.java @@ -0,0 +1,7 @@ +package calculator.model; + +import java.util.List; + +public interface ExpressionConverter { + List convert(String infixExpression); +} diff --git a/src/main/java/calculator/model/calculator/CalculationResult.java b/src/main/java/calculator/model/calculator/CalculationResult.java new file mode 100644 index 000000000..3611db1a2 --- /dev/null +++ b/src/main/java/calculator/model/calculator/CalculationResult.java @@ -0,0 +1,17 @@ +package calculator.model.calculator; + +public class CalculationResult { + private final String expression; + private final Integer answer; + private static final String CALCULATION_EQUALS_SIGN = " = "; + + public CalculationResult(String expression, Integer answer){ + this.expression = expression; + this.answer = answer; + } + + @Override + public String toString() { + return expression + CALCULATION_EQUALS_SIGN + answer; + } +} diff --git a/src/main/java/calculator/model/calculator/Operation.java b/src/main/java/calculator/model/calculator/Operation.java new file mode 100644 index 000000000..154c54715 --- /dev/null +++ b/src/main/java/calculator/model/calculator/Operation.java @@ -0,0 +1,73 @@ +package calculator.model.calculator; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiFunction; + + +public class Operation { + private static final Map operatorMap = new HashMap<>(); + + private static final Integer LOW = 0; + private static final Integer HIGH = 1; + + private static final String OPERATOR_INPUT_ERROR = "연산자 입력에 오류가 발생했습니다."; + private static final String INVALID_OPERAND = "올바른 피연산자가 아닙니다."; + + private Operation(){ + Arrays.stream(Operator.values()) + .forEach(op -> operatorMap.put(op.operator, op)); + } + + public static Operation getInstance(){ + return LazyHolder.INSTANCE; + } + + private static class LazyHolder{ + private static final Operation INSTANCE = new Operation(); + } + + public Integer calculate(Integer a, String operator, Integer b){ + return Optional.ofNullable(operatorMap.get(operator)) + .orElseThrow(() -> new IllegalArgumentException(OPERATOR_INPUT_ERROR)) + .mapCalculate(operator,a,b); + + } + public static Operator getOperator(String operator){ + return operatorMap.get(operator); + } + + public enum Operator { + + PLUS("+", (num1, num2) -> num1 + num2, LOW), + MINUS("-", (num1, num2) -> num1 - num2, LOW), + MULTIPLY("*", (num1, num2) -> num1 * num2, HIGH), + DIVIDE("/", (num1, num2) -> num1 / num2, HIGH); + + + private final String operator; + private final BiFunction expression; + private final Integer priority; + + Operator(String operator, BiFunction expression, Integer priority) { + this.operator = operator; + this.expression = expression; + this.priority = priority; + } + + + public boolean isPrioritySameOrGreater(Operator operator) { + return priority >= operator.priority; + } + + public Integer mapCalculate(String operator, Integer num1, Integer num2) { + if(Operation.getOperator(operator) == DIVIDE && num2 == 0){ + throw new ArithmeticException(INVALID_OPERAND); + } + return getOperator(operator).expression.apply(num1, num2); + } + } + +} diff --git a/src/main/java/calculator/model/calculator/PostfixCalculator.java b/src/main/java/calculator/model/calculator/PostfixCalculator.java new file mode 100644 index 000000000..50b75b2af --- /dev/null +++ b/src/main/java/calculator/model/calculator/PostfixCalculator.java @@ -0,0 +1,33 @@ +package calculator.model.calculator; + +import calculator.model.BasicCalculator; + +import java.util.List; +import java.util.Stack; + +public class PostfixCalculator implements BasicCalculator { + + private static final String OPERATOR_REGEX = "[-*/+]"; + + @Override + public Integer calculate(List expression) { + Stack calcStack = new Stack<>(); + Operation operation = Operation.getInstance(); + + int op1, op2; + + for(String s : expression){ + if(s.matches(OPERATOR_REGEX)){ + op2 = Integer.parseInt(calcStack.pop()); + op1 = Integer.parseInt(calcStack.pop()); + + Integer result = operation.calculate(op1, s, op2); + calcStack.push(String.valueOf(result)); + } + else{ + calcStack.push(s); + } + } + return Integer.valueOf(calcStack.pop()); + } +} diff --git a/src/main/java/calculator/model/converter/InfixToPostfixConverter.java b/src/main/java/calculator/model/converter/InfixToPostfixConverter.java new file mode 100644 index 000000000..ecbb12062 --- /dev/null +++ b/src/main/java/calculator/model/converter/InfixToPostfixConverter.java @@ -0,0 +1,59 @@ +package calculator.model.converter; + +import calculator.model.ExpressionConverter; +import calculator.model.calculator.Operation; + +import java.util.ArrayList; +import java.util.List; +import java.util.Stack; + +public class InfixToPostfixConverter implements ExpressionConverter { + private final ArrayList postfix; + private final Stack opStack; + private final Operation operation; + + private static final String EXPRESSION_DELIMITER = " "; + private static final String OPERATOR_REGEX = "[-*/+]"; + private static final String OPERAND_REGEX = "^\\d+$"; + + + public InfixToPostfixConverter(){ + postfix = new ArrayList<>(); + opStack = new Stack<>(); + operation = Operation.getInstance(); + } + + @Override + public List convert(String infixExpression) { + StringBuilder num = new StringBuilder(); + + // 중위 표기식을 리스트로 변환 + String[] infix = infixExpression.split(EXPRESSION_DELIMITER); + + // 후위 표기식으로 전환 + for(String s : infix){ + if(s.matches(OPERATOR_REGEX)){ + if(!num.isEmpty()){ + postfix.add(num.toString()); + num = new StringBuilder(); + } + if (opStack.isEmpty()) opStack.push(s); + else { + if (Operation.getOperator(opStack.peek()).isPrioritySameOrGreater(Operation.getOperator(s))) { + postfix.add(opStack.pop()); + } + opStack.push(s); + } + } else if (s.matches(OPERAND_REGEX)) { + num.append(s); + } + } + if(!num.isEmpty()) { + postfix.add(num.toString()); + } + while(!opStack.isEmpty()){ + postfix.add(opStack.pop()); + } + return postfix; + } +} diff --git a/src/main/java/calculator/model/menu/Menu.java b/src/main/java/calculator/model/menu/Menu.java new file mode 100644 index 000000000..8ab2bb05c --- /dev/null +++ b/src/main/java/calculator/model/menu/Menu.java @@ -0,0 +1,36 @@ +package calculator.model.menu; + +import java.util.Arrays; + +public enum Menu { + EXIT('0', "종료"), + CALCULATION_HISTORY('1', "조회"), + CALCULATE('2', "계산"); + + private final Character command; + private final String explanation; + + private static final Integer MENU_INPUT_LENGTH = 1; + private static final Integer FIRST_INDEX = 0; + + Menu(Character command, String explanation) { + this.command = command; + this.explanation = explanation; + } + + public Character getCommand(){ + return command; + } + + public static boolean isValidMenuInput(String input){ + if (input.length() != MENU_INPUT_LENGTH) return false; + Character firstChar = input.charAt(FIRST_INDEX); + return Arrays.stream(Menu.values()).filter(m -> m.getCommand() + .equals(firstChar)).count() == 1; + } + + @Override + public String toString() { + return command + ". " + explanation; + } +} diff --git a/src/main/java/calculator/model/validator/CalculatorValidator.java b/src/main/java/calculator/model/validator/CalculatorValidator.java new file mode 100644 index 000000000..5fbfe3d6f --- /dev/null +++ b/src/main/java/calculator/model/validator/CalculatorValidator.java @@ -0,0 +1,28 @@ +package calculator.model.validator; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; + + +public class CalculatorValidator { + private static final String OPERATOR_REGEX = "[-*/+]"; + private static final String OPERAND_REGEX = "^(0|[-]?[1-9]\\d*)$"; + + private static final int MINIMUM_OPS = 3; + private static final int FIRST_INDEX = 0; + + private static final String EXPRESSION_DELIMITER = " "; + + + public static boolean isValidExpression(String expression){ + AtomicInteger index = new AtomicInteger(FIRST_INDEX); + long countOfValidOps = Arrays.stream(expression.split(EXPRESSION_DELIMITER)) + .filter(e -> isEvenNumber(index) ? e.matches(OPERAND_REGEX) : e.matches(OPERATOR_REGEX)) + .count(); + return countOfValidOps >= MINIMUM_OPS && Arrays.stream(expression.split(" ")).count() == countOfValidOps; + } + + private static boolean isEvenNumber(AtomicInteger index) { + return index.getAndIncrement() % 2 == 0; + } +} diff --git a/src/main/java/calculator/repository/CalculationRepository.java b/src/main/java/calculator/repository/CalculationRepository.java new file mode 100644 index 000000000..8fc77a669 --- /dev/null +++ b/src/main/java/calculator/repository/CalculationRepository.java @@ -0,0 +1,30 @@ +package calculator.repository; + +import calculator.model.calculator.CalculationResult; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +public class CalculationRepository { + private static Map results; + private static AtomicInteger idCounter; + + + public CalculationRepository() { + results = new ConcurrentHashMap<>(); + idCounter = new AtomicInteger(0); + } + + public void save(CalculationResult result) { + results.put(idCounter.getAndIncrement(), result); + } + + public Map findAll() { + return results; + } + + void clearStore(){ + results.clear(); + } +} diff --git a/src/test/java/calculator/CalculatorConsoleTest.java b/src/test/java/calculator/CalculatorConsoleTest.java new file mode 100644 index 000000000..be6c01920 --- /dev/null +++ b/src/test/java/calculator/CalculatorConsoleTest.java @@ -0,0 +1,74 @@ +package calculator; + +import calculator.model.calculator.CalculationResult; +import calculator.repository.CalculationRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import static org.junit.jupiter.api.Assertions.*; + +class CalculatorConsoleTest { + + private static ByteArrayOutputStream outputMessage; + private static final CalculatorConsole console = new CalculatorConsole(); + private static CalculationRepository repository; + + private static final String NEW_LINE = "\r\n"; + + + @BeforeEach + void setUpOutputStreams(){ + repository = new CalculationRepository(); + outputMessage = new ByteArrayOutputStream(); // OutputStream 생성 + System.setOut(new PrintStream(outputMessage)); // 생성한 OutputStream 으로 설정 + } + + @AfterEach + void restoresStreams(){ + System.setOut(System.out); + } + + @Test + @DisplayName("콘솔에 메뉴 출력하기") + void putMenu() { + console.putMenu(); + assertEquals("0. 종료" + NEW_LINE + "1. 조회" + NEW_LINE + "2. 계산", outputMessage.toString().strip()); + + } + + @Test + @DisplayName("계산 이력 콘솔에 출력하기") + void showCalculationResult() { + String expression1 = "3 + 5"; + Integer answer1 = 8; + + String expression2 = "5 * 2"; + Integer answer2 = 10; + + repository.save(new CalculationResult(expression1, answer1)); + repository.save(new CalculationResult(expression2, answer2)); + + + console.showCalculationResult(repository.findAll()); + assertEquals("3 + 5 = 8" + NEW_LINE + "5 * 2 = 10", outputMessage.toString().strip()); + } + + @Test + @DisplayName("계산의 수식과 답 콘솔에 출력하기") + void showResult() { + String expression = "3 + 5"; + Integer answer = 8; + + CalculationResult result = new CalculationResult(expression, answer); + repository.save(result); + + + console.showResult(result.toString()); + assertEquals("3 + 5 = 8", outputMessage.toString().strip()); + } +} \ No newline at end of file diff --git a/src/test/java/calculator/CalculatorTest.java b/src/test/java/calculator/CalculatorTest.java new file mode 100644 index 000000000..9312d78a7 --- /dev/null +++ b/src/test/java/calculator/CalculatorTest.java @@ -0,0 +1,53 @@ +package calculator; + +import calculator.model.calculator.PostfixCalculator; +import calculator.repository.CalculationRepository; +import calculator.model.converter.InfixToPostfixConverter; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.PrintStream; + +class CalculatorTest { + + private final static ByteArrayOutputStream outputMessage = new ByteArrayOutputStream(); + private final PrintStream systemOut = System.out; + private final InputStream systemIn = System.in; + + + @BeforeEach + public void setUpStreams(){ + System.setOut(new PrintStream(outputMessage)); + } + + @AfterEach + void restoresStreams(){ + System.setOut(System.out); + System.setIn(systemIn); + } + + @Test + @DisplayName("계산기 통합 테스트") + void 계산기통합테스트_계산하기() { // fail + String[] inputs = { "2", "3 + 5 * -1", "2", "-1 * 2 + 5 - 1", "1", "0"}; + InputStream in = new ByteArrayInputStream(String.join("\r\n", inputs).getBytes()); + System.setIn(in); + + CalculatorConsole console = new CalculatorConsole(); + new Calculator( + new PostfixCalculator(), + new InfixToPostfixConverter(), + console, + console, + new CalculationRepository() + ).run(); + + String output = outputMessage.toString(); + System.out.println("output = " + output); + } +} \ No newline at end of file diff --git a/src/test/java/calculator/model/calculator/CalculationResultTest.java b/src/test/java/calculator/model/calculator/CalculationResultTest.java new file mode 100644 index 000000000..f3ae5b0f3 --- /dev/null +++ b/src/test/java/calculator/model/calculator/CalculationResultTest.java @@ -0,0 +1,41 @@ +package calculator.model.calculator; + +import calculator.model.calculator.CalculationResult; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +class CalculationResultTest { + @ParameterizedTest + @DisplayName("CalculationResult의 객체 생성 후 expression과 result를 올바르게 출력하는지 검증하는 테스트") + @MethodSource("testData") + void expression_and_answer(String expression, Integer answer, String result){ + CalculationResult calculationResult = new CalculationResult(expression, answer); + Assertions.assertEquals(calculationResult.toString(), result); + } + + private static Stream testData(){ + return Stream.of( + Arguments.of("3 + 5", 8, "3 + 5 = 8"), + Arguments.of("3 + 8 * -1", -5, "3 + 8 * -1 = -5"), + Arguments.of("3 + 1 - 1", 3, "3 + 1 - 1 = 3") + ); + } + + @ParameterizedTest + @DisplayName("오버라이딩한 Calculation Result의 ToString 테스트") + @CsvSource({ + "3 + 5, 8, 3 + 5 = 8", + "-2 * 1, -2, -2 * 1 = -2" + }) + void testToString(String expression, Integer answer, String expectedResult) { + CalculationResult result = new CalculationResult(expression, answer); + + Assertions.assertEquals(expectedResult, result.toString()); + } +} \ No newline at end of file diff --git a/src/test/java/calculator/model/calculator/InfixToPostfixConverterTest.java b/src/test/java/calculator/model/calculator/InfixToPostfixConverterTest.java new file mode 100644 index 000000000..c702a4e46 --- /dev/null +++ b/src/test/java/calculator/model/calculator/InfixToPostfixConverterTest.java @@ -0,0 +1,43 @@ +package calculator.model.calculator; + +import calculator.model.calculator.Operation; +import calculator.model.converter.InfixToPostfixConverter; +import calculator.model.ExpressionConverter; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + + +class InfixToPostfixConverterTest { + + private static ExpressionConverter converter; + private static final Operation operation = Operation.getInstance(); + + @BeforeEach + void beforeEach(){ + converter = new InfixToPostfixConverter(); + } + + @ParameterizedTest + @MethodSource("testExpressionData") + void convert(String infix, List expectedPostfix) { + List convertedToPostfix = converter.convert(infix); + Assertions.assertEquals(expectedPostfix, convertedToPostfix); + } + + private static Stream testExpressionData(){ + return Stream.of( + Arguments.of("3 + 5", Arrays.asList("3", "5", "+")), + Arguments.of("3 - 5 * -2", Arrays.asList("3", "5", "*", "-2", "-")), // Failed + Arguments.of("3 + 5 - 2 * 1", Arrays.asList("3", "5", "+", "2", "1", "*", "-")), + Arguments.of("1 + 5 * 2", Arrays.asList("1", "5", "2", "*", "+")), + Arguments.of("3 + 6 * 2 / 3", Arrays.asList("3", "6", "2", "*", "3", "/", "+")) + ); + } +} \ No newline at end of file diff --git a/src/test/java/calculator/model/calculator/OperationTest.java b/src/test/java/calculator/model/calculator/OperationTest.java new file mode 100644 index 000000000..91bac110c --- /dev/null +++ b/src/test/java/calculator/model/calculator/OperationTest.java @@ -0,0 +1,87 @@ +package calculator.model.calculator; + +import calculator.model.calculator.Operation; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + + +class OperationTest { + + private static final Operation operation = Operation.getInstance(); + + + @ParameterizedTest + @DisplayName("Operator 사칙 연산을 검증 하는 테스트") + @CsvSource({ + "1, +, 5, 6", + "2, -,1, 1", + "3, *, -5, -15", + "12, /, 4, 3" + }) + void calculate(Integer a, String operator, Integer b, Integer result) { + Integer calculated = operation.calculate(a, operator, b); + Assertions.assertEquals(result, calculated); + } + + @ParameterizedTest + @DisplayName("잘못된 표현 수식에 대한 연산을 검증 하는 테스트") + @CsvSource({ + "2, /, 0", + "3, /, 0" + }) + void calculateInvalidExpression(Integer a, String operator, Integer b) { + try { + Integer calculated = operation.calculate(a, operator, b); + } catch (RuntimeException e){ + Assertions.assertEquals("올바른 피연산자가 아닙니다.", e.getMessage()); + } + } + + + @ParameterizedTest + @DisplayName("Operator 사칙 연산을 검증 하는 테스트 - 잘못된 결과 값에 대한 테스트") + @CsvSource({ + "1, +, 5, 5", + "2, -, 1, 4", + "3, *, -5, 15", + "12, /, 4, 1" + }) + void validateWrongCalculation(Integer a, String operator, Integer b, Integer result) { + Integer calculated = operation.calculate(a, operator, b); + Assertions.assertNotEquals(result, calculated); + + } + + @ParameterizedTest + @DisplayName("String 형태의 연산자를 Operator 객체로 가져오는 것을 검증하는 테스트") + @MethodSource("testData") + void getOperator(Operation.Operator operator, String value) { + Assertions.assertEquals(operator, Operation.getOperator(value)); + + } + + private static Stream testData(){ + return Stream.of( + Arguments.of(Operation.Operator.PLUS, "+"), + Arguments.of(Operation.Operator.MINUS, "-"), + Arguments.of(Operation.Operator.DIVIDE, "/"), + Arguments.of(Operation.Operator.MULTIPLY, "*") + ); + } + + @Test + @DisplayName("Operation이 싱글톤으로 객체가 생성되는지 확인하는 테스트") + void getInstance() { + Operation operation = Operation.getInstance(); + Operation operation2 = Operation.getInstance(); + + Assertions.assertEquals(operation, operation2); + } +} \ No newline at end of file diff --git a/src/test/java/calculator/model/calculator/PostfixCalculatorTest.java b/src/test/java/calculator/model/calculator/PostfixCalculatorTest.java new file mode 100644 index 000000000..a609da585 --- /dev/null +++ b/src/test/java/calculator/model/calculator/PostfixCalculatorTest.java @@ -0,0 +1,35 @@ +package calculator.model.calculator; + +import calculator.model.calculator.PostfixCalculator; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +class PostfixCalculatorTest { + + @ParameterizedTest + @MethodSource("testData") + void calculate(List postfix, Integer expectedResult) { + PostfixCalculator postfixCalculator = new PostfixCalculator(); + + Integer result = postfixCalculator.calculate(postfix); + + assertEquals(expectedResult, result); + } + + private static Stream testData() { + return Stream.of( + Arguments.of(Arrays.asList("3", "5", "+"), 8), + Arguments.of(Arrays.asList("3", "5", "*", "-2", "-"), 13), // failed + Arguments.of(Arrays.asList("3", "5", "+", "2", "1", "*", "-"), 6), + Arguments.of(Arrays.asList("1", "5", "2", "*", "+"), 11), + Arguments.of(Arrays.asList("3", "6", "2", "*", "3", "/", "+"), 7) + ); + } +} \ No newline at end of file diff --git a/src/test/java/calculator/model/menu/MenuTest.java b/src/test/java/calculator/model/menu/MenuTest.java new file mode 100644 index 000000000..8bbb8d9d7 --- /dev/null +++ b/src/test/java/calculator/model/menu/MenuTest.java @@ -0,0 +1,50 @@ +package calculator.model.menu; + +import calculator.model.menu.Menu; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class MenuTest { + + @ParameterizedTest + @ValueSource(strings = {"0", "1", "2"}) + @DisplayName("올바른 메뉴 선택을 하였을 때 성공하는 테스트") + void isValidMenu(String menuInput) { + assertTrue(Menu.isValidMenuInput(menuInput)); + } + + @ParameterizedTest + @ValueSource(strings = {"3", "calculate", "no", " ", "\n"}) + @DisplayName("올바르지 메뉴 선택을 하였을 때 ") + void isNotValidMenuInput(String wrongMenuInput) { + assertFalse(Menu.isValidMenuInput(wrongMenuInput)); + } + + @Test + @DisplayName("Menu의 오버라이딩한 toString이 잘 작동하는지 테스트") + void testToString() { + List menuStringExpected = List.of( + "0. 종료", + "1. 조회", + "2. 계산" + ); + + + List menuList = Arrays.stream(Menu.values()) + .map(Menu::toString).toList(); + Assertions.assertThat(menuList) + .hasSize(3) + .contains("0. 종료") + .contains("1. 조회") + .contains("2. 계산") + .isEqualTo(menuStringExpected); + } +} \ No newline at end of file diff --git a/src/test/java/calculator/model/validator/CalculatorValidatorTest.java b/src/test/java/calculator/model/validator/CalculatorValidatorTest.java new file mode 100644 index 000000000..39e9154fa --- /dev/null +++ b/src/test/java/calculator/model/validator/CalculatorValidatorTest.java @@ -0,0 +1,50 @@ +package calculator.model.validator; + +import calculator.model.validator.CalculatorValidator; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +class CalculatorValidatorTest { + + @ParameterizedTest + @MethodSource("testValidExpression") + @DisplayName("올바른 수식 표현을 검증하는 테스트") + void validateExpression(String expression) { + boolean isValidResult = CalculatorValidator.isValidExpression(expression); + assertTrue(isValidResult); + } + + private static Stream testValidExpression(){ + return Stream.of( + Arguments.of("3 + 6 / -2"), + Arguments.of("3 * 1 - 2"), + Arguments.of("-2 * -1 + 3") + ); + } + + @ParameterizedTest + @MethodSource("testInvalidExpression") + @DisplayName("올바르지 않은 수식 표현을 검증하는 테스트") + void validateInvalidExpression(String expression) { + boolean isValidResult = CalculatorValidator.isValidExpression(expression); + assertFalse(isValidResult); + } + + private static Stream testInvalidExpression() { + return Stream.of( + Arguments.of("3 + "), + Arguments.of("3"), + Arguments.of("+"), + Arguments.of("-3"), + Arguments.of("-3 + + "), + Arguments.of("-3 + 5 5 6"), + Arguments.of("+ 1 - 5") + ); + } +} \ No newline at end of file diff --git a/src/test/java/calculator/model/validator/ExpressionValidationTest.java b/src/test/java/calculator/model/validator/ExpressionValidationTest.java new file mode 100644 index 000000000..139c7abe9 --- /dev/null +++ b/src/test/java/calculator/model/validator/ExpressionValidationTest.java @@ -0,0 +1,35 @@ +package calculator.model.validator; + +import calculator.model.validator.CalculatorValidator; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class ExpressionValidationTest { + @Test + @DisplayName("올바른 연산 식을 입력한 경우") + void validExpression(){ + Assertions.assertThat(CalculatorValidator.isValidExpression("3 + 5 * 2 - 1")) + .isEqualTo(true); + } + @Test + @DisplayName("올바른 연산자를 입력하지 않은 경우") + void invalidOperator(){ + Assertions.assertThat(CalculatorValidator.isValidExpression("3 _ 5 * 2")) + .isEqualTo(false); + } + + @Test + @DisplayName("올바른 연산 식을 입력하지 않은 경우") + void invalidExpression(){ + Assertions.assertThat(CalculatorValidator.isValidExpression("- 5 / 5 + 1")) + .isEqualTo(false); + } + + @Test + @DisplayName("분모가 0 또는 음수인 경우 에러 처리") + void invalidArithmeticOperation(){ + Assertions.assertThat(CalculatorValidator.isValidExpression("3 + 5 / -1")) + .isEqualTo(false); + } +} diff --git a/src/test/java/calculator/repository/CalculationRepositoryTest.java b/src/test/java/calculator/repository/CalculationRepositoryTest.java new file mode 100644 index 000000000..92dbbc47b --- /dev/null +++ b/src/test/java/calculator/repository/CalculationRepositoryTest.java @@ -0,0 +1,63 @@ +package calculator.repository; + +import calculator.model.calculator.CalculationResult; +import org.assertj.core.api.Assertions; +import org.assertj.core.data.Index; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + + +import java.util.List; +import static org.junit.jupiter.api.Assertions.*; + +class CalculationRepositoryTest { + + private static CalculationRepository repository; + + @BeforeEach + void beforeEach() { + repository = new CalculationRepository(); + } + + @AfterEach + void afterEach(){ + repository.clearStore(); + } + + @Test + @DisplayName("CalculationRepository에 CaclulationResult가 잘 저장되었는지 확인하는 테스트") + void save() { + String expression = "3 + 5 * 2"; + Integer answer = 13; + + repository.save(new CalculationResult(expression, answer)); + + List results = repository.findAll().values().stream().map(CalculationResult::toString).toList(); + Assertions.assertThat(results) + .hasSize(1) + .contains("3 + 5 * 2 = 13", Index.atIndex(0)); + } + + @Test + @DisplayName("Calculation Repository에서 CalculationResult 값들을 잘 가져오는지 확인하는 테스트") + void findAll() { + String expression1 = "3 + 5 * 2"; + Integer answer1 = 13; + + String expression2 = "5 - 1 * 2"; + Integer answer2 = 3; + + repository.save(new CalculationResult(expression1, answer1)); + repository.save(new CalculationResult(expression2, answer2)); + + List results = repository.findAll().values().stream().map(CalculationResult::toString).toList(); + assertEquals(2, results.size()); + Assertions.assertThat(results) + .hasSize(2) + .contains("3 + 5 * 2 = 13") + .contains("5 - 1 * 2 = 3"); + + } +} \ No newline at end of file