Skip to content

Instantly share code, notes, and snippets.

@mmstick
Last active July 17, 2017 18:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mmstick/ebe63e232b7b49873db67f92ef8156f2 to your computer and use it in GitHub Desktop.
Save mmstick/ebe63e232b7b49873db67f92ef8156f2 to your computer and use it in GitHub Desktop.
Rust implementation of an ATM-like console
use std::io::{stdin, stdout, BufRead, Error, Write};
use std::fmt::{self, Display};
fn main() {
// Create the account that we will emulate
let mut account = Account::new();
// Print the first input message to the screen
print!("Enter your account name: ");
let stdout = stdout();
let _ = stdout.lock().flush();
// Obtain the name of the account
let stdin = stdin();
match stdin.lock().read_line(&mut account.name) {
// Remove the newline read at the end
Ok(_) => { account.name.pop().unwrap(); },
// Handle the error
Err(why) => {
eprintln!("Error getting account name: {}", why);
return
}
}
// Create a string that will be used as a buffer for all future inputs.
let mut answer = String::new();
loop {
// Submit a request for a bank action
match BankAction::request(&mut answer, "Depositing or withdrawing? (1 = Deposit; 2 = Withdraw; 3 = Exit): ") {
// Ask for a deposit amount and deposit that amount into the account, if the amount is valid.
Ok(BankAction::Deposit) => {
match Currency::request(&mut answer, "Enter amount to deposit: ") {
Ok(amount) => account.deposit(amount),
Err(why) => eprintln!("{}", why)
}
},
// Ask for a withdrawal amount and attempt to withdraw that amount from the account, if it's available.
Ok(BankAction::Withdraw) => {
match Currency::request(&mut answer, "Enter amount to withdraw: ") {
Ok(amount) => account.withdraw(amount),
Err(why) => eprintln!("{}", why)
};
},
// Exit the program
Ok(BankAction::Exit) => break,
// An error occurred when reading the bank action.
Err(why) => eprintln!("Error occurred: {}", why)
}
}
}
struct Account {
name: String,
balance: Currency
}
impl Account {
fn new() -> Account {
Account {
name: String::new(),
balance: Currency(0)
}
}
/// Deposit the amount into the balance
fn deposit(&mut self, amount: Currency) {
self.balance.0 += amount.0;
println!("Deposited {} into account. (Balance: {})", amount, self.balance);
}
/// Withdraw the amount from the balance, if possible
fn withdraw(&mut self, amount: Currency) {
// Perform a checked subtraction to determine if the amount in the balance is available.
match self.balance.0.checked_sub(amount.0) {
// If it's available, this will contain the new balance in the account
Some(new_balance) => {
self.balance.0 = new_balance;
println!("Withdrew {} from account. (Balance: {})", amount, self.balance);
},
// Print an error that the amount is not available.
None => eprintln!("error: funds not available in account. (Balance: {})", self.balance)
}
}
}
/// Obtained by performing a `BankAction::request()`, this variant denotes the
/// action to be performed by the ATM.
enum BankAction {
Deposit,
Withdraw,
Exit
}
/// A possible error that may be returned by a `BankAction::request()`.
enum BankError {
IO(Error),
InvalidString(String),
InvalidAction(usize)
}
/// Currencies are represented as real, whole numbers, not fractions.
/// Obtained by performing a `BankAction::request()`.
#[derive(Clone, Copy)]
struct Currency(usize);
/// A possible error that may be returned by a `BankAction::request()`.
enum CurrencyError {
IO(Error),
Invalid(String)
}
/// An ATM may ask for a variety of requests. This trait defines the method that
/// a type should implement, if it wants to perform a request.
trait ATMRequest {
type Success;
type Error;
/// A request should print a msg dialgoue, read a value into the provided buffer,
/// and then attempt to parse that buffer, and clearing it before returning.
fn request(&mut String, msg: &str) -> Result<Self::Success, Self::Error>;
}
impl ATMRequest for BankAction {
type Success = BankAction;
type Error = BankError;
fn request(buffer: &mut String, msg: &str) -> Result<BankAction, BankError> {
use BankError::*;
// Print the supplied dialogue.
print!("{}", msg);
let stdout = stdout();
let _ = stdout.lock().flush();
// Clear the buffer first
buffer.clear();
// Read the value from standard input.
let stdin = stdin();
stdin.lock().read_line(buffer)?;
// Remove the newline read at the end.
buffer.pop().unwrap();
/// Match the parsed input to it's corresponding action.
buffer.parse::<u8>().map_err(|_| InvalidString(buffer.clone()))
.and_then(|action| match action {
1 => Ok(BankAction::Deposit),
2 => Ok(BankAction::Withdraw),
3 => Ok(BankAction::Exit),
answer => Err(InvalidAction(answer as usize))
})
}
}
impl ATMRequest for Currency {
type Success = Currency;
type Error = CurrencyError;
fn request(buffer: &mut String, msg: &str) -> Result<Currency, CurrencyError> {
use CurrencyError::*;
// Print the supplied dialogue.
print!("{}", msg);
let stdout = stdout();
let _ = stdout.lock().flush();
// Clear the buffer first
buffer.clear();
// Read the value from standard input.
let stdin = stdin();
stdin.lock().read_line(buffer)?;
// Remove the newline read at the end.
buffer.pop().unwrap();
// Match the buffer to it's corresponding Currency result. Because currencies may contain
// decimal places, special handling needs to happen in order to parse the value.
match buffer.find('.') {
Some(position) => {
// Split the buffer at the position where the decimal point was found.
let (major, mut minor_str) = buffer.split_at(position);
// Remove the decimal point from the minor string.
minor_str = &minor_str[1..];
// Parse the major value, mapping the error value accordingly
major.parse::<usize>().map_err(|_| Invalid(buffer.clone()))
// If the parse succeeded, we will next parse the minor string
.and_then(|major| minor_str.parse::<usize>().map_err(|_| Invalid(buffer.clone())).and_then(|minor| {
// If there's only one character, it's because that number is a tenth.
let minor = if minor_str.len() == 1 { minor * 10 } else { minor };
// Don't allow anyone to cheat the machine.
if minor > 99 {
Err(Invalid(buffer.clone()))
} else {
// A major value is 100 times the value of a minor, so add them up accordingly.
Ok(Currency(minor + (major * 100)))
}
}).map_err(|_| Invalid(buffer.clone())))
},
None => buffer.parse::<usize>().map(|v| Currency(v * 100))
.map_err(|_| Invalid(buffer.clone()))
}
}
}
impl From<Error> for BankError {
fn from(error: Error) -> BankError { BankError::IO(error) }
}
impl From<Error> for CurrencyError {
fn from(e: Error) -> CurrencyError { CurrencyError::IO(e) }
}
impl Display for BankError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
BankError::IO(ref err) => write!(f, "error: standard input error: {}", err),
BankError::InvalidString(ref string) => write!(f, "error: invalid input supplied: {}", string),
BankError::InvalidAction(ref action) => write!(f, "error: no operation is mapped to {}", action)
}
}
}
impl Display for Currency {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "${}.{:02}", self.0 / 100, self.0 % 100)
}
}
impl Display for CurrencyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
CurrencyError::IO(ref error) => write!(f, "error: standard input error: {}", error),
CurrencyError::Invalid(ref string) => write!(f, "error: {} is not a valid amount", string),
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment