Skip to content
Snippets Groups Projects
Commit 026de187 authored by Dao's avatar Dao
Browse files

feat(part2): Exercise 4.

parent e91b096d
No related branches found
No related tags found
No related merge requests found
### IntelliJ IDEA ###
/.idea/
out/
!**/src/main/**/out/
!**/src/test/**/out/
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
\ No newline at end of file
import java.util.Objects;
import java.util.Optional;
/*
* @.classInvariant: getId() != null && getBalanceInCents() >= 0
*/
public class Card {
private final CardId id;
private long balanceInCents = 0;
private TicketType lastPurchasedTicketType = null;
private long ticketExpiredAt = 0; // number of milliseconds passed since 1970
/*
* @.pre: true
* @.post: Object is constructed
* throw NullPointerException if id == null
*/
public Card(CardId id) {
this.id = Objects.requireNonNull(id, "Id should not be null");
}
/*
* @.pre: true
* @.post: RESULT != null
*/
public CardId getId() {
return id;
}
/*
* @.pre: true
* @.post: getBalanceInCents() = OLD(getBalanceInCents()) + amountInCents
* throw ZeroOrNegativeAmountException if amountInCents <= 0
*/
public void topUp(long amountInCents) throws ZeroOrNegativeAmountException {
if (amountInCents <= 0) {
throw new ZeroOrNegativeAmountException("Top up amount should be greater than zero");
}
balanceInCents += amountInCents;
}
/*
* @.pre: true
* @.post: RESULT >= 0
*/
public long getBalanceInCents() {
return balanceInCents;
}
/*
* @.pre: true
* @.post: RESULT == true if a ticket is purchased and is not expired, otherwise RESULT == false
*/
public boolean isTicketValid() {
return isTicketPurchased() && !isTicketExpired();
}
/*
* @.pre: true
* @.post: RESULT == null if there is no ticket
*/
public Optional<TicketType> getLastPurchasedTicketType() {
return Optional.ofNullable(lastPurchasedTicketType);
}
/*
* @.pre: ticketType != null
* @.post: isTicketValid() == true && getBalanceInCents() == OLD(getBalanceInCents()) - ticketType.priceInCents()
* throw NotEnoughMoneyForTicketException if getBalanceInCents() < ticketType.priceInCents()
*/
public void buyTicket(TicketType ticketType) throws NotEnoughMoneyForTicketException {
if (balanceInCents < ticketType.priceInCents()) {
throw new NotEnoughMoneyForTicketException("Your balance is not sufficient for the ticket");
}
balanceInCents -= ticketType.priceInCents();
lastPurchasedTicketType = ticketType;
ticketExpiredAt = getTimestamp() + ticketType.durationInMilliseconds();
}
@Override
public String toString() {
return id.toString();
}
private boolean isTicketPurchased() {
return null != lastPurchasedTicketType;
}
private boolean isTicketExpired() {
return getTimestamp() > ticketExpiredAt;
}
private long getTimestamp() {
return System.currentTimeMillis();
}
}
class ZeroOrNegativeAmountException extends RuntimeException {
public ZeroOrNegativeAmountException(String message) {
super(message);
}
}
class NotEnoughMoneyForTicketException extends RuntimeException {
public NotEnoughMoneyForTicketException(String message) {
super(message);
}
}
\ No newline at end of file
import java.util.Objects;
/*
* @.classInvariant: id() != null
*/
public record CardId(Object id) {
/*
* @.pre: true
* @.post: Object is constructed
* throw NullPointerException if id == null
*/
public CardId {
Objects.requireNonNull(id, "Id should not be null");
}
@Override
public String toString() {
return id.toString();
}
}
import java.util.*;
import java.util.function.Predicate;
public class CommandLineApp {
private final Card card;
private final CommandLineHelper helper;
private final TicketTypeRepository ticketTypeRepository;
private final MoneyHelper moneyHelper;
public CommandLineApp() {
card = createCard();
helper = new CommandLineHelper();
ticketTypeRepository = new TicketTypeRepository();
moneyHelper = new MoneyHelper();
}
public void run() {
int choice = getMenuChoice();
helper.clearScreen();
switch (choice) {
case 1:
showBalance();
break;
case 2:
topUp();
break;
case 3:
checkTicketValidity();
break;
}
}
private int getMenuChoice() {
List<String> menu = new ArrayList<>();
menu.add("Your card ID: " + card.toString());
menu.add(" ");
menu.add("1. Show balance");
menu.add("2. Top-up");
menu.add("3. Buy ticket");
menu.add("4. Exit");
helper.printMenu(menu);
return helper.getIntInput(
"Please choose",
"Error: please select a number between 1 and 4",
(number) -> number >= 1 && number <= 4
);
}
private void showBalance() {
helper.printSuccessMenu("Your balance is: " + moneyHelper.formatCents(card.getBalanceInCents()));
System.out.println();
run();
}
private void topUp() {
boolean isSucceed = false;
while (true) {
try {
long euro = helper.getLongInput("Please enter amount in Euro", (number) -> number >= 0);
long cents = helper.getLongInput("Please enter amount in cents", (number) -> number >= 0);
long amount = moneyHelper.euroToCent(euro) + cents;
card.topUp(amount);
isSucceed = true;
break;
} catch (ZeroOrNegativeAmountException e) {
helper.printErrorMessage("Error: " + e.getMessage());
String isContinue = helper.getStringInput("Do you want to continue? (y/n)");
if (isContinue.equals("n")) {
break;
}
}
}
if (isSucceed) {
helper.printSuccessMessage("Top-up successfully");
showBalance();
return;
}
System.out.println();
run();
}
private void checkTicketValidity() {
boolean isBuyNewTicket = !card.isTicketValid();
if (!isBuyNewTicket) {
helper.printSuccessMessage("Your ticket is still valid. Current ticket type:");
helper.printSuccessMessage(formatTicketType(card.getLastPurchasedTicketType().get()));
String isContinue = helper.getStringInput("Do you want to buy another? (y/n)");
isBuyNewTicket = isContinue.equals("y");
}
if (isBuyNewTicket) {
System.out.println();
buyTicket();
} else {
System.out.println();
run();
}
}
private void buyTicket() {
Optional<TicketType> optChoseType = getChoseTicketType();
if (optChoseType.isPresent()) {
try {
card.buyTicket(optChoseType.get());
helper.printSuccessMessage("Buy ticket successfully");
} catch (NotEnoughMoneyForTicketException e) {
helper.printErrorMessage("Error: " + e.getMessage());
}
}
System.out.println();
run();
}
private Optional<TicketType> getChoseTicketType() {
TicketType choseType = null;
List<String> menu = new ArrayList<>();
menu.add("Ticket types: ");
menu.add(" ");
for (TicketType type: ticketTypeRepository.getAllTypes()) {
menu.add(formatTicketType(type));
}
while (true) {
try {
helper.printMenu(menu);
String typeName = helper.getStringInput("Please enter the ticket type name or zero to cancel");
if (typeName.equals("0")) {
break;
}
choseType = ticketTypeRepository.getByName(typeName);
break;
} catch (TicketTypeNotFoundException e) {
helper.printErrorMessage("Error: " + e.getMessage());
String isContinue = helper.getStringInput("Do you want to continue? (y/n)");
if (isContinue.equals("n")) {
break;
}
}
}
return Optional.ofNullable(choseType);
}
private String formatTicketType(TicketType type) {
return "Name: '" + type.name()
+ "'. Description: " + type.description()
+ ". Price: " + moneyHelper.formatCents(type.priceInCents()) + ".";
}
private Card createCard() {
UUID uuid = UUID.randomUUID();
CardId cardId = new CardId(uuid);
return new Card(cardId);
}
}
class CommandLineHelper {
private final String ANSI_RESET = "\u001B[0m";
private final String ANSI_GREEN = "\u001B[32m";
private final String ANSI_RED = "\u001B[31m";
private final Scanner reader;
public CommandLineHelper() {
reader = new Scanner(System.in);
}
public void printMenu(List<String> strings) {
this.printMenu(strings, '*');
}
public void printMenu(List<String> strings, char character) {
int maxLen = strings.stream()
.mapToInt(String::length)
.max()
.orElse(0);
System.out.println(repeatCharacter(character, maxLen + 4));
for (String str: strings) {
System.out.println(
character + " " + str
+ repeatCharacter(' ', maxLen - str.length())
+ " " + character
);
}
System.out.println(repeatCharacter(character, maxLen + 4));
}
public void printMenu(List<String> strings, char character, String color) {
System.out.print(color);
printMenu(strings, character);
System.out.print(ANSI_RESET);
}
public void printMenu(List<String> strings, String color) {
this.printMenu(strings, '*', color);
}
public void printMenu(String string, String color) {
List<String> messages = new ArrayList<>();
messages.add(string);
printMenu(messages, color);
}
public void printSuccessMenu(String string) {
printMenu(string, ANSI_GREEN);
}
public void printSuccessMessage(String string) {
System.out.println(ANSI_GREEN + string + ANSI_RESET);
}
public void printErrorMessage(String string) {
System.out.println(ANSI_RED + string + ANSI_RESET);
}
public String repeatCharacter(char character, int number) {
if (number <= 0) {
return "";
}
char[] repeat = new char[number];
Arrays.fill(repeat, character);
return new String(repeat);
}
public void clearScreen() {
System.out.print("\033[H\033[2J");
System.out.flush();
}
public String getStringInput(String message) {
System.out.print(message + ": ");
return reader.nextLine();
}
public int getIntInput(String message, String errorMessage, Predicate<Integer> tester) {
int input;
while (true) {
try {
System.out.print(message + ": ");
input = Integer.parseInt(reader.nextLine());
if (null == tester || tester.test(input)) {
break;
}
printErrorMessage(errorMessage);
} catch (NumberFormatException e) {
printErrorMessage(errorMessage);
}
}
return input;
}
public long getLongInput(String message, Predicate<Long> tester) {
return getLongInput(message, "Error: please enter a number", tester);
}
public long getLongInput(String message, String errorMessage, Predicate<Long> tester) {
long input;
while (true) {
try {
System.out.print(message + ": ");
input = Long.parseLong(reader.nextLine());
if (null == tester || tester.test(input)) {
break;
}
printErrorMessage(errorMessage);
} catch (NumberFormatException e) {
printErrorMessage(errorMessage);
}
}
return input;
}
}
\ No newline at end of file
public class Main {
public static void main(String[] args) {
CommandLineApp app = new CommandLineApp();
app.run();
}
}
\ No newline at end of file
import java.text.NumberFormat;
import java.util.Locale;
public class MoneyHelper {
public long euroToCent(long euro) {
return euro * 100;
}
public String formatCents(long cents) {
NumberFormat nf = NumberFormat.getCurrencyInstance(Locale.FRANCE);
return nf.format(cents / 100.0);
}
}
import java.util.Objects;
/*
* @.classInvariant: name() != null && durationInMilliseconds() > 0 && priceInCents() >= 0
*/
public record TicketType(String name, String description, long durationInMilliseconds, long priceInCents) {
/*
* @.pre: true
* @.post: Object is constructed
* throw NullPointerException if name == null
* throw ZeroOrNegativeDurationException if durationInMilliseconds <= 0
* throw NegativePriceException if priceInCents < 0
*/
public TicketType {
Objects.requireNonNull(name, "Id should not be null");
if (durationInMilliseconds <= 0) {
throw new ZeroOrNegativeDurationException("Duration should be greater than zero");
}
if (priceInCents < 0) {
throw new NegativePriceException("Price should be greater than or equal to zero");
}
}
}
class ZeroOrNegativeDurationException extends RuntimeException {
public ZeroOrNegativeDurationException(String message) {
super(message);
}
}
class NegativePriceException extends RuntimeException {
public NegativePriceException(String message) {
super(message);
}
}
\ No newline at end of file
import java.util.ArrayList;
import java.util.List;
public class TicketTypeRepository {
private final List<TicketType> types;
public TicketTypeRepository() {
types = new ArrayList<>();
types.add(new TicketType(
"single",
"Single ticket. Valid for 2 hours",
(long) 2 * 60 * 60 * 1000,
3 * 100
));
types.add(new TicketType(
"day",
"Day ticket. Valid for 24 hours",
(long) 24 * 60 * 60 * 1000,
8 * 100
));
types.add(new TicketType(
"monthly",
"Monthly ticket. Valid for 30 days",
(long) 30 * 24 * 60 * 60 * 1000,
55 * 100
));
}
public List<TicketType> getAllTypes() {
return types;
}
/*
* @.pre: name != null
* @.post: RESULT != null
* throw TicketTypeNotFoundException if there is no ticket type with this name.
*/
public TicketType getByName(String name) {
List<TicketType> result = types.stream()
.filter((type) -> type.name().equals(name))
.limit(2)
.toList();
if (result.isEmpty()) {
throw new TicketTypeNotFoundException("Ticket type '" + name + "' is not found");
}
return result.getFirst();
}
}
class TicketTypeNotFoundException extends RuntimeException {
public TicketTypeNotFoundException(String message) {
super(message);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment