diff --git a/README.md b/README.md index edd124ad18..f2eb4ad78c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -# java-lotto +# java-lottoTicket 로또 미션 진행을 위한 저장소 diff --git a/build.gradle b/build.gradle index 163d901377..db8d2fdbd8 100644 --- a/build.gradle +++ b/build.gradle @@ -9,9 +9,6 @@ repositories { } dependencies { - compile('com.sparkjava:spark-core:2.9.0') - compile('com.sparkjava:spark-template-handlebars:2.7.1') - compile('ch.qos.logback:logback-classic:1.2.3') testCompile('org.junit.jupiter:junit-jupiter:5.6.0') testCompile('org.assertj:assertj-core:3.15.0') } diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000000..e3c10565c6 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,70 @@ +## 객체 협력 정리 +![객체 협력 정리](../image/객체협력정리.png) + + +## 매세지 정의 +- 로또를 구매하라 +- 로또를 생성하라 +- 로또 갯수만큼 로또 번호를 생성하라 +- 로또 번호를 생성하라 +- 한 게임을 생성하라 +- 구입금액을 입력하라 +- 당첨 번호를 입력하라 +- 보너스 볼을 입력하라 +- 로또 갯수를 구하라 +- 생성된 로또번호를 출력하라 +- 당첨 갯수를 구하라 +- 수익률을 계산하라 + +## 기능 요구사항 정리 +- [x] 구현 부분 구입금액 입력 확인 +- [x] 보장된 구입금액을 입력 받는다. + - 보장된 구입금액(로또 티켓 구입 금액보다 큰 양수의 숫자) + - [x] 보장된 구입금액이 아니면 에러가 발생한다. + - `IllegalArgumentException` +- [x] 구입금액 따른 로또의 갯수를 구한다. +- [x] 보장된 로또 번호로 한 로또를 생성한다. + - 보장된 로또 번호(중복되지 않은 1~45의 숫자이며, 6개의 숫자) +- [x] 로또 갯수만큼 보장된 로또번호를 출력한다. + - 보장된 로또번호(중복되지 않은 1~45의 숫자이며, 6개의 숫자) +- [x] 콘솔 창에 당첨번호를 입력한다. +- [x] 보장된 당첨 번호를 입력한다. + - 보장된 당첨 번호(중복되지 않은 1~45의 숫자, 6개의 숫자) + - [x] 보장된 당첨 번호가 아니면 에러가 발생한다. + - `IllegalArgumentException` +- [x] 보장된 보너스 볼을 입력 받는다. + - 보장된 보너스 볼(보장된 당첨 번호와 중복되지 않는 1~45의 숫자) + - [x] 보장된 보너스 볼이 아니면 에러가 발생한다. + - `IllegalArgumentException` +- [x] 보너스 볼 포함 당첨 현황을 구한다. +- [x] 당첨 통계를 출력한다. +- [x] 수익률을 출력한다. + +## 리펙토링 목록 정리 +- [x] DTO 제거기(DTO 없이 view에 값을 전달 할 수 있다.) +- [x] 책임에 대해 한번더 생각하기 +- [x] BonusBall 제거 +- [x] LottoGameMachine 책임 분리하기 + - [x] 입력 파싱 책임 제거 + - [x] gnerate 제거 + - [x] of 제거 +- [x] 테스트메서드 한글 이름 영어로 변경 (의도를 더 드러내자) +- [x] 사용되지 않는 값 제거 +- [x] 사용되지 않은 메서드 제거 +- [x] 제네릭 빠진 것 확인 +- [x] DisplayName 의도를 더 정확히 드러내 + + +## 프로그래밍 요구사항 +- indent(인덴트, 들여쓰기) depth를 2단계에서 1단계로 줄여라. + - depth의 경우 if문을 사용하는 경우 1단계의 depth가 증가한다. if문 안에 while문을 사용한다면 depth가 2단계가 된다. +- else를 사용하지 마라. +- 메소드의 크기가 최대 10라인을 넘지 않도록 구현한다. + - method가 한 가지 일만 하도록 최대한 작게 만들어라. +- 배열 대신 ArrayList를 사용한다. +- java enum을 적용해 프로그래밍을 구현한다. +- 규칙 3: 모든 원시값과 문자열을 포장한다. +- 규칙 5: 줄여쓰지 않는다(축약 금지). +- 규칙 8: 일급 콜렉션을 쓴다. + + \ No newline at end of file diff --git "a/image/\352\260\235\354\262\264\355\230\221\353\240\245\354\240\225\353\246\254.png" "b/image/\352\260\235\354\262\264\355\230\221\353\240\245\354\240\225\353\246\254.png" new file mode 100644 index 0000000000..b1173ceb53 Binary files /dev/null and "b/image/\352\260\235\354\262\264\355\230\221\353\240\245\354\240\225\353\246\254.png" differ diff --git a/src/main/java/Application.java b/src/main/java/Application.java new file mode 100644 index 0000000000..5fed41c284 --- /dev/null +++ b/src/main/java/Application.java @@ -0,0 +1,13 @@ +import domain.LottoGameMachine; +import service.LottoService; +import view.LottoGameScreen; +import view.MainScreen; + +public class Application { + public static void main(String[] args) { + LottoGameMachine lottoGameMachine = new LottoGameMachine(); + LottoService lottoService = new LottoService(lottoGameMachine); + GameManageApplication gameManageApplication = new GameManageApplication(new MainScreen(), new LottoGameScreen(), lottoService); + gameManageApplication.run(); + } +} diff --git a/src/main/java/GameManageApplication.java b/src/main/java/GameManageApplication.java new file mode 100644 index 0000000000..b7804dea30 --- /dev/null +++ b/src/main/java/GameManageApplication.java @@ -0,0 +1,56 @@ +import domain.bettingMoney.BettingMoney; +import domain.lotto.LottoTickets; +import domain.lotto.TicketCount; +import domain.lotto.WinningLotto; +import domain.result.Result; +import service.LottoService; +import util.InputUtil; +import view.LottoGameScreen; +import view.MainScreen; +import view.dto.LottoGameResultDto; + +import java.util.Set; + +public class GameManageApplication { + private final MainScreen mainScreen; + private final LottoGameScreen lottoGameScreen; + private final LottoService lottoService; + + public GameManageApplication(final MainScreen mainScreen, final LottoGameScreen lottoGameScreen, LottoService lottoService) { + this.mainScreen = mainScreen; + this.lottoGameScreen = lottoGameScreen; + this.lottoService = lottoService; + } + + public void run() { + BettingMoney bettingMoney = getBettingMoney(); + TicketCount ticketCount = getTicketCount(bettingMoney); + mainScreen.showTicketCount(ticketCount); + LottoTickets lottoTickets = lottoService.getLottoTickets(bettingMoney); + lottoGameScreen.showAllLottoStatus(lottoTickets.getLottoTickets()); + WinningLotto winningLotto = getWinningLotto(); + + Result result = new Result(lottoTickets, winningLotto); + lottoGameScreen.showGameResult(new LottoGameResultDto(result.getResults())); + lottoGameScreen.showRevenueResult(result.findEarningsRate(bettingMoney)); + } + + private WinningLotto getWinningLotto() { + lottoGameScreen.confirmWinningLotto(); + Set winningNumbers = InputUtil.inputWinningNumbers(); + lottoGameScreen.confirmBonusLotto(); + int bonusNumber = InputUtil.inputBonusNumber(); + return new WinningLotto(winningNumbers, bonusNumber); + } + + private TicketCount getTicketCount(final BettingMoney bettingMoney) { + int ticketCount = bettingMoney.getTicketCount(); + return TicketCount.of(ticketCount); + } + + private BettingMoney getBettingMoney() { + mainScreen.showInputMoney(); + int input = InputUtil.nextInt(); + return BettingMoney.of(input); + } +} diff --git a/src/main/java/domain/LottoGameMachine.java b/src/main/java/domain/LottoGameMachine.java new file mode 100644 index 0000000000..b6506a1133 --- /dev/null +++ b/src/main/java/domain/LottoGameMachine.java @@ -0,0 +1,24 @@ +package domain; + +import domain.ball.LottoBall; +import domain.ball.LottoBalls; +import domain.bettingMoney.BettingMoney; +import domain.lotto.LottoTicket; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class LottoGameMachine { + public List buyTickets(BettingMoney bettingMoney) { + int ticketCount = bettingMoney.getTicketCount(); + return IntStream.range(0, ticketCount) + .mapToObj(count -> makeTicket()) + .collect(Collectors.toList()); + } + + private LottoTicket makeTicket() { + List lottoBalls = LottoBalls.getRandomLottoBalls(); + return new LottoTicket(new LottoBalls(lottoBalls)); + } +} diff --git a/src/main/java/domain/ball/LottoBall.java b/src/main/java/domain/ball/LottoBall.java new file mode 100644 index 0000000000..7484a9ebd3 --- /dev/null +++ b/src/main/java/domain/ball/LottoBall.java @@ -0,0 +1,56 @@ +package domain.ball; + +import java.util.Objects; + +public class LottoBall implements Comparable { + public static final int MIN_LOTTO_VALUE = 1; + public static final int MAX_LOTTO_VALUE = 45; + public static final String PERMIT_LOTTO_NUMBER_EXCEPTION_MESSAGE = "%d~%d 사이의 번호만 허용합니다."; + + private final int value; + + public LottoBall(final int value) { + validateNumber(value); + this.value = value; + } + + private void validateNumber(final int value) { + if (!isBetweenNumber(value)) { + throw new IllegalArgumentException(String.format(PERMIT_LOTTO_NUMBER_EXCEPTION_MESSAGE, MIN_LOTTO_VALUE, MAX_LOTTO_VALUE)); + } + } + + private boolean isBetweenNumber(final int number) { + return number >= MIN_LOTTO_VALUE && number <= MAX_LOTTO_VALUE; + } + + public int getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + LottoBall that = (LottoBall) o; + return value == that.value; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + + @Override + public int compareTo(LottoBall o) { + if (this.value > o.value) { + return 1; + } + return -1; + } + + @Override + public String toString() { + return value + " "; + } +} diff --git a/src/main/java/domain/ball/LottoBalls.java b/src/main/java/domain/ball/LottoBalls.java new file mode 100644 index 0000000000..79078fd3fa --- /dev/null +++ b/src/main/java/domain/ball/LottoBalls.java @@ -0,0 +1,75 @@ +package domain.ball; + +import domain.result.LottoRank; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static domain.ball.LottoBall.MAX_LOTTO_VALUE; +import static domain.ball.LottoBall.MIN_LOTTO_VALUE; + +public class LottoBalls { + private static final int LOTTO_BALL_SIZE = 6; + + private final List lottoBalls; + + public LottoBalls(final List lottoBalls) { + List copy = new ArrayList<>(lottoBalls); + validateLottoNumbers(copy); + this.lottoBalls = copy; + } + + public static List getRandomLottoBalls() { + List lottoBalls = IntStream.rangeClosed(MIN_LOTTO_VALUE, MAX_LOTTO_VALUE) + .mapToObj(LottoBall::new) + .collect(Collectors.toList()); + Collections.shuffle(lottoBalls); + return lottoBalls.stream() + .limit(LOTTO_BALL_SIZE) + .sorted() + .collect(Collectors.toList()); + } + + public List getLottoBalls() { + List copy = new ArrayList<>(this.lottoBalls); + return Collections.unmodifiableList(copy); + } + + public LottoRank matchCount(LottoBalls lottoBalls, LottoBall bonusBall) { + int count = (int) this.lottoBalls.stream() + .filter(lottoBalls::contains) + .count(); + + boolean containBonus = this.lottoBalls.stream() + .anyMatch(bonusBall::equals); + + return LottoRank.findRankByBonusAndMatches(containBonus, count); + } + + private void validateLottoNumbers(final List lottoBalls) { + validateDuplicate(lottoBalls); + validateSize(lottoBalls); + } + + private void validateDuplicate(final List lottoBalls) { + boolean isUnique = lottoBalls.stream() + .allMatch(new HashSet<>()::add); + if (!isUnique) { + throw new IllegalArgumentException(String.format("로또 번호에 중복된 값이 있습니다. 다시 입력해주세요. 입력값 %s", lottoBalls.toString())); + } + } + + private void validateSize(final List lottoNumbers) { + if (lottoNumbers.size() != LOTTO_BALL_SIZE) { + throw new IllegalArgumentException(String.format("%d개의 로또 번호가 필요합니다.", LOTTO_BALL_SIZE)); + } + } + + private boolean contains(LottoBall lottoBall) { + return this.lottoBalls.contains(lottoBall); + } +} diff --git a/src/main/java/domain/bettingMoney/BettingMoney.java b/src/main/java/domain/bettingMoney/BettingMoney.java new file mode 100644 index 0000000000..15680f5605 --- /dev/null +++ b/src/main/java/domain/bettingMoney/BettingMoney.java @@ -0,0 +1,36 @@ +package domain.bettingMoney; + +import java.math.BigDecimal; + +import static domain.lotto.LottoTicket.TICKET_PRICE; + +public class BettingMoney { + private static final String NOT_ENOUGH_MONEY_EXCEPTION_MESSAGE = "%d 이상의 금액만 가능합니다. 현재 입력 금액: %d"; + + private final int bettingMoney; + + private BettingMoney(final int bettingMoney) { + validateMoney(bettingMoney); + this.bettingMoney = bettingMoney; + } + + public static BettingMoney of(final int bettingMoney) { + return new BettingMoney(bettingMoney); + } + + public int getTicketCount() { + return this.bettingMoney / TICKET_PRICE; + } + + public BigDecimal getEarningRate(final int prizeMoney) { + BigDecimal bettingMoney = BigDecimal.valueOf(this.bettingMoney); + BigDecimal prize = BigDecimal.valueOf(prizeMoney); + return prize.divide(bettingMoney); + } + + private void validateMoney(final int bettingMoney) { + if (bettingMoney < TICKET_PRICE) { + throw new IllegalArgumentException(String.format(NOT_ENOUGH_MONEY_EXCEPTION_MESSAGE, TICKET_PRICE, bettingMoney)); + } + } +} diff --git a/src/main/java/domain/lotto/LottoTicket.java b/src/main/java/domain/lotto/LottoTicket.java new file mode 100644 index 0000000000..436383d9c6 --- /dev/null +++ b/src/main/java/domain/lotto/LottoTicket.java @@ -0,0 +1,27 @@ +package domain.lotto; + +import domain.ball.LottoBall; +import domain.ball.LottoBalls; +import domain.result.LottoRank; + +import java.util.Collections; +import java.util.List; + +public class LottoTicket { + public static final int TICKET_PRICE = 1000; + + private final LottoBalls lottoBalls; + + public LottoTicket(final LottoBalls lottoBalls) { + this.lottoBalls = lottoBalls; + } + + public List getLottoBalls() { + List lottoBalls = this.lottoBalls.getLottoBalls(); + return Collections.unmodifiableList(lottoBalls); + } + + public LottoRank findMatchesNumber(WinningLotto winningLotto) { + return winningLotto.winningMatchCount(lottoBalls); + } +} diff --git a/src/main/java/domain/lotto/LottoTickets.java b/src/main/java/domain/lotto/LottoTickets.java new file mode 100644 index 0000000000..bc6c1cb009 --- /dev/null +++ b/src/main/java/domain/lotto/LottoTickets.java @@ -0,0 +1,30 @@ +package domain.lotto; + +import domain.result.LottoRank; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class LottoTickets { + private final List lottoTickets; + + public LottoTickets(List lottoTickets) { + this.lottoTickets = new ArrayList<>(lottoTickets); + } + + public List getLottoTickets() { + ArrayList copy = new ArrayList<>(this.lottoTickets); + return Collections.unmodifiableList(copy); + } + + public List findMatches(WinningLotto winningLotto) { + List lottoRanks = new ArrayList<>(); + lottoTickets.stream() + .map(lotto -> lotto.findMatchesNumber(winningLotto)) + .forEach(match -> { + lottoRanks.add(match); + }); + return lottoRanks; + } +} diff --git a/src/main/java/domain/lotto/TicketCount.java b/src/main/java/domain/lotto/TicketCount.java new file mode 100644 index 0000000000..e112d7d066 --- /dev/null +++ b/src/main/java/domain/lotto/TicketCount.java @@ -0,0 +1,27 @@ +package domain.lotto; + +public class TicketCount { + private static final int ZERO = 0; + private static final String TICKET_MINIMUM_SIZE_EXCEPTION_MESSAGE = "최소 한 개의 로또를 구매해야 합니다. 현재 갯수: %d"; + + private final int ticketCount; + + private TicketCount(final int ticketCount) { + validateLottoCount(ticketCount); + this.ticketCount = ticketCount; + } + + public static TicketCount of(final int lottoCount) { + return new TicketCount(lottoCount); + } + + public int getTicketCount() { + return ticketCount; + } + + private void validateLottoCount(final int lottoCount) { + if (lottoCount <= ZERO) { + throw new IllegalArgumentException(String.format(TICKET_MINIMUM_SIZE_EXCEPTION_MESSAGE, lottoCount)); + } + } +} diff --git a/src/main/java/domain/lotto/WinningLotto.java b/src/main/java/domain/lotto/WinningLotto.java new file mode 100644 index 0000000000..04dbec8020 --- /dev/null +++ b/src/main/java/domain/lotto/WinningLotto.java @@ -0,0 +1,52 @@ +package domain.lotto; + +import domain.ball.LottoBall; +import domain.ball.LottoBalls; +import domain.result.LottoRank; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class WinningLotto { + private static final String WINNING_NUMBERS_SIZE_EXCEPTION_MESSAGE = "우승 로또 번호의 갯수는 %d개 이어야 합니다. 현재 갯수: %d"; + private static final String DUPLICATE_BONUS_NUMBER_EXCEPTION_MESSAGE = "보너스 번호 %d는 중복된 번호입니다"; + private static final int WINNING_NUMBERS_SIZE = 6; + + private final LottoBalls winningBalls; + private final LottoBall bonusBall; + + public WinningLotto(final Set winningNumbers, final int bonusNumber) { + validateWinningNumbers(winningNumbers); + validateBonusNumber(winningNumbers, bonusNumber); + this.winningBalls = makeWinningBalls(winningNumbers); + this.bonusBall = makeBonusBall(bonusNumber); + } + + public LottoRank winningMatchCount(final LottoBalls lottoBalls) { + return lottoBalls.matchCount(this.winningBalls, bonusBall); + } + + private LottoBalls makeWinningBalls(Set winningNumbers) { + List lottoBalls = winningNumbers.stream() + .map(LottoBall::new) + .collect(Collectors.toList()); + return new LottoBalls(lottoBalls); + } + + private LottoBall makeBonusBall(int bonusNumber) { + return new LottoBall(bonusNumber); + } + + private void validateWinningNumbers(Set winningNumbers) { + if (winningNumbers.size() != WINNING_NUMBERS_SIZE) { + throw new IllegalArgumentException(String.format(WINNING_NUMBERS_SIZE_EXCEPTION_MESSAGE, WINNING_NUMBERS_SIZE, winningNumbers.size())); + } + } + + private void validateBonusNumber(final Set winningNumbers, final int bonusNumber) { + if (winningNumbers.contains(bonusNumber)) { + throw new IllegalArgumentException(String.format(DUPLICATE_BONUS_NUMBER_EXCEPTION_MESSAGE, bonusNumber)); + } + } +} diff --git a/src/main/java/domain/result/LottoRank.java b/src/main/java/domain/result/LottoRank.java new file mode 100644 index 0000000000..01a06ee644 --- /dev/null +++ b/src/main/java/domain/result/LottoRank.java @@ -0,0 +1,50 @@ +package domain.result; + +import java.util.Arrays; + +public enum LottoRank { + NONE_MATCHES(0, 0, false), + THREE_MATCHES(3, 5_000, false), + FOUR_MATCHES(4, 50_000, false), + FIVE_MATCHES(5, 1_500_000, false), + FIVE_AND_BONUS_MATCHES(5, 30_000_00, true), + SIX_MATCHES(6, 2_000_000_000, false); + + private final int matches; + private final int prize; + private final boolean isSecond; + + LottoRank(final int matches, final int prize, boolean isSecond) { + this.matches = matches; + this.prize = prize; + this.isSecond = isSecond; + } + + public static LottoRank findRankByBonusAndMatches(final boolean isBonus, final int matches) { + if (!isBonus || matches != 5) { + return Arrays.stream(values()).filter(lottoRank -> lottoRank.isSameMatches(matches)) + .findFirst().orElse(NONE_MATCHES); + } + return FIVE_AND_BONUS_MATCHES; + } + + public int getPrize() { + return prize; + } + + public int getMatches() { + return matches; + } + + public boolean isSecond() { + return isSecond; + } + + public boolean hasMatches() { + return this != NONE_MATCHES; + } + + private boolean isSameMatches(final int matches) { + return this.matches == matches; + } +} diff --git a/src/main/java/domain/result/Result.java b/src/main/java/domain/result/Result.java new file mode 100644 index 0000000000..09d0761710 --- /dev/null +++ b/src/main/java/domain/result/Result.java @@ -0,0 +1,49 @@ +package domain.result; + +import domain.bettingMoney.BettingMoney; +import domain.lotto.LottoTickets; +import domain.lotto.WinningLotto; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class Result { + private final LottoTickets lottoTickets; + private final WinningLotto winningLotto; + private final Map results; + + public Result(LottoTickets lottoTickets, WinningLotto winningLotto) { + this.lottoTickets = lottoTickets; + this.winningLotto = winningLotto; + this.results = new HashMap<>(); + setResult(); + } + + public BigDecimal findEarningsRate(BettingMoney bettingMoney) { + int prize = results.entrySet().stream() + .map(Map.Entry::getKey) + .mapToInt(lottoRank -> lottoRank.getPrize() * results.get(lottoRank)) + .sum(); + return bettingMoney.getEarningRate(prize); + } + + public Map getResults() { + return results; + } + + private Map setResult() { + List lottoRanks = lottoTickets.findMatches(winningLotto); + lottoRanks.forEach(this::putResult); + return results; + } + + private void putResult(final LottoRank lottoRank) { + if (!results.containsKey(lottoRank)) { + results.put(lottoRank, 1); + return; + } + results.put(lottoRank, results.get(lottoRank) + 1); + } +} diff --git a/src/main/java/lotto/WebUILottoApplication.java b/src/main/java/lotto/WebUILottoApplication.java deleted file mode 100644 index 1926550119..0000000000 --- a/src/main/java/lotto/WebUILottoApplication.java +++ /dev/null @@ -1,22 +0,0 @@ -package lotto; - -import spark.ModelAndView; -import spark.template.handlebars.HandlebarsTemplateEngine; - -import java.util.HashMap; -import java.util.Map; - -import static spark.Spark.get; - -public class WebUILottoApplication { - public static void main(String[] args) { - get("/", (req, res) -> { - Map model = new HashMap<>(); - return render(model, "index.html"); - }); - } - - private static String render(Map model, String templatePath) { - return new HandlebarsTemplateEngine().render(new ModelAndView(model, templatePath)); - } -} diff --git a/src/main/java/service/LottoService.java b/src/main/java/service/LottoService.java new file mode 100644 index 0000000000..f3bc3c720d --- /dev/null +++ b/src/main/java/service/LottoService.java @@ -0,0 +1,21 @@ +package service; + +import domain.LottoGameMachine; +import domain.bettingMoney.BettingMoney; +import domain.lotto.LottoTicket; +import domain.lotto.LottoTickets; + +import java.util.List; + +public class LottoService { + private final LottoGameMachine lottoGameMachine; + + public LottoService(LottoGameMachine lottoGameMachine) { + this.lottoGameMachine = lottoGameMachine; + } + + public LottoTickets getLottoTickets(BettingMoney bettingMoney) { + List lottoTickets = lottoGameMachine.buyTickets(bettingMoney); + return new LottoTickets(lottoTickets); + } +} diff --git a/src/main/java/util/InputUtil.java b/src/main/java/util/InputUtil.java new file mode 100644 index 0000000000..97e08acd40 --- /dev/null +++ b/src/main/java/util/InputUtil.java @@ -0,0 +1,35 @@ +package util; + +import java.util.Arrays; +import java.util.Scanner; +import java.util.Set; +import java.util.stream.Collectors; + +public class InputUtil { + private static final Scanner SCANNER = new Scanner(System.in); + private static final String DELIMITER = ","; + + private InputUtil() { + } + + public static int nextInt() { + String inputValue = SCANNER.nextLine(); + try { + return Integer.parseInt(inputValue); + } catch (NumberFormatException numberFormatException) { + throw new IllegalArgumentException(String.format("자연수만 입력 가능합니다. | 현재 입력 값 : %s ", inputValue)); + } + } + + public static Set inputWinningNumbers() { + String winningNumbersText = SCANNER.nextLine(); + return Arrays.stream(winningNumbersText.split(DELIMITER)) + .map(String::trim) + .map(Integer::parseInt) + .collect(Collectors.toSet()); + } + + public static int inputBonusNumber() { + return nextInt(); + } +} diff --git a/src/main/java/util/OutputUtil.java b/src/main/java/util/OutputUtil.java new file mode 100644 index 0000000000..ac741ed589 --- /dev/null +++ b/src/main/java/util/OutputUtil.java @@ -0,0 +1,10 @@ +package util; + +public class OutputUtil { + private OutputUtil() { + } + + public static void printMessage(final String message) { + System.out.println(message); + } +} diff --git a/src/main/java/view/LottoGameScreen.java b/src/main/java/view/LottoGameScreen.java new file mode 100644 index 0000000000..2cc1ab35c0 --- /dev/null +++ b/src/main/java/view/LottoGameScreen.java @@ -0,0 +1,85 @@ +package view; + +import domain.ball.LottoBall; +import domain.lotto.LottoTicket; +import domain.result.LottoRank; +import util.OutputUtil; +import view.dto.LottoGameResultDto; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +public class LottoGameScreen { + public static final String LOTTO_PREFIX = "["; + public static final String LOTTO_POSTFIX = "]"; + public static final String DELIMITER = ", "; + public static final String WINNING_LOTTO_CONFIRMATION = "\n지난 주 당첨 번호를 입력해 주세요."; + public static final String BONUS_LOTTO_CONFIRMATION = "보너스 볼을 입력해 주세요."; + public static final String RESULT = "당첨통계"; + public static final String LINE = "----------"; + public static final String LOTTO_RESULT_MESSAGE = "%s개 일치 (%d원)- %d개"; + public static final String SECOND_PRIZE_RESULT_MESSAGE = "%d개 일치, 보너스 볼 일치 (%d)원- %d"; + public static final String REVENUE_RESULT_FORMATTER = "총 수익률은 %.2f입니다."; + + public void showAllLottoStatus(final List lottoTickets) { + lottoTickets.stream() + .forEach(lottoTicket -> showTicketStatus(lottoTicket)); + } + + public void confirmWinningLotto() { + OutputUtil.printMessage(WINNING_LOTTO_CONFIRMATION); + } + + public void confirmBonusLotto() { + OutputUtil.printMessage(BONUS_LOTTO_CONFIRMATION); + } + + public void showGameResult(LottoGameResultDto lottoGameResultDto) { + OutputUtil.printMessage(RESULT); + OutputUtil.printMessage(LINE); + showMatchesResult(lottoGameResultDto); + } + + public void showRevenueResult(BigDecimal earningsRate) { + OutputUtil.printMessage(String.format(REVENUE_RESULT_FORMATTER, earningsRate.doubleValue())); + } + + private void showTicketStatus(final LottoTicket lottoTicket) { + String lottoStatus = makeSingleLottoTicketStatus(lottoTicket.getLottoBalls()); + OutputUtil.printMessage(lottoStatus); + } + + private String makeSingleLottoTicketStatus(final List lottoBalls) { + List status = lottoBalls.stream() + .map(lottoBall -> String.valueOf(lottoBall.getValue())) + .collect(Collectors.toList()); + return LOTTO_PREFIX + String.join(DELIMITER, status) + LOTTO_POSTFIX; + } + + private void showMatchesResult(LottoGameResultDto lottoGameResultDto) { + Map matches = lottoGameResultDto.getMatches(); + Arrays.stream(LottoRank.values()) + .filter(LottoRank::hasMatches) + .forEach(lottoRank -> + OutputUtil.printMessage(String.format(findMessage(lottoRank), lottoRank.getMatches(), lottoRank.getPrize(), getCount(matches, lottoRank)))); + } + + private String findMessage(LottoRank key) { + if (key.isSecond()) { + return SECOND_PRIZE_RESULT_MESSAGE; + } + return LOTTO_RESULT_MESSAGE; + } + + private Integer getCount(Map matches, LottoRank lottoRank) { + Integer count = matches.get(lottoRank); + if (Objects.isNull(count)) { + return 0; + } + return count; + } +} diff --git a/src/main/java/view/MainScreen.java b/src/main/java/view/MainScreen.java new file mode 100644 index 0000000000..bdebcde553 --- /dev/null +++ b/src/main/java/view/MainScreen.java @@ -0,0 +1,16 @@ +package view; + +import domain.lotto.TicketCount; +import util.OutputUtil; + +public class MainScreen { + public static final String BUY_STATUS = "%d개를 구매했습니다."; + + public void showInputMoney() { + OutputUtil.printMessage("구입금액을 입력해 주세요."); + } + + public void showTicketCount(final TicketCount lottoCount) { + OutputUtil.printMessage(String.format(BUY_STATUS, lottoCount.getTicketCount())); + } +} diff --git a/src/main/java/view/dto/LottoGameResultDto.java b/src/main/java/view/dto/LottoGameResultDto.java new file mode 100644 index 0000000000..f5d2967383 --- /dev/null +++ b/src/main/java/view/dto/LottoGameResultDto.java @@ -0,0 +1,17 @@ +package view.dto; + +import domain.result.LottoRank; + +import java.util.Map; + +public class LottoGameResultDto { + private final Map matches; + + public LottoGameResultDto(Map matches) { + this.matches = matches; + } + + public Map getMatches() { + return matches; + } +} diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html deleted file mode 100644 index 3a7dbfecdf..0000000000 --- a/src/main/resources/templates/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - 로또 - - -Hello World!! - - \ No newline at end of file diff --git a/src/test/java/domain/ball/LottoBallTest.java b/src/test/java/domain/ball/LottoBallTest.java new file mode 100644 index 0000000000..6275fc8256 --- /dev/null +++ b/src/test/java/domain/ball/LottoBallTest.java @@ -0,0 +1,27 @@ +package domain.ball; + +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 static org.assertj.core.api.Assertions.assertThatCode; + +class LottoBallTest { + + @DisplayName("LottoBall 정상 생성 테스트.") + @Test + void lottoBallGenerateTest() { + assertThatCode(() -> new LottoBall(3)) + .doesNotThrowAnyException(); + } + + @DisplayName("보장되지 않은(중복되지 않은 1~45의 숫자이며, 6개의 숫자) LottoBall이 저장될 시 에러 발생한다.") + @ParameterizedTest + @ValueSource(ints = {-1, 0, 46, 47}) + void lottoBallNotGuaranteedErrorTest(int value) { + Assertions.assertThatThrownBy(() -> new LottoBall(value)) + .isInstanceOf(IllegalArgumentException.class); + } +} \ No newline at end of file diff --git a/src/test/java/domain/ball/LottoBallsTest.java b/src/test/java/domain/ball/LottoBallsTest.java new file mode 100644 index 0000000000..12d28a78df --- /dev/null +++ b/src/test/java/domain/ball/LottoBallsTest.java @@ -0,0 +1,61 @@ +package domain.ball; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class LottoBallsTest { + + @DisplayName("LottoBalls 정상 생성 테스트.") + @Test + void lottoBallsGenerateTest() { + //given + List lottoNumbers = Arrays.asList(1, 2, 3, 4, 5, 6); + + //when + List lottoBalls = lottoNumbers.stream() + .map(lottoNumber -> new LottoBall(lottoNumber)) + .collect(Collectors.toList()); + + //then + assertThatCode(() -> new LottoBalls(lottoBalls)) + .doesNotThrowAnyException(); + } + + @DisplayName("LottoBalls 인스턴스의 사이즈가 6이 아니면 에러가 발생한다.") + @Test + void lottoBallsSizeErrorTest() { + //given + List lottoNumbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7); + + //when + List lottoBalls = lottoNumbers.stream() + .map(lottoNumber -> new LottoBall(lottoNumber)) + .collect(Collectors.toList()); + + assertThatThrownBy(() -> new LottoBalls(lottoBalls)) + .isInstanceOf(IllegalArgumentException.class); + } + + @DisplayName("LottoBalls에 중복된 LottoBall이 있다면 에러가 발생한다.") + @Test + void lottoBallsDuplicateErrorTest() { + //given + List lottoNumbers = Arrays.asList(1, 2, 3, 4, 6, 6); + + //when + List lottoBalls = lottoNumbers.stream() + .map(lottoNumber -> new LottoBall(lottoNumber)) + .collect(Collectors.toList()); + + //then + assertThatThrownBy(() -> new LottoBalls(lottoBalls)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/domain/bettingMoney/BettingMoneyTest.java b/src/test/java/domain/bettingMoney/BettingMoneyTest.java new file mode 100644 index 0000000000..b918337e3a --- /dev/null +++ b/src/test/java/domain/bettingMoney/BettingMoneyTest.java @@ -0,0 +1,27 @@ +package domain.bettingMoney; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class BettingMoneyTest { + + @DisplayName("BettingMoney 정상 생성테스트.") + @ParameterizedTest + @ValueSource(ints = {10000, 14000}) + void bettingMoneyGenerateTest(int value) { + assertThatCode(() -> BettingMoney.of(value)) + .doesNotThrowAnyException(); + } + + @DisplayName("보장된 숫자(로또 티켓 구입 금액보다 큰 양수의 숫자))가 아니면 에러가 발생한다.") + @ParameterizedTest + @ValueSource(ints = {-1, 0, 900}) + void bettingMoneyNotGuaranteedErrorTest(int value) { + assertThatThrownBy(() -> BettingMoney.of(value)) + .isInstanceOf(IllegalArgumentException.class); + } +} \ No newline at end of file diff --git a/src/test/java/domain/lotto/LottoTicketTest.java b/src/test/java/domain/lotto/LottoTicketTest.java new file mode 100644 index 0000000000..d9d576db6c --- /dev/null +++ b/src/test/java/domain/lotto/LottoTicketTest.java @@ -0,0 +1,30 @@ +package domain.lotto; + +import domain.ball.LottoBall; +import domain.ball.LottoBalls; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThatCode; + +public class LottoTicketTest { + + @DisplayName("LottoTicket 정상 생성 테스트.") + @Test + public void lottoTicketGenerateTest() { + //given + List lottoNumbers = Arrays.asList(1, 2, 3, 4, 5, 6); + + //when + List lottoBalls = lottoNumbers.stream() + .map(lottoNumber -> new LottoBall(lottoNumber)) + .collect(Collectors.toList()); + + //then + assertThatCode(() -> new LottoTicket(new LottoBalls(lottoBalls))).doesNotThrowAnyException(); + } +} diff --git a/src/test/java/domain/lotto/TicketCountTest.java b/src/test/java/domain/lotto/TicketCountTest.java new file mode 100644 index 0000000000..f3dcccc753 --- /dev/null +++ b/src/test/java/domain/lotto/TicketCountTest.java @@ -0,0 +1,27 @@ +package domain.lotto; + +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 static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class TicketCountTest { + + @DisplayName("ticketCount 정상 생성 테스트.") + @Test + void ticketCountGenerateTest() { + assertThatCode(() -> TicketCount.of(1)) + .doesNotThrowAnyException(); + } + + @DisplayName("ticketCount의 수가 0이하이면 에러가 발생한다") + @ParameterizedTest + @ValueSource(ints = {-1, 0}) + void ticketCountGenerateErrorTest(int value) { + assertThatThrownBy(() -> TicketCount.of(value)) + .isInstanceOf(IllegalArgumentException.class); + } +} \ No newline at end of file diff --git a/src/test/java/domain/lotto/WinningLottoTicketTest.java b/src/test/java/domain/lotto/WinningLottoTicketTest.java new file mode 100644 index 0000000000..a9c24de768 --- /dev/null +++ b/src/test/java/domain/lotto/WinningLottoTicketTest.java @@ -0,0 +1,64 @@ +package domain.lotto; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThatCode; + +public class WinningLottoTicketTest { + + @DisplayName("WinningLotto 정상 생성 테스트.") + @Test + void winningLottoGenerateTest() { + //given + int[] lottoNumbers = {1, 2, 3, 4, 5, 6}; + int bonusNumber = 7; + + //when + Set winningNumbers = Arrays.stream(lottoNumbers) + .boxed() + .collect(Collectors.toSet()); + + //then + assertThatCode(() -> new WinningLotto(winningNumbers, bonusNumber)) + .doesNotThrowAnyException(); + } + + @DisplayName("winningLotto LottoBalls의 수와 BonusBall의 수거 중복일 시 에러가 발생한다.") + @Test + void winningLottoDuplicateErrorTest() { + //given + int[] lottoNumbers = {1, 2, 3, 4, 5, 6}; + int bonusNumber = 6; + + //when + Set winningNumbers = Arrays.stream(lottoNumbers) + .boxed() + .collect(Collectors.toSet()); + + //then + assertThatCode(() -> new WinningLotto(winningNumbers, bonusNumber)) + .isInstanceOf(IllegalArgumentException.class); + } + + @DisplayName("WinningBalls 갯수가 5개 이하이면 에러가 발생한다.") + @Test + void winningLottoBallSizeErrorTest() { + //given + int[] lottoNumbers = {1, 2, 3, 5, 5, 6}; + int bonusNumber = 6; + + //when + Set winningNumbers = Arrays.stream(lottoNumbers) + .boxed() + .collect(Collectors.toSet()); + + //then + assertThatCode(() -> new WinningLotto(winningNumbers, bonusNumber)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/domain/result/ResultTest.java b/src/test/java/domain/result/ResultTest.java new file mode 100644 index 0000000000..13a14c3da0 --- /dev/null +++ b/src/test/java/domain/result/ResultTest.java @@ -0,0 +1,133 @@ +package domain.result; + +import domain.ball.LottoBall; +import domain.ball.LottoBalls; +import domain.bettingMoney.BettingMoney; +import domain.lotto.LottoTicket; +import domain.lotto.LottoTickets; +import domain.lotto.WinningLotto; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.util.*; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +class ResultTest { + + private WinningLotto winningLotto; + + @BeforeEach + void initWinningLotto() { + int[] winningNumber = {1, 2, 3, 4, 5, 6}; + int bonusNumber = 7; + Set winningNumbers = Arrays.stream(winningNumber) + .boxed() + .collect(Collectors.toSet()); + winningLotto = new WinningLotto(winningNumbers, bonusNumber); + } + + @DisplayName("Result 정상 생성 테스트.") + @Test + void resultGenerateTest() { + //given + List lottoNumbers = Arrays.asList(1, 2, 3, 4, 5, 6); + + //when + List lottoBalls = lottoNumbers.stream() + .map(lottoNumber -> new LottoBall(lottoNumber)) + .collect(Collectors.toList()); + LottoTickets lottoTickets = new LottoTickets(Collections.singletonList(new LottoTicket(new LottoBalls(lottoBalls)))); + + //then + assertThatCode(() -> new Result(lottoTickets, winningLotto)) + .doesNotThrowAnyException(); + } + + @DisplayName("Result 결과 테스트.") + @Test + void resultFindRankTest() { + //given + List lottoNumbers = Arrays.asList(1, 2, 3, 4, 5, 6); + + //when + List lottoBalls = lottoNumbers.stream() + .map(lottoNumber -> new LottoBall(lottoNumber)) + .collect(Collectors.toList()); + LottoTickets lottoTickets = new LottoTickets(Collections.singletonList(new LottoTicket(new LottoBalls(lottoBalls)))); + Result result = new Result(lottoTickets, winningLotto); + + Map results = result.getResults(); + + //then + assertThat(results.get(LottoRank.SIX_MATCHES)).isEqualTo(1); + } + + @DisplayName("Result 복수 결과 반환 테스트.") + @Test + void resultManyRankTest() { + //given + List lottoNumbers = Arrays.asList(1, 2, 3, 4, 5, 6); + List lottoNumbers2 = Arrays.asList(1, 2, 3, 4, 5, 8); + + //when + List lottoBalls = lottoNumbers.stream() + .map(lottoNumber -> new LottoBall(lottoNumber)) + .collect(Collectors.toList()); + List lottoBalls2 = lottoNumbers2.stream() + .map(lottoNumber -> new LottoBall(lottoNumber)) + .collect(Collectors.toList()); + LottoTickets lottoTickets = new LottoTickets(Arrays.asList(new LottoTicket(new LottoBalls(lottoBalls)), new LottoTicket(new LottoBalls(lottoBalls2)))); + Result result = new Result(lottoTickets, winningLotto); + + Map results = result.getResults(); + + assertThat(results.get(LottoRank.SIX_MATCHES)).isEqualTo(1); + assertThat(results.get(LottoRank.FIVE_MATCHES)).isEqualTo(1); + } + + @DisplayName("5개의 볼, 보너스 볼이 맞을 때 2등 당첨된다.") + @Test + void resultSecondPrizeTest() { + //given + List lottoNumbers = Arrays.asList(1, 2, 3, 4, 5, 7); + + //when + List lottoBalls = lottoNumbers.stream() + .map(lottoNumber -> new LottoBall(lottoNumber)) + .collect(Collectors.toList()); + LottoTickets lottoTickets = new LottoTickets(Collections.singletonList(new LottoTicket(new LottoBalls(lottoBalls)))); + Result result = new Result(lottoTickets, winningLotto); + + Map results = result.getResults(); + + //then + assertThat(results.get(LottoRank.FIVE_AND_BONUS_MATCHES)).isEqualTo(1); + } + + @DisplayName("수익률 반환 테스트.") + @Test + void resultEarningsRateTest() { + //given + List lottoNumbers = Arrays.asList(1, 2, 3, 7, 8, 9); + List lottoNumbers2 = Arrays.asList(1, 2, 3, 7, 8, 9); + + //when + List lottoBalls = lottoNumbers.stream() + .map(lottoNumber -> new LottoBall(lottoNumber)) + .collect(Collectors.toList()); + List lottoBalls2 = lottoNumbers2.stream() + .map(lottoNumber -> new LottoBall(lottoNumber)) + .collect(Collectors.toList()); + LottoTickets lottoTickets = new LottoTickets(Arrays.asList(new LottoTicket(new LottoBalls(lottoBalls)), new LottoTicket(new LottoBalls(lottoBalls2)))); + Result result = new Result(lottoTickets, winningLotto); + + BigDecimal earningsRate = result.findEarningsRate(BettingMoney.of(2000)); + System.out.println("earningsRate.doubleValue() = " + earningsRate.doubleValue()); + assertThat(earningsRate).isEqualTo(BigDecimal.valueOf(5)); + } +} \ No newline at end of file diff --git a/src/test/java/empty.txt b/src/test/java/empty.txt deleted file mode 100644 index e69de29bb2..0000000000