diff --git a/build.gradle b/build.gradle index 67e032179..f89cef282 100644 --- a/build.gradle +++ b/build.gradle @@ -16,10 +16,11 @@ dependencies { java { toolchain { - languageVersion = JavaLanguageVersion.of(8) + languageVersion = JavaLanguageVersion.of(17) } } + test { useJUnitPlatform() } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c02..7454180f2 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 28ff446a2..ffed3a254 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 4f906e0c8..1b6c78733 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# 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. @@ -17,67 +17,101 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# 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 -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +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 -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +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" +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 - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +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 @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 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" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + 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 @@ -106,80 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +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 -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -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" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +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 - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + 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 - i=`expr $i + 1` + # 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 - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# 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. +# -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +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/src/main/docs/README.md b/src/main/docs/README.md new file mode 100644 index 000000000..68086b718 --- /dev/null +++ b/src/main/docs/README.md @@ -0,0 +1,40 @@ +# 자판기 +### 기능 : 상품을 구매하면 자판기가 가진 동전만으로 잔돈을 거슬러준다. +1. 자판기의 보유 금액 입력받기 + - Coin으로 나누어 떨어져야 한다. 즉, 10원 단위로 나누어 떨어진다. + - 0원도 가능하다 (잔돈 반환 불가능 시, 자판기에 남는다.) + - 0원 미만은 불가능 하다. +2. 투입 금액 받기 + ```[콜라,1500,20];[사이다,1000,10]``` + - 상품명 입력받기 + - 가격 입력받기 + - [x] 최소 상품 판매 금액은 100원 이상이다. + - [x] 10원으로 나누어떨어져야 한다. + - 수량 입력받기 + - [x] 최소 1개 이상이다. +3. 자판이가 보유하고 있는 금액에서 동전을 무작위로 생성하기 + - 자판기가 보유한 동전을 출력한다. +4. 잔돈 돌려주기 + - [ ] 동전의 개수를 최소한으로 잔돈 돌려주기 + - 지폐 단위는 사용 불가하다. + - [ ] 잔액 중 동전만 사용해 반환 불가능 시에 남은 금액은 자판기에 남는다. +5. 상품을 구매하기 + - [ ] ```구매할 상품명```이 자판기에 존재하는지 검사한다. + - [ ] 현재 금액으로 구매가 가능한지 검사한다. + - [ ] 상품을 구매하여 최초 투입 금액에서 차감한다. + - [ ] 더이상 구매가 불가능한 경우(남은 금액이 상품의 최저 가격보다 적음, 모든 상품이 소진됨.) 잔돈을 반환한다 + + +# 프로그래밍 요구사항 - Coin +Coin 클래스를 활용해 구현해야 한다. +필드(인스턴스 변수)인 amount의 접근 제어자 private을 변경할 수 없다. + +IllegalArgumentException를 발생시키고, "[ERROR]"로 시작하는 에러 메시지를 출력 후 해당 부분부터 다시 입력을 받는다 + +Randoms, Console API를 사용한다. + +✅ indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다. +✅ 3항 연산자를 쓰지 않는다. +✅ 함수(또는 메소드)의 길이가 15라인을 넘어가지 않도록 구현한다. +✅ 함수(또는 메소드)가 한 가지 일만 잘 하도록 구현한다. +✅ else 예약어를 쓰지 않는다. \ No newline at end of file diff --git a/src/main/java/vendingmachine/Application.java b/src/main/java/vendingmachine/Application.java index 9d3be447b..aad3ee57d 100644 --- a/src/main/java/vendingmachine/Application.java +++ b/src/main/java/vendingmachine/Application.java @@ -1,7 +1,13 @@ package vendingmachine; +import vendingmachine.controller.VendingMachineController; +import vendingmachine.view.Input; +import vendingmachine.view.InputView; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + Input input = InputView.getInstance(); + VendingMachineController controller = VendingMachineController.from(input); + controller.run(); } } diff --git a/src/main/java/vendingmachine/controller/CoinService.java b/src/main/java/vendingmachine/controller/CoinService.java new file mode 100644 index 000000000..a14a28847 --- /dev/null +++ b/src/main/java/vendingmachine/controller/CoinService.java @@ -0,0 +1,19 @@ +package vendingmachine.controller; + +import java.util.HashMap; +import vendingmachine.domain.Coin; +import vendingmachine.utils.CoinGenerator; + +public class CoinService { + + private HashMap coins; + private int remainMoney; + + public void setCoinsByMoney(final Integer holdMoney, CoinGenerator generator) { + coins = generator.countCoin(holdMoney); + } + + public void change(){ + //TODO : 잔돈을 가능한 적은 코인으로 교환한다 + } +} diff --git a/src/main/java/vendingmachine/controller/VendingMachineController.java b/src/main/java/vendingmachine/controller/VendingMachineController.java new file mode 100644 index 000000000..d8aeb82c7 --- /dev/null +++ b/src/main/java/vendingmachine/controller/VendingMachineController.java @@ -0,0 +1,76 @@ +package vendingmachine.controller; + +import vendingmachine.domain.Products; +import vendingmachine.service.PurchaseService; +import vendingmachine.utils.CoinGenerator; +import vendingmachine.utils.Convertor; +import vendingmachine.utils.ExceptionHandler; +import vendingmachine.view.Input; +import vendingmachine.view.OutputView; + +public class VendingMachineController { + private final Input input; + private final CoinGenerator coinGenerator = new CoinGenerator(); + private final CoinService coinService = new CoinService(); + private Products products; + private PurchaseService purchaseService; + + private VendingMachineController(final Input input) { + this.input = input; + } + + public static VendingMachineController from(final Input input) { + return new VendingMachineController(input); + } + + public void run() { + setHoldCoin(); + setProducts(); + setInputAmount(); + //TODO : while(isStilRemainMoney) + requestWanted(); + spendAll(); + } + + private void setHoldCoin() { + OutputView.printRequestMachinHoldMoney(); + String inputString = input.readHoldMoney(); + Integer holdMoney = ExceptionHandler.convert(Convertor::convertToMoney, inputString); + if (holdMoney == null) { + setHoldCoin(); + } + coinService.setCoinsByMoney(holdMoney, coinGenerator); + //TODO : printHoldCoin + } + + private void setProducts() { + OutputView.printRequestProducts(); + String inputString = input.readProducts(); + Products products = ExceptionHandler.convert(Convertor::convertToProducts, inputString); + if (products == null) { + setProducts(); + } + this.products = products; + } + + private void setInputAmount() { + OutputView.printRequestInputAmount(); + String inputString = input.readInputAmount(); + Integer inputAmount = ExceptionHandler.convert(Convertor::convertToMoney, inputString); + if (inputAmount == null) { + setInputAmount(); + } + purchaseService = PurchaseService.of(products, inputAmount); + + } + + private void requestWanted() { + OutputView.printRequestWanted(); + String inputString = input.readWanted(); + purchaseService.purchase(inputString); + } + + private void spendAll() { + //TODO : coinService getRemainCoins() + } +} diff --git a/src/main/java/vendingmachine/Coin.java b/src/main/java/vendingmachine/domain/Coin.java similarity index 51% rename from src/main/java/vendingmachine/Coin.java rename to src/main/java/vendingmachine/domain/Coin.java index c76293fbc..17c1b30a5 100644 --- a/src/main/java/vendingmachine/Coin.java +++ b/src/main/java/vendingmachine/domain/Coin.java @@ -1,4 +1,4 @@ -package vendingmachine; +package vendingmachine.domain; public enum Coin { COIN_500(500), @@ -12,5 +12,11 @@ public enum Coin { this.amount = amount; } - // 추가 기능 구현 + public boolean isDivided(int price){ + return (price % this.amount) == 0; + } + + public int getAmount() { + return amount; + } } diff --git a/src/main/java/vendingmachine/domain/Product.java b/src/main/java/vendingmachine/domain/Product.java new file mode 100644 index 000000000..62e58e416 --- /dev/null +++ b/src/main/java/vendingmachine/domain/Product.java @@ -0,0 +1,16 @@ +package vendingmachine.domain; + +public class Product { + private final String name; + private final ProductPrice price; + + private Product(final String name, final int price) { + this.name = name; + this.price = ProductPrice.from(price); + } + + public static Product of(String name, int price) { + return new Product(name, price); + } + +} diff --git a/src/main/java/vendingmachine/domain/ProductPrice.java b/src/main/java/vendingmachine/domain/ProductPrice.java new file mode 100644 index 000000000..ebf1a24f0 --- /dev/null +++ b/src/main/java/vendingmachine/domain/ProductPrice.java @@ -0,0 +1,17 @@ +package vendingmachine.domain; + +import vendingmachine.validators.ProductPriceValidator; + +public class ProductPrice { + + private final int price; + + public ProductPrice(final int price) { + this.price = price; + } + + public static ProductPrice from(int price){ + ProductPriceValidator.validate(price); + return new ProductPrice(price); + } +} diff --git a/src/main/java/vendingmachine/domain/Products.java b/src/main/java/vendingmachine/domain/Products.java new file mode 100644 index 000000000..e29e38134 --- /dev/null +++ b/src/main/java/vendingmachine/domain/Products.java @@ -0,0 +1,21 @@ +package vendingmachine.domain; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import vendingmachine.validators.ProductsValidator; + +public class Products { + private final Map products; + + private Products(final Map products) { + this.products = products; + } + + public static Products from(final Map input) { + List counts = input.values().stream().collect(Collectors.toList()); + ProductsValidator.validate(counts); + return new Products(input); + } + +} diff --git a/src/main/java/vendingmachine/service/PurchaseService.java b/src/main/java/vendingmachine/service/PurchaseService.java new file mode 100644 index 000000000..816d13283 --- /dev/null +++ b/src/main/java/vendingmachine/service/PurchaseService.java @@ -0,0 +1,21 @@ +package vendingmachine.service; + +import vendingmachine.domain.Products; + +public class PurchaseService { + private final Products products; + private final int inputAmount; + + public PurchaseService(final Products products, final int inputAmount) { + this.products = products; + this.inputAmount = inputAmount; + } + + public static PurchaseService of(final Products products, final Integer inputAmount) { + return new PurchaseService(products, inputAmount); + } + + public void purchase(final String inputString) { + + } +} diff --git a/src/main/java/vendingmachine/utils/CoinGenerator.java b/src/main/java/vendingmachine/utils/CoinGenerator.java new file mode 100644 index 000000000..4d3a49226 --- /dev/null +++ b/src/main/java/vendingmachine/utils/CoinGenerator.java @@ -0,0 +1,41 @@ +package vendingmachine.utils; + +import static camp.nextstep.edu.missionutils.Randoms.pickNumberInList; + +import java.util.HashMap; +import java.util.List; +import vendingmachine.domain.Coin; + +public class CoinGenerator { + private static final int COIN10 = 0; + private static final int COIN50 = 1; + private static final int COIN100 = 2; + private static final int COIN500 = 3; + + public HashMap countCoin(int holdMoney) { + HashMap map = new HashMap<>(); + int coins[][] = generateByHoldMoney(holdMoney); + map.put(Coin.COIN_10, coins[COIN10][0]); + map.put(Coin.COIN_50, coins[COIN50][0]); + map.put(Coin.COIN_100, coins[COIN100][0]); + map.put(Coin.COIN_500, coins[COIN500][0]); + return map; + } + + private int[][] generateByHoldMoney(int holdMoney) { + int coins[][] = new int[Coin.values().length][1]; + while (holdMoney > 0) { + int random = pickNumberInList(List.of( + COIN10, + COIN50, + COIN100, + COIN500)); + if (holdMoney > random) { + holdMoney -= random; + coins[random][0]++; + } + } + return coins; + } + +} diff --git a/src/main/java/vendingmachine/utils/Convertor.java b/src/main/java/vendingmachine/utils/Convertor.java new file mode 100644 index 000000000..3e7eb967b --- /dev/null +++ b/src/main/java/vendingmachine/utils/Convertor.java @@ -0,0 +1,60 @@ +package vendingmachine.utils; + +import static java.util.regex.Pattern.compile; + +import java.util.AbstractMap; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import vendingmachine.domain.Product; +import vendingmachine.domain.Products; + +public class Convertor { + private static final String PRODUCTS_DELIMITTER = ";"; + private static final String PRODUCT_PREFIX = "["; + private static final String PRODUCT_SUFFIX = "]"; + private static final String PRODUCT_DELIMITTER = ","; + private static final String DELIMITTER_EXCEPTION = "상품명, 가격, 수량은 쉼표로, 개별 상품은 대괄호([])로 묶어야 합니다."; + private static final String PRODUCT_MATCH_REGEX = "\\"+PRODUCT_PREFIX+"^*.*"+"\\"+PRODUCT_SUFFIX+"$"; + private static final Pattern PRODUCT_PATTERN = compile(PRODUCT_MATCH_REGEX); + + public static Products convertToProducts(String input){ + List productSplited = Arrays.stream(input.split(PRODUCTS_DELIMITTER)) + .filter(Convertor::matchProduct) + .map(product -> product.replace(PRODUCT_PREFIX, "").replace(PRODUCT_SUFFIX, "")) + .collect(Collectors.toList()); + Map products = productSplited.stream() + .map(Convertor::convertToProduct) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + return Products.from(products); + } + + private static boolean matchProduct(final String product) { + Matcher matcher = PRODUCT_PATTERN.matcher(product); + if(!matcher.matches()) throw new IllegalArgumentException(DELIMITTER_EXCEPTION); + return true; + } + + private static AbstractMap.SimpleEntry convertToProduct(String product) { + String[] productInformation = product.split(PRODUCT_DELIMITTER); + if(productInformation.length != 3) throw new IllegalArgumentException(DELIMITTER_EXCEPTION); + + String name = productInformation[0]; + int price = covertToInt(productInformation[1]); + int count = covertToInt(productInformation[2]); + + return new AbstractMap.SimpleEntry<>(Product.of(name, price), count); + } + + public static int convertToMoney(final String inputString) { + return covertToInt(inputString); + } + + private static int covertToInt(String input){ + return Integer.parseInt(input); + } + +} diff --git a/src/main/java/vendingmachine/utils/ExceptionHandler.java b/src/main/java/vendingmachine/utils/ExceptionHandler.java new file mode 100644 index 000000000..34135de67 --- /dev/null +++ b/src/main/java/vendingmachine/utils/ExceptionHandler.java @@ -0,0 +1,39 @@ +package vendingmachine.utils; + +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; +import vendingmachine.view.OutputView; + +public class ExceptionHandler { + private static final String ERROR_PREFIX = "[ERROR]"; + private static final int MAX_RECUR_DEPTH = 10; + + public static T input(Supplier supplier, int depth) { + if (depth >= MAX_RECUR_DEPTH) { + throw new IllegalArgumentException( + String.format("[ERROR] 입력 재시도 최대 가능한 %d회를 초과하였습니다.", MAX_RECUR_DEPTH)); + } + try { + return supplier.get(); + } catch (IllegalArgumentException e) { + printExceptionMessage(e); + return input(supplier, depth + 1); + } + } + + public static R convert(Function function, T inputString) { + try { + return function.apply(inputString); + } catch (IllegalArgumentException e) { + printExceptionMessage(e); + return null; + } + } + + private static void printExceptionMessage(final IllegalArgumentException e) { + OutputView.printExceptionMessage(String.format("%s %s", ERROR_PREFIX, e.getMessage())); + } + +} \ No newline at end of file diff --git a/src/main/java/vendingmachine/validators/InputValidator.java b/src/main/java/vendingmachine/validators/InputValidator.java new file mode 100644 index 000000000..d6f47428c --- /dev/null +++ b/src/main/java/vendingmachine/validators/InputValidator.java @@ -0,0 +1,54 @@ +package vendingmachine.validators; + +import static java.util.regex.Pattern.compile; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class InputValidator { + private static final String NUMBER_MATCH_REGEX = "^[0-9]*$"; + private static final Pattern NUMBER = compile(NUMBER_MATCH_REGEX); + private static final String NUMBERFORMAT_EXCEPTION = "정수의 범위를 벗어났습니다"; + private static final String NOT_NUMBER_EXCEPTION = "숫자0-9만 입력 가능합니다"; + private static final String EMPTY_INPUT_EXCEPTION = "사용자의 입력이 비어있습니다."; + + public static String validateInt(final String intInput) { + isNumberPattern(intInput); + isInIntegerRange(intInput); + return intInput; + } + + private static void isInIntegerRange(final String intInput) { + try { + Integer.parseInt(intInput); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(NUMBERFORMAT_EXCEPTION); + } + } + + private static void isNumberPattern(final String intInput) { + Matcher matcher = NUMBER.matcher(intInput); + if (!matcher.matches()) { + throw new IllegalArgumentException(NOT_NUMBER_EXCEPTION); + } + + } + + public static String validateString(final String stringInput) { + isEmptyString(stringInput); + isBlankString(stringInput); + return stringInput; + } + + private static void isBlankString(final String stringInput) { + if (stringInput.isEmpty()) { + throw new IllegalArgumentException(EMPTY_INPUT_EXCEPTION); + } + } + + private static void isEmptyString(final String stringInput) { + if (stringInput.isEmpty()) { + throw new IllegalArgumentException(EMPTY_INPUT_EXCEPTION); + } + } +} diff --git a/src/main/java/vendingmachine/validators/ProductPriceValidator.java b/src/main/java/vendingmachine/validators/ProductPriceValidator.java new file mode 100644 index 000000000..b262b3311 --- /dev/null +++ b/src/main/java/vendingmachine/validators/ProductPriceValidator.java @@ -0,0 +1,29 @@ +package vendingmachine.validators; + +import vendingmachine.domain.Coin; + +public class ProductPriceValidator { + private static final int MINIMAL_PRODUCT_MONEY = 100; + private static final String BOUNDARY_EXCEPTION = String.format("상품의 최소 금액은 %d원입니다", MINIMAL_PRODUCT_MONEY); + private static final String DIVIDED_BYCOIN_EXCEPTION = String.format("상품 금액은 %d원 단위로 나누어 떨어집니다.", + Coin.COIN_10.getAmount()); + + + public static void validate(final int price) { + isBoundary(price); + isDivided(price); + } + + private static void isDivided(final int price) { + if (Coin.COIN_10.isDivided(price)) { + return; + } + throw new IllegalArgumentException(DIVIDED_BYCOIN_EXCEPTION); + } + + private static void isBoundary(final int price) { + if (price < MINIMAL_PRODUCT_MONEY) { + throw new IllegalArgumentException(BOUNDARY_EXCEPTION); + } + } +} diff --git a/src/main/java/vendingmachine/validators/ProductsValidator.java b/src/main/java/vendingmachine/validators/ProductsValidator.java new file mode 100644 index 000000000..8325b40b6 --- /dev/null +++ b/src/main/java/vendingmachine/validators/ProductsValidator.java @@ -0,0 +1,18 @@ +package vendingmachine.validators; + +import java.util.List; + +public class ProductsValidator { + private static final int MINIMAL_PRODUCT_COUNT = 1; + private static final String BOUNDARY_EXCEPTION = String.format("상품의 최소 수량은 %d개 이상입니다", MINIMAL_PRODUCT_COUNT); + + public static void validate(final List counts) { + counts.stream().forEach(ProductsValidator::isBoundary); + } + + private static void isBoundary(final int count) { + if (count < MINIMAL_PRODUCT_COUNT) { + throw new IllegalArgumentException(BOUNDARY_EXCEPTION); + } + } +} diff --git a/src/main/java/vendingmachine/view/Input.java b/src/main/java/vendingmachine/view/Input.java new file mode 100644 index 000000000..faba7f66b --- /dev/null +++ b/src/main/java/vendingmachine/view/Input.java @@ -0,0 +1,9 @@ +package vendingmachine.view; + +public interface Input { + String readHoldMoney(); + String readProducts(); + String readInputAmount(); + String readWanted(); + +} diff --git a/src/main/java/vendingmachine/view/InputView.java b/src/main/java/vendingmachine/view/InputView.java new file mode 100644 index 000000000..0d5c37234 --- /dev/null +++ b/src/main/java/vendingmachine/view/InputView.java @@ -0,0 +1,44 @@ +package vendingmachine.view; + +import camp.nextstep.edu.missionutils.Console; +import vendingmachine.validators.InputValidator; + +public class InputView implements Input{ + + private static final InputView inputView = new InputView(); + + public static Input getInstance() { + return new ProxyInputView(inputView); + } + + @Override + public String readHoldMoney() { + return readInt(); + } + + @Override + public String readProducts() { + return readString(); + } + + @Override + public String readInputAmount() { + return readInt(); + } + + @Override + public String readWanted() { + return readString(); + } + + private String readInt(){ + String intInput = Console.readLine(); + return InputValidator.validateInt(intInput); + } + + private String readString(){ + String stringInput = Console.readLine(); + return InputValidator.validateString(stringInput); + } +} + diff --git a/src/main/java/vendingmachine/view/OutputView.java b/src/main/java/vendingmachine/view/OutputView.java new file mode 100644 index 000000000..716dba113 --- /dev/null +++ b/src/main/java/vendingmachine/view/OutputView.java @@ -0,0 +1,41 @@ +package vendingmachine.view; + +public class OutputView { + public static void printRequestMachinHoldMoney(){ + System.out.println(OutputViewMessage.REQUEST_MACHINE_HOLD_MONEY.getMessage()); + } + public static void printRequestProducts(){ + System.out.println(OutputViewMessage.REQUEST_PRODUCTS.getMessage()); + } + public static void printRequestInputAmount() { + System.out.println(OutputViewMessage.REQUEST_INPUT_AMOUNT.getMessage()); + } + public static void printRemainAmount(int inputAmount) { + System.out.printf(OutputViewMessage.REMAIN_AMOUNT.getMessage(), inputAmount); + } + + public static void printRequestWanted() { + System.out.println(OutputViewMessage.REQUEST_WANTED.getMessage()); + } + + public static void printExceptionMessage(final String error) { + System.out.println(error); + } +} + +enum OutputViewMessage{ + REQUEST_MACHINE_HOLD_MONEY("자판기가 보유하고 있는 금액을 입력해 주세요."), + REQUEST_PRODUCTS("상품명과 가격, 수량을 입력해 주세요."), + REQUEST_INPUT_AMOUNT("투입 금액을 입력해 주세요."), + REQUEST_WANTED("구매할 상품명을 입력해 주세요."), + REMAIN_AMOUNT("투입 금액: %d원\n"); + + private final String message; + private OutputViewMessage(final String message) { + this.message = message; + } + + public String getMessage() { + return message; + } +} \ No newline at end of file diff --git a/src/main/java/vendingmachine/view/ProxyInputView.java b/src/main/java/vendingmachine/view/ProxyInputView.java new file mode 100644 index 000000000..8abb0b260 --- /dev/null +++ b/src/main/java/vendingmachine/view/ProxyInputView.java @@ -0,0 +1,32 @@ +package vendingmachine.view; + +import vendingmachine.utils.ExceptionHandler; + +public class ProxyInputView implements Input { + + private final Input viewable; + + public ProxyInputView(Input viewable) { + this.viewable = viewable; + } + + @Override + public String readHoldMoney() { + return ExceptionHandler.input(viewable::readHoldMoney, 0); + } + + @Override + public String readProducts() { + return ExceptionHandler.input(viewable::readProducts, 0); + } + + @Override + public String readInputAmount() { + return ExceptionHandler.input(viewable::readInputAmount, 0); + } + + @Override + public String readWanted() { + return ExceptionHandler.input(viewable::readWanted, 0); + } +} diff --git a/src/test/java/vendingmachine/domain/ProductTest.java b/src/test/java/vendingmachine/domain/ProductTest.java new file mode 100644 index 000000000..20a83846c --- /dev/null +++ b/src/test/java/vendingmachine/domain/ProductTest.java @@ -0,0 +1,18 @@ +package vendingmachine.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class ProductTest { + + @ParameterizedTest + @CsvSource(value = {"'사이다',1000","'콜라',1500000"}) + void 팩터리메서드_테스트(String name, int price) { + // when + Product result = Product.of(name, price); + // then + assertThat(result).isInstanceOf(Product.class); + } +} \ No newline at end of file diff --git a/src/test/java/vendingmachine/domain/ProductsTest.java b/src/test/java/vendingmachine/domain/ProductsTest.java new file mode 100644 index 000000000..06cf3064b --- /dev/null +++ b/src/test/java/vendingmachine/domain/ProductsTest.java @@ -0,0 +1,36 @@ +package vendingmachine.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +class ProductsTest { + + @ParameterizedTest + @MethodSource("createProducts") + void 생성자_테스트(){ + // given + Map products = new HashMap<>(); + // when + Products result = Products.from(products); + // then + assertThat(result).isInstanceOf(Products.class); + } + + private static Stream> createProducts(){ + Map test1 = new HashMap<>(); + test1.put(Product.of("사이다", 1000), 1); + test1.put(Product.of("콜라", 1000), 101); + + return Stream.of( + new HashMap<>(), + test1 + ); + } +} \ No newline at end of file diff --git a/src/test/java/vendingmachine/utils/CoinGeneratorTest.java b/src/test/java/vendingmachine/utils/CoinGeneratorTest.java new file mode 100644 index 000000000..121d5b92d --- /dev/null +++ b/src/test/java/vendingmachine/utils/CoinGeneratorTest.java @@ -0,0 +1,16 @@ +package vendingmachine.utils; + +import static camp.nextstep.edu.missionutils.Randoms.pickNumberInList; + +import java.util.List; +import org.junit.jupiter.api.Test; + +class CoinGeneratorTest { + + @Test + void pickNumberInList는_주어진동전금액에서_무작위로_하나의동전을선택한다() { + int result = pickNumberInList(List.of(500, 100, 50, 10)); + System.out.println(result); + } + +} \ No newline at end of file diff --git a/src/test/java/vendingmachine/utils/ConvertorTest.java b/src/test/java/vendingmachine/utils/ConvertorTest.java new file mode 100644 index 000000000..93febb3fb --- /dev/null +++ b/src/test/java/vendingmachine/utils/ConvertorTest.java @@ -0,0 +1,50 @@ +package vendingmachine.utils; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatNoException; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import vendingmachine.domain.Product; +import vendingmachine.domain.Products; + +class ConvertorTest { + + @Test + void convertToProducts는_문자열을_상품목록으로_반환한다() { + // given + String input = "[콜라,1500,20];[사이다,1000,10]"; + // when&then + Products products = Convertor.convertToProducts(input); + assertThat(products).isInstanceOf(Products.class); + assertThatNoException().isThrownBy(() -> Convertor.convertToProducts(input)); + } + + @ParameterizedTest + @ValueSource(strings = {"[콜라,1500,,];[사이다,1000,10]", "[사이다,1000,10];[,", " ", "[]", "사이다,1000,10]", "[콜라,1500,,];;[사이다,1000,10]", + "[콜라,1500,1"}) + void convertToProducts는_상품입력형식이_맞지않으면_실패(String input) { + final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> Convertor.convertToProducts(input)); + assertEquals(exception.getMessage(), "상품명, 가격, 수량은 쉼표로, 개별 상품은 대괄호([])로 묶어야 합니다."); + } + + private static Products createProducts() { + Map test = new HashMap<>(); + test.put(Product.of("콜라", 1500), 20); + test.put(Product.of("사이다", 1000), 10); + return Products.from(test); + } + + @Test + void splitTest(){ + String input = "[콜라,1500,20]"; + String pattern = "\\[^*.*\\]$"; + System.out.println("TESTTTT"+input.matches(pattern)); + } +} \ No newline at end of file diff --git a/src/test/java/vendingmachine/validators/InputValidatorTest.java b/src/test/java/vendingmachine/validators/InputValidatorTest.java new file mode 100644 index 000000000..0eab7e17e --- /dev/null +++ b/src/test/java/vendingmachine/validators/InputValidatorTest.java @@ -0,0 +1,36 @@ +package vendingmachine.validators; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatNoException; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class InputValidatorTest { + @ParameterizedTest + @ValueSource(strings = {"1", "111", "0"}) + void validateInt는_0이상의_정수의_범위만_가능하다(String given) { + // when&then + assertThatNoException().isThrownBy(() -> InputValidator.validateInt(given)); + } + + + @ParameterizedTest + @ValueSource(strings = {"1s", "111원", "3000원", "0.0"}) + void validateInt는_숫자만_입력가능하다(String given) { + final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> InputValidator.validateInt(given)); + + assertEquals(exception.getMessage(), "숫자0-9만 입력 가능합니다"); + } + + @ParameterizedTest + @ValueSource(strings = {"10000000000000000000000000000000", "1000000000000000000"}) + void validateInt는_정수범위만_입력가능하다(String given) { + final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> InputValidator.validateInt(given)); + + assertEquals(exception.getMessage(), "정수의 범위를 벗어났습니다"); + } +} \ No newline at end of file diff --git a/src/test/java/vendingmachine/validators/ProductPriceValidatorTest.java b/src/test/java/vendingmachine/validators/ProductPriceValidatorTest.java new file mode 100644 index 000000000..9c3639b56 --- /dev/null +++ b/src/test/java/vendingmachine/validators/ProductPriceValidatorTest.java @@ -0,0 +1,36 @@ +package vendingmachine.validators; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatNoException; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import vendingmachine.domain.Coin; + +class ProductPriceValidatorTest { + + @ParameterizedTest + @ValueSource(ints = {100, 10000, 130000}) + void validate는_가격을_검사한다(int price) { + assertThatNoException().isThrownBy(() -> ProductPriceValidator.validate(price)); + } + + @ParameterizedTest + @ValueSource(ints = {99, 0, -1}) + void validate는_가격이_100원미만이면_실패(int lessThanMinimum) { + final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> ProductPriceValidator.validate(lessThanMinimum)); + assertEquals(exception.getMessage(), String.format("상품의 최소 금액은 %d원입니다", 100)); + } + + @ParameterizedTest + @ValueSource(ints = {199, 123, 1000000001}) + void validate는_가격이_10원으로_나누어떨어지지않으면_실패(int nonDiveded) { + final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> ProductPriceValidator.validate(nonDiveded)); + assertEquals(exception.getMessage(), String.format("상품 금액은 10원 단위로 나누어 떨어집니다.", + Coin.COIN_10.getAmount())); + } + +} \ No newline at end of file diff --git a/src/test/java/vendingmachine/validators/ProductsValidatorTest.java b/src/test/java/vendingmachine/validators/ProductsValidatorTest.java new file mode 100644 index 000000000..9da48f48e --- /dev/null +++ b/src/test/java/vendingmachine/validators/ProductsValidatorTest.java @@ -0,0 +1,30 @@ +package vendingmachine.validators; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatNoException; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.List; +import org.junit.jupiter.api.Test; + +class ProductsValidatorTest { + + @Test + void validate는_최소수량_이상인지_검사한다() { + // given + List given = List.of(1, 2, 3, 4, 5, 6); + // when&then + assertThatNoException().isThrownBy(() -> ProductsValidator.validate(given)); + } + + @Test + void validate는_최소수량_미만이면_실패() { + // given + List given = List.of(1, 2, 3, 4, 0, 6); + // when&then + final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> ProductsValidator.validate(given)); + assertEquals(exception.getMessage(), String.format("상품의 최소 수량은 %d개 이상입니다", 1)); + + } +} \ No newline at end of file