Last active
July 17, 2017 18:06
-
-
Save mmstick/ebe63e232b7b49873db67f92ef8156f2 to your computer and use it in GitHub Desktop.
Rust implementation of an ATM-like console
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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