diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..8377beb81 --- /dev/null +++ b/.gitignore @@ -0,0 +1,175 @@ +# Created by https://www.toptal.com/developers/gitignore/api/java,intellij,gradle +# Edit at https://www.toptal.com/developers/gitignore?templates=java,intellij,gradle + +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +.idea +gradle +gradlew +gradlew.bat + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +### Java ### +# 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* + +### Gradle ### +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +### Gradle Patch ### +# Java heap dump +*.hprof + +# End of https://www.toptal.com/developers/gitignore/api/java,intellij,gradle \ No newline at end of file diff --git a/calculator/build.gradle b/calculator/build.gradle new file mode 100644 index 000000000..ca90a6c50 --- /dev/null +++ b/calculator/build.gradle @@ -0,0 +1,27 @@ +plugins { + id 'java' +} + +group 'com.programmers' +version '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.junit.jupiter:junit-jupiter:5.8.1' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' + + compileOnly 'org.projectlombok:lombok:1.18.28' + annotationProcessor 'org.projectlombok:lombok:1.18.28' + testCompileOnly 'org.projectlombok:lombok:1.18.28' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.28' + + implementation "org.assertj:assertj-core:3.11.1" +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/calculator/settings.gradle b/calculator/settings.gradle new file mode 100644 index 000000000..4b53eb355 --- /dev/null +++ b/calculator/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'calculator' + diff --git a/calculator/src/main/java/com/programmers/App.java b/calculator/src/main/java/com/programmers/App.java new file mode 100644 index 000000000..5ab8b4efe --- /dev/null +++ b/calculator/src/main/java/com/programmers/App.java @@ -0,0 +1,15 @@ +package com.programmers; + +import com.programmers.engine.model.CalculationFormula; +import com.programmers.engine.model.Menu; + +public class App { + + public static void main(String[] args) { + Menu menu = new Menu(); + CalculationFormula calculationFormula = new CalculationFormula(); + Calculator calculator = new Calculator(menu, menu, calculationFormula, menu); + + calculator.run(); + } +} \ No newline at end of file diff --git a/calculator/src/main/java/com/programmers/Calculator.java b/calculator/src/main/java/com/programmers/Calculator.java new file mode 100644 index 000000000..868dd66ff --- /dev/null +++ b/calculator/src/main/java/com/programmers/Calculator.java @@ -0,0 +1,41 @@ +package com.programmers; + +import com.programmers.engine.io.Input; +import com.programmers.engine.io.Output; +import com.programmers.engine.model.CalculationFormula; +import com.programmers.engine.model.Menu; +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class Calculator implements Runnable { + + public static final int HISTORY = 1; + public static final int CALCULATE = 2; + public static final int EXIT = 3; + private final Input input; + private final Output output; + private final CalculationFormula calculationFormula; + private final Menu menu; + + @Override + public void run() { + while (true) { + output.showMenu(); + + switch (input.selectOption()) { + case HISTORY: + calculationFormula.showResult(); + continue; + case CALCULATE: + String formula = menu.getInfix(); + calculationFormula.calculate(formula); + continue; + case EXIT: + output.exit(); + return; + default: + output.incorrectOption(); + } + } + } +} diff --git a/calculator/src/main/java/com/programmers/engine/converter/InfixToPostfix.java b/calculator/src/main/java/com/programmers/engine/converter/InfixToPostfix.java new file mode 100644 index 000000000..f94568669 --- /dev/null +++ b/calculator/src/main/java/com/programmers/engine/converter/InfixToPostfix.java @@ -0,0 +1,54 @@ +package com.programmers.engine.converter; + +import java.util.Stack; + +public class InfixToPostfix { + + private static int getPriority(char operator) { + switch (operator) { + case '+': + case '-': + return 1; + case '*': + case '/': + return 2; + } + return -1; + } + + public static String InfixToPostfix(String infix) { + StringBuilder postfixStringBuilder = new StringBuilder(); + Stack operatorStack = new Stack<>(); + + for (int i = 0; i < infix.length(); i++) { + char ch = infix.charAt(i); + + if (ch == ' ') { + continue; + } + + if (isDigit(ch)) { + postfixStringBuilder.append(ch); + } else { + while (isPriority(operatorStack, ch)) { + postfixStringBuilder.append(operatorStack.pop()); + } + operatorStack.push(ch); + } + } + + while (!operatorStack.isEmpty()) { + postfixStringBuilder.append(operatorStack.pop()); + } + return postfixStringBuilder.toString(); + } + + private static boolean isDigit(char ch) { + return Character.isDigit(ch); + } + + private static boolean isPriority(Stack operatorStack, char ch) { + return !operatorStack.isEmpty() && getPriority(ch) <= getPriority( + operatorStack.peek()); + } +} diff --git a/calculator/src/main/java/com/programmers/engine/converter/PostfixToAnswer.java b/calculator/src/main/java/com/programmers/engine/converter/PostfixToAnswer.java new file mode 100644 index 000000000..a2cecd6d4 --- /dev/null +++ b/calculator/src/main/java/com/programmers/engine/converter/PostfixToAnswer.java @@ -0,0 +1,36 @@ +package com.programmers.engine.converter; + +import com.programmers.engine.model.Operator; +import java.util.Stack; + +public class PostfixToAnswer { + + public static double PostfixToAnswer(String postfix) { + Stack operandStack = new Stack<>(); + + for (int i = 0; i < postfix.length(); i++) { + char ch = postfix.charAt(i); + + if (ch == ' ') { + continue; + } + + if (isDigit(ch)) { + operandStack.push((double) (ch - '0')); + } else { + Operator operator = Operator.fromSymbol(ch); + double rightOperand = operandStack.pop(); + double leftOperand = operandStack.pop(); + double result = operator.calculate(leftOperand, rightOperand); + + operandStack.push(result); + } + } + return operandStack.pop(); + } + + private static boolean isDigit(char ch) { + return Character.isDigit(ch); + } +} + diff --git a/calculator/src/main/java/com/programmers/engine/io/Input.java b/calculator/src/main/java/com/programmers/engine/io/Input.java new file mode 100644 index 000000000..37a8f17e2 --- /dev/null +++ b/calculator/src/main/java/com/programmers/engine/io/Input.java @@ -0,0 +1,10 @@ +package com.programmers.engine.io; + +public interface Input { + + int selectOption(); + + String getInfix(); + + String getReplacedInfix(String infix); +} diff --git a/calculator/src/main/java/com/programmers/engine/io/Output.java b/calculator/src/main/java/com/programmers/engine/io/Output.java new file mode 100644 index 000000000..353377b8a --- /dev/null +++ b/calculator/src/main/java/com/programmers/engine/io/Output.java @@ -0,0 +1,10 @@ +package com.programmers.engine.io; + +public interface Output { + + void showMenu(); + + void exit(); + + void incorrectOption(); +} diff --git a/calculator/src/main/java/com/programmers/engine/model/CalculationFormula.java b/calculator/src/main/java/com/programmers/engine/model/CalculationFormula.java new file mode 100644 index 000000000..35835bd72 --- /dev/null +++ b/calculator/src/main/java/com/programmers/engine/model/CalculationFormula.java @@ -0,0 +1,23 @@ +package com.programmers.engine.model; + +import static com.programmers.engine.converter.PostfixToAnswer.PostfixToAnswer; + +import com.programmers.engine.converter.InfixToPostfix; + +public class CalculationFormula { + + private static final History history = new History(); + private static final Menu menu = new Menu(); + + public void showResult() { + history.showHistory(); + } + + public void calculate(String infix) { + String replacedInfix = menu.getReplacedInfix(infix); + String postfix = InfixToPostfix.InfixToPostfix(replacedInfix); + double answer = PostfixToAnswer(postfix); + System.out.println(answer + "\n"); + history.save(infix, answer); + } +} diff --git a/calculator/src/main/java/com/programmers/engine/model/History.java b/calculator/src/main/java/com/programmers/engine/model/History.java new file mode 100644 index 000000000..e709ddf2e --- /dev/null +++ b/calculator/src/main/java/com/programmers/engine/model/History.java @@ -0,0 +1,22 @@ +package com.programmers.engine.model; + +import java.util.HashMap; +import java.util.Map; + +public class History { + + private final Map history = new HashMap<>(); + private int index = 0; + + public void save(String formula, double answer) { + history.put(++index, formula + " = " + answer); + } + + public void showHistory() { + for (int key : history.keySet()) { + String formula = history.get(key); + System.out.println(formula); + } + System.out.println(); + } +} diff --git a/calculator/src/main/java/com/programmers/engine/model/Menu.java b/calculator/src/main/java/com/programmers/engine/model/Menu.java new file mode 100644 index 000000000..9742b348c --- /dev/null +++ b/calculator/src/main/java/com/programmers/engine/model/Menu.java @@ -0,0 +1,43 @@ +package com.programmers.engine.model; + +import com.programmers.engine.io.Input; +import com.programmers.engine.io.Output; +import java.util.Scanner; + +public class Menu implements Input, Output { + + private static final Scanner scanner = new Scanner(System.in); + + @Override + public int selectOption() { + System.out.print("선택 : "); + int select = Integer.parseInt(scanner.nextLine()); + System.out.println(); + return select; + } + + @Override + public String getInfix() { + return scanner.nextLine(); + } + + @Override + public String getReplacedInfix(String infix) { + return infix.replaceAll("\\s+", ""); + } + + @Override + public void showMenu() { + System.out.println("1. 조회\n2. 계산 \n3. 종료\n"); + } + + @Override + public void exit() { + System.out.println("계산기를 종료합니다."); + } + + @Override + public void incorrectOption() { + System.out.println("올바른 메뉴 옵션을 선택해주세요."); + } +} diff --git a/calculator/src/main/java/com/programmers/engine/model/Operator.java b/calculator/src/main/java/com/programmers/engine/model/Operator.java new file mode 100644 index 000000000..831f46c46 --- /dev/null +++ b/calculator/src/main/java/com/programmers/engine/model/Operator.java @@ -0,0 +1,48 @@ +package com.programmers.engine.model; + +public enum Operator { + ADD('+') { + @Override + public double calculate(double leftOperand, double rightOperand) { + return leftOperand + rightOperand; + } + }, + SUBTRACT('-') { + @Override + public double calculate(double leftOperand, double rightOperand) { + return leftOperand - rightOperand; + } + }, + MULTIPLY('*') { + @Override + public double calculate(double leftOperand, double rightOperand) { + return leftOperand * rightOperand; + } + }, + DIVIDE('/') { + @Override + public double calculate(double leftOperand, double rightOperand) { + if (rightOperand == 0) { + throw new ArithmeticException("0으로 나눌 수 없습니다."); + } + return Math.round(leftOperand / rightOperand * 100.0) / 100.0; + } + }; + + private final char symbol; + + Operator(char symbol) { + this.symbol = symbol; + } + + public static Operator fromSymbol(char symbol) { + for (Operator operator : values()) { + if (operator.symbol == symbol) { + return operator; + } + } + throw new IllegalArgumentException("잘못된 연산자입니다." + symbol); + } + + public abstract double calculate(double leftOperand, double rightOperand); +} diff --git a/calculator/src/test/java/com/programmers/engine/model/CalculatorTest.java b/calculator/src/test/java/com/programmers/engine/model/CalculatorTest.java new file mode 100644 index 000000000..e7ee64641 --- /dev/null +++ b/calculator/src/test/java/com/programmers/engine/model/CalculatorTest.java @@ -0,0 +1,33 @@ +package com.programmers.engine.model; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class CalculatorTest { + + @Test + @DisplayName("더하기 성공") + void 덧셈성공() { + assertThat(Operator.ADD.calculate(1, 2)).isEqualTo(3); + } + + @Test + @DisplayName("빼기 성공") + void 뺄셈성공() { + assertThat(Operator.SUBTRACT.calculate(2, 1)).isEqualTo(1); + } + + @Test + @DisplayName("곱하기 성공") + void 곱셈성공() { + assertThat(Operator.MULTIPLY.calculate(3, 4)).isEqualTo(12); + } + + @Test + @DisplayName("나누기 성공") + void 나눗셈성공() { + assertThat(Operator.DIVIDE.calculate(1, 2)).isEqualTo(0.5); + } +}