Skip to content

Commit

Permalink
[1단계 - 로또 구현] 케빈(박진홍) 미션 제출합니다. (#228)
Browse files Browse the repository at this point in the history
* refactor: application 수정

* feat: 요구사항 정리

* feat: 로또 티켓 구매 금액 생성 및 유효성 로직 기능 구현

* feat: 입력 금액에서 구매 가능한 로또 티켓 개수 계산 기능 구현

* feat: 로또 번호 생성 및 유효성 검증 로직 추가

* test: 로또 번호 정상 생성 테스트 추가

* feat: 로ë� 티켓은 중복되지 않은 6자리의 수로 구성되어야 한다

* feat: 로또 티켓 정상 발급 테스트 및 기능구현

* refactor: LottoTicket 검ìì�성 검증 로직 메서드 분리

* feat: 로또 번호 자동 생성하는 기능 구현

* feat: LottoTickets 생성 및 테스트 코드 추가

* feat: 당첨 번호 생성 및 보너스 볼 중복 검사 기능 구현

* refactor: WinningLottoTicket 클래스를 상속보다는 조합을 사용

* feat: 로또 순위 반환 기능 구현

* feat: 티켓 두장을 비교하여 맞은 개수를 반환하는 기능 구현

* refactor: contains 메서드 접근자를 퍼블릭으로 수정

* feat: LottoTickets에서 당첨 티켓을 비교하여 결과를 집계

* feat: 로또 통계는 순위별 당첨 티켓 개수를 반환한다.

* refactor: 로또 티켓이 일급 컬렉션 통계 객체를 반환하도록 함

* feat: 수익률 집계 기능 구현

* feat: view 구현

* refactor: 생성자 검증 로직 메서드 추출

* refactor: LottoRank 내 of 메소드 로직 분리

* refactor: LottoStatistics 상수 추가 및 메소드명 수정

* refactor: compare 메서드명을 직관적으로 수정

* feat: PurchasingPrice 구매가능 티켓 개수 반환 메서드명 변경

* refactor: OutputView 반복문 삭제

* style: test method given when then 패턴 형식에 맞게 수정

* style: 가독성 위해 줄바꿈 추가

* refactor: LottoNumber를 싱글톤으로 변경

* refactor: LottoTicket 메서드명 수정 및 람다 메서드 참조 사용

* refactor: WinningLottoTicket 유효성 검증 로직 분리

* refactor: LottoTickets 람다 스트림 사용 및 테스트를 위한 메서드 삭제

프로덕션에서 사용하지 않는 코드는 삭제한다.

* chore: 코드 컨벤션 포매팅

* refactor: getter 메서드명 변경

필드변수명 변경에 따라 getter 또한 변경한다

* refactor: 로또 번호 생성기 클래스 상수 분리

* refactor: 통계 결과 메서드 명을 getCountsByRank로 수정

등수에 따른 개수라는 의미를 직관적으로 살리기 위해 변경한다

* refactor: 수익률 계산 기능 메서드 분리 및 반복문 삭제

* refactor: LottoTickets 자동 발행 메서드명 변경

자동으로 티켓을 발권한다는 역할을 메서드명에 드러나도록 변경

* refactor: LottoRank 열거 상수 네이밍 변경

- 각 상수들의 접두로 PRIZE를 추가한다.
- 상금 상수의 가독성을 위해 언더바를 추가한다.

* refactor: 로또 당첨 통계 상금별 티켓 개수 반환 메서드명 변경

counts가 아닌 ticketCounts라고 명시한다.

* refactor: OutputView 반복문 삭제 및 메서드 명 상세 설명

- 각 메서드가 어떤 내용을 출력하는지를 명시해준다.
- 또한 반복문을 삭제하고 람다 스트림을 이용한다.

* refactor: enumMap factory 추가

* refactor: 객체 생성시 기존 값들을 새로운 컬렉션에 담기도록 변경

* refactor: 인터페이스 및 열거형 접근제어자 변경

- 열거형 생성자는 기본 default를 사용한다.
- 인터페이스 추상 메서드의 접근 제어자의 public은 생략한다.

* refactor: LottoStatistics 클래스명을 LottoResult로 변경

- getStatistics 등의 메서드 명 또한 checkResult로 직관되게 변경

* refactor: PurchasingPrice 객체는 티켓 가격을 모르도록 변경

- 구입가격 객체는 양의 정수만을 허용하도록 변경한다.

* refactor: 자동 발권 기능의 파라미터를 int가 아닌 PurchasingPrice 파라미터로 변경

* feat: LottoTickets 생성시 구매 금액이 부족한 경우 예외 발생

* feat: 로또 티켓 갯수는 LottoTickets에서 반환한다

* feat: LottoTickets가 자신을 구입하는데 든 비용을 반환하도록 함

* refactor: 수익률 집계 메서드 시그니쳐 변경

* refactor: OutputView에서 수익률 출력 방식 변경

도메인 객체로 메시지를 보내던 기존 방식을
외부에서 주입받은 수익률을 바로 출력하도록 변경한다.

* feat: 로또 결과 통계 Map 초기화 및 반환 기능 구현

* refactor: LottoTicket 내부 컬렉션을 list에서 set으로 변경

* refactor: 로또 티켓 번호 정렬 기능을 View로 변경

LottoTicket은 Set이기 때문에 정렬을 View단으로 넘긴다

* refactor: getMatchCounts 메서드명 동사를 check로 변경

* refacotr: 통계 Map 초기화 메서드명 변경

Co-authored-by: DESKTOP-VDP6SAT\xntm3 <ohjoohyung@github.com>
  • Loading branch information
xlffm3 and ohjoohyung authored Feb 21, 2021
1 parent e984845 commit 918f6f8
Show file tree
Hide file tree
Showing 21 changed files with 865 additions and 22 deletions.
23 changes: 23 additions & 0 deletions src/main/java/lotto/Application.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package lotto;

import lotto.domain.*;
import lotto.view.InputView;
import lotto.view.OutputView;

import java.util.List;

public class Application {
public static void main(String[] args) {
PurchasingPrice purchasingPrice = new PurchasingPrice(InputView.inputPurchasingPrice());
LottoTickets lottoTickets = LottoTickets.generateAutomatic(purchasingPrice, new RandomLottoNumberGenerator());
OutputView.printPurchasedLottoTicketCounts(lottoTickets.getTicketCounts());
OutputView.printAllLottoTicketNumbers(lottoTickets);

List<Integer> winningTicketNumbers = InputView.inputWinningTicketNumbers();
int bonusBallNumber = InputView.inputBonusBallNumber();
WinningLottoTicket winningLottoTicket = WinningLottoTicket.of(winningTicketNumbers, bonusBallNumber);
LottoResult lottoResult = lottoTickets.checkResult(winningLottoTicket);
double yield = lottoResult.calculateYield(lottoTickets.getPurchasingPrice());
OutputView.printLottoResult(lottoResult, yield);
}
}
22 changes: 0 additions & 22 deletions src/main/java/lotto/WebUILottoApplication.java

This file was deleted.

60 changes: 60 additions & 0 deletions src/main/java/lotto/docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
## 구현 기능 목록 정리

* 입력한 구입 금액은 최소 1000원 이상이어야 한다.
* 예외)
* 숫자가 아닌 문자열이 입력된 경우.
* 1000원 미만인 경우.
* 구입 금액으로 살 수 있는 로또 티켓 개수를 계산한다.
* 로또 번호는 1~45번 까지만 유효하다.
* 로또 티켓은 6개의 로또 번호로 구성되며, 중복이 있어서는 안 된다.
* 자동(랜덤)으로 로또 티켓을 만든다.
* 당첨 번호 또한 로또 티켓과 같은 규칙을 적용받는다.
* 보너스볼 번호는 로또 번호와 같은 규칙을 적용받으며 당첨 번호와 중복될 수 없다.
* 예외)
* 당첨 번호와 중복되는 경우.
* 1~45 범위의 숫자가 아닌 경우.
* 구매한 로또 티켓의 번호와 당첨 번호(+ 보너스볼)를 비교한다.
* 6개가 다 맞으면 1등(2000000000).
* 5개 맞고 보너스 볼 일치하면 2등(30000000).
* 5개 맞으면 3등(1500000).
* 4개 맞으면 4등(50000).
* 3개 맞으면 5등(5000).
* 나머지는 상금 없음.
* 맞은 개수에 따라서 로또 순위를 반환한다.
* 로또 당첨 통계를 확인한다.
* 각 등수별 당첨 티켓의 개수를 카운팅한다.
* 전체 구매 금액 대비 수익금액의 비율(수익률)을 계산한다.
* (추가)
* 로또 번호 싱글톤 적용
* (가능하다면) 반복문을 스트림으로 바꿔보자

## 프로그래밍 요구 사항

* indent(인덴트, 들여쓰기) depth를 2단계에서 1단계로 줄여라.
* depth의 경우 if문을 사용하는 경우 1단계의 depth가 증가한다. if문 안에 while문을 사용한다면 depth가 2단계가 된다.
* else를 사용하지 마라.
* 메소드의 크기가 최대 10라인을 넘지 않도록 구현한다.
* method가 한 가지 일만 하도록 최대한 작게 만들어라.
* 배열 대신 ArrayList를 사용한다.
* java enum을 적용해 프로그래밍을 구현한다.
* 규칙 3: 모든 원시값과 문자열을 포장한다.
* 규칙 5: 줄여쓰지 않는다(축약 금지).
* 규칙 8: 일급 콜렉션을 쓴다.

## Model

* PurchasingPrice
* LottoNumber
* LottoTicket
* 당첨번호 //추후 수정
* LottoTickets
* LottoResult(Statistics) //추후 수정

## View

* 구입 금액을 입력받는다.
* 당첨 번호를 입력받는다.
* 보너스 볼을 입력받는다.
* 구입한 로또 티켓 갯수를 출력한다.
* 구입한 로또 티켓 번호들을 출력한다.
* 당첨 통계를 출력한다.
32 changes: 32 additions & 0 deletions src/main/java/lotto/domain/LottoNumber.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package lotto.domain;

import java.util.HashMap;
import java.util.Map;

public class LottoNumber {
private static final Map<Integer, LottoNumber> CACHE = new HashMap<>();
private static final int MINIMUM_NUMBER = 1;
private static final int MAXIMUM_NUMBER = 45;
private static final String NUMBER_RANGE_ERROR = "로또 번호 범위가 벗어났습니다.";

private final int number;

private LottoNumber(int number) {
validateNumber(number);
this.number = number;
}

public static LottoNumber from(int number) {
return CACHE.computeIfAbsent(number, LottoNumber::new);
}

private void validateNumber(int lottoNumber) {
if (lottoNumber < MINIMUM_NUMBER || lottoNumber > MAXIMUM_NUMBER) {
throw new IllegalArgumentException(NUMBER_RANGE_ERROR);
}
}

public int getNumber() {
return number;
}
}
7 changes: 7 additions & 0 deletions src/main/java/lotto/domain/LottoNumberGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package lotto.domain;

import java.util.List;

public interface LottoNumberGenerator {
List<Integer> generate();
}
49 changes: 49 additions & 0 deletions src/main/java/lotto/domain/LottoRank.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package lotto.domain;

import java.util.Arrays;

public enum LottoRank {
FIRST_PRIZE(6, false, 2_000_000_000),
SECOND_PRIZE(5, true, 30_000_000),
THIRD_PRIZE(5, false, 1_500_000),
FOURTH_PRIZE(4, false, 50_000),
FIFTH_PRIZE(3, false, 5_000),
MISS(0, false, 0);

private final int matchCounts;
private final boolean isBonusBall;
private final int prizeMoney;

LottoRank(int matchCounts, boolean isBonusBall, int prizeMoney) {
this.matchCounts = matchCounts;
this.isBonusBall = isBonusBall;
this.prizeMoney = prizeMoney;
}

public static LottoRank of(int matchCounts, boolean isBonusBall) {
if (isSecondPrize(matchCounts, isBonusBall)) {
return SECOND_PRIZE;
}
return Arrays.stream(LottoRank.values())
.filter(lottoRank -> lottoRank != LottoRank.SECOND_PRIZE)
.filter(lottoRank -> lottoRank.matches(matchCounts))
.findFirst()
.orElseGet(() -> MISS);
}

private static boolean isSecondPrize(int matchCounts, boolean isBonusBall) {
return isBonusBall == SECOND_PRIZE.isBonusBall && matchCounts == SECOND_PRIZE.matchCounts;
}

private boolean matches(int matchCounts) {
return this.matchCounts == matchCounts;
}

public int getPrizeMoney() {
return prizeMoney;
}

public int getMatchCounts() {
return matchCounts;
}
}
41 changes: 41 additions & 0 deletions src/main/java/lotto/domain/LottoResult.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package lotto.domain;

import java.util.Arrays;
import java.util.Collections;
import java.util.EnumMap;
import java.util.Map;

public class LottoResult {
private static final long ZERO = 0;

private final Map<LottoRank, Long> statistics;

public LottoResult(Map<LottoRank, Long> statistics) {
initiateDefaultEntry(statistics);
this.statistics = new EnumMap<>(statistics);
}

private void initiateDefaultEntry(Map<LottoRank, Long> statistics) {
Arrays.stream(LottoRank.values())
.forEach(lottoRank -> statistics.computeIfAbsent(lottoRank, key -> ZERO));
}

public long getTicketCountsByRank(LottoRank lottoRank) {
return statistics.computeIfAbsent(lottoRank, key -> ZERO);
}

public double calculateYield(int purchasingPrice) {
return ((double) calculatePrizeMoneyTotal()) / purchasingPrice;
}

private long calculatePrizeMoneyTotal() {
return statistics.keySet()
.stream()
.mapToLong(lottoRank -> lottoRank.getPrizeMoney() * statistics.get(lottoRank))
.sum();
}

public Map<LottoRank, Long> getStatistics() {
return Collections.unmodifiableMap(statistics);
}
}
47 changes: 47 additions & 0 deletions src/main/java/lotto/domain/LottoTicket.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package lotto.domain;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

public class LottoTicket {
private static final int VALID_LOTTO_NUMBER_COUNTS = 6;
private static final String INVALID_LOTTO_NUMBER_COUNTS = "로또 티켓은 중복되지 않은 6자리의 숫자로 구성되어야 합니다.";

private final Set<LottoNumber> lottoNumbers;

private LottoTicket(Set<LottoNumber> lottoNumbers) {
validateNumberCounts(lottoNumbers.size());
this.lottoNumbers = lottoNumbers;
}

public static LottoTicket from(List<Integer> numbers) {
validateNumberCounts(numbers.size());
Set<LottoNumber> lottoNumbers = numbers.stream()
.map(LottoNumber::from)
.collect(Collectors.toSet());
return new LottoTicket(lottoNumbers);
}

private static void validateNumberCounts(int numberCounts) {
if (numberCounts != VALID_LOTTO_NUMBER_COUNTS) {
throw new IllegalArgumentException(INVALID_LOTTO_NUMBER_COUNTS);
}
}

public int checkMatchCounts(LottoTicket lottoTicket) {
return (int) lottoNumbers.stream()
.filter(lottoTicket::contains)
.count();
}

public boolean contains(LottoNumber lottoNumber) {
return lottoNumbers.contains(lottoNumber);
}

public List<LottoNumber> getLottoNumbers() {
return Collections.unmodifiableList(new ArrayList<>(lottoNumbers));
}
}
55 changes: 55 additions & 0 deletions src/main/java/lotto/domain/LottoTickets.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package lotto.domain;

import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class LottoTickets {
private static final int LOTTO_TICKET_PRICE = 1000;
private static final int ZERO = 0;
private static final String NOT_ENOUGH_PURCHASING_MONEY = "금액이 부족하여 로또 티켓을 구매할 수 없습니다.";

private final List<LottoTicket> lottoTickets;

private LottoTickets(List<LottoTicket> lottoTickets) {
this.lottoTickets = lottoTickets;
}

public static LottoTickets generateAutomatic(PurchasingPrice purchasingPrice, LottoNumberGenerator lottoNumberGenerator) {
int purchasableTicketCounts = purchasingPrice.calculatePurchasableTicketCounts(LOTTO_TICKET_PRICE);
validateTicketCounts(purchasableTicketCounts);
List<LottoTicket> lottoTickets = Stream.generate(() -> LottoTicket.from(lottoNumberGenerator.generate()))
.limit(purchasableTicketCounts)
.collect(Collectors.toList());
return new LottoTickets(lottoTickets);
}

private static void validateTicketCounts(int purchasableTicketCounts) {
if (purchasableTicketCounts == ZERO) {
throw new IllegalArgumentException(NOT_ENOUGH_PURCHASING_MONEY);
}
}

public LottoResult checkResult(WinningLottoTicket winningLottoTicket) {
Map<LottoRank, Long> statistics = lottoTickets.stream()
.map(winningLottoTicket::compareNumbers)
.collect(Collectors.groupingBy(lottoRank -> lottoRank, () -> new EnumMap<>(LottoRank.class),
Collectors.counting()));
return new LottoResult(statistics);
}

public int getPurchasingPrice() {
return lottoTickets.size() * LOTTO_TICKET_PRICE;
}

public int getTicketCounts() {
return lottoTickets.size();
}

public List<LottoTicket> getLottoTickets() {
return Collections.unmodifiableList(lottoTickets);
}
}
27 changes: 27 additions & 0 deletions src/main/java/lotto/domain/PurchasingPrice.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package lotto.domain;

public class PurchasingPrice {
private static final int ZERO = 0;
private static final String INVALID_PRICE = "로또 티켓 구매 금액은 양의 정수이어야 합니다.";

private final int price;

public PurchasingPrice(int price) {
validatePrice(price);
this.price = price;
}

private void validatePrice(int price) {
if (price <= ZERO) {
throw new IllegalArgumentException(INVALID_PRICE);
}
}

public int calculatePurchasableTicketCounts(int ticketCost) {
return price / ticketCost;
}

public int getPrice() {
return price;
}
}
22 changes: 22 additions & 0 deletions src/main/java/lotto/domain/RandomLottoNumberGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package lotto.domain;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class RandomLottoNumberGenerator implements LottoNumberGenerator {
private static final List<Integer> NUMBERS = IntStream.rangeClosed(1, 45)
.boxed()
.collect(Collectors.toList());
private static final int FROM_INDEX = 0;
private static final int TO_INDEX = 6;

@Override
public List<Integer> generate() {
Collections.shuffle(NUMBERS);
List<Integer> numbers = NUMBERS.subList(FROM_INDEX, TO_INDEX);
return new ArrayList<>(numbers);
}
}
Loading

0 comments on commit 918f6f8

Please sign in to comment.