Release v0.2.0

This commit is contained in:
Zachary Dziura 2018-03-16 02:22:24 +00:00
parent 01e8097fcc
commit 26aecf26ec
6 changed files with 409 additions and 93 deletions

7
.gitignore vendored
View file

@ -12,5 +12,12 @@ Cargo.lock
# These are backup files generated by rustfmt # These are backup files generated by rustfmt
**/*.rs.bk **/*.rs.bk
### VisualStudioCode ###
.vscode/
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history
# End of https://www.gitignore.io/api/rust # End of https://www.gitignore.io/api/rust

View file

@ -1,6 +1,6 @@
[package] [package]
name = "sterling" name = "sterling"
version = "0.1.0" version = "0.2.0"
description = "Converts a given D&D 5e currency value to the Silver Standard." description = "Converts a given D&D 5e currency value to the Silver Standard."
authors = ["Zachary Dziura <zcdziura@gmail.com>"] authors = ["Zachary Dziura <zcdziura@gmail.com>"]
readme = "README.md" readme = "README.md"
@ -12,6 +12,10 @@ keywords = ["dnd", "coins", "converter", "currency", "5e"]
clap = "2.31.1" clap = "2.31.1"
lazy_static = "1.0.0" lazy_static = "1.0.0"
regex = "0.2" regex = "0.2"
serde = "1.0"
serde_derive = "1.0"
serde_yaml = "0.7"
[profile.release] [profile.release]
lto = true lto = true
panic = "abort"

View file

@ -7,15 +7,20 @@ and [I make Silver Standard for 5th Edition (Spreadsheets.)](https://www.reddit.
## Usage ## Usage
``` ```
sterling [VALUE]... USAGE:
sterling.exe [FLAGS] [OPTIONS] [VALUE]...
FLAGS: FLAGS:
-f, --full Print currencies with full name, rather than with alias.
-h, --help Prints help information -h, --help Prints help information
-V, --version Prints version information -V, --version Prints version information
OPTIONS:
-c, --config <CONFIG> Specify location of config file; defaults to './sterling-conf.yml'.
ARGS: ARGS:
<VALUE>... The value to be converted; should be suffixed with the coin's short-hand <VALUE>... The value to be converted; should be suffixed with the coin's short-hand
abbreviation, i.e. p, g, e, s, or c. Defaults coin type to 'g'. abbreviation, i.e. p, g, e, s, or c.
``` ```
## Examples ## Examples
@ -25,16 +30,52 @@ ARGS:
sterling 100p // 10g sterling 100p // 10g
// Convert one hundred platinum, fifty gold coins: // Convert one hundred platinum, fifty gold coins:
sterling 100p 50g // 10g 5s sterling 100p 50g // 10g, 5s
// Convert fifteen thousand copper coins: // Convert fifteen thousand copper coins, printing the full names of the coins:
sterling 15000c // 1g 50s sterling -f 15000c // 1 gold, 50 silver
// Convert one platinum, thirty-six gold, twelve electrum, eighty-two silver, and four hundred // Convert one platinum, thirty-six gold, twelve electrum, eighty-two silver, and four hundred
// sixty-nine copper coins // sixty-nine copper coins, printing the full names of the coins
sterling 1p 36g 12e 82s 469c // 64s 89c sterling --full 1p 36g 12e 82s 469c // 64 silver, 89 copper
// Convert one platinum, thirty-six gold, twelve electrum, eighty-two silver, and four hundred
// sixty-nine copper coins, printing the full names of the coins, using the custom config file
// detailed below.
sterling --full -c "~/Documents/D&D/sterling-conf.yml" 1p 36g 12e 82s 469c // 27 sterling, 9 farthing
``` ```
## Custom Currencies
`sterling` allows for user-defined currencies, with their own names and conversion rates. By
default, `sterling` will look at a file within the current directory called `sterling-conf.yml`, or
in whatever location as supplied by the `-c` flag. Below is an example `sterling-conf.yml` file,
showing the actual currencies that I use within my own campaign!
```
-
name: "florin"
rate: 8640
alias: "F"
-
name: "sterling"
rate: 240
alias: "s"
-
name: "penny"
rate: 12
alias: "p"
plural: "pence"
-
name: "farthing"
rate: 1
alias: "f"
```
Please note that the `rate` value is defined as the number of copper coins that goes into one of
that particular currency. In the example above, twelve copper coins goes into one "penny", and
two-hundred forty copper coins goes into one "sterling".
## Abstract ## Abstract
Items and expenses are, by default, assigned arbitrary currency values within the official D&D 5th Items and expenses are, by default, assigned arbitrary currency values within the official D&D 5th
@ -46,7 +87,7 @@ campaign aught to treat gold similarly!
## Explanation ## Explanation
The basis of the Silver Standard treats 1 gold coin from the official D&D 5e source books as 1 The basis of the Silver Standard treats one gold coin from the official D&D 5e source books as one
silver coin, and that there are one hundred of a given coin to every one of the next highest valued silver coin, and that there are one hundred of a given coin to every one of the next highest valued
coin. That's all. Thus, one hundred fifty copper coins equals one silver and fifty copper coins, coin. That's all. Thus, one hundred fifty copper coins equals one silver and fifty copper coins,
while a suit of heavy plate armor equals fifteen gold coins, rather than fifteen hundred. while a suit of heavy plate armor equals fifteen gold coins, rather than fifteen hundred.

62
src/config.rs Normal file
View file

@ -0,0 +1,62 @@
use std::error::Error;
use std::fmt::{self, Display, Formatter};
use std::convert::From;
use std::fs::File;
use std::io::{self, BufReader, ErrorKind};
use serde_yaml;
use currency::Currency;
pub fn load_config(filename: &str) -> Result<Vec<Currency>, ConfigError> {
let config_file = File::open(filename)?;
let mut configs: Vec<Currency> = serde_yaml::from_reader(BufReader::new(config_file))?;
configs.sort_by(|a, b| b.cmp(a));
Ok(configs)
}
pub fn default_config() -> Vec<Currency> {
vec![
Currency::new("platinum", 1000000, "p", None),
Currency::new("gold", 10000, "g", None),
Currency::new("silver", 100, "s", None),
Currency::new("copper", 1, "c", None),
]
}
#[derive(Debug)]
pub struct ConfigError {
desc: String,
pub kind: ErrorKind,
}
impl Display for ConfigError {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "Sterling Error: {}", self.desc)
}
}
impl Error for ConfigError {
fn description(&self) -> &str {
&self.desc
}
}
impl From<io::Error> for ConfigError {
fn from(error: io::Error) -> Self {
ConfigError {
desc: error.description().to_owned(),
kind: error.kind(),
}
}
}
impl From<serde_yaml::Error> for ConfigError {
fn from(error: serde_yaml::Error) -> Self {
ConfigError {
desc: error.description().to_owned(),
kind: ErrorKind::Other,
}
}
}

73
src/currency.rs Normal file
View file

@ -0,0 +1,73 @@
use std::cmp::Ordering;
#[derive(Clone, Debug, Eq, Serialize, Deserialize)]
pub struct Currency {
pub name: String,
pub rate: usize,
pub value: Option<usize>,
pub alias: String,
pub plural: Option<String>,
}
impl Currency {
pub fn new(name: &str, rate: usize, alias: &str, plural: Option<String>) -> Currency {
Currency {
name: name.to_owned(),
rate,
value: None,
alias: alias.to_owned(),
plural,
}
}
pub fn with_value(&mut self, value: usize) -> Currency {
Currency {
name: self.name.clone(),
rate: self.rate,
value: Some(value),
alias: self.alias.clone(),
plural: self.plural.clone(),
}
}
pub fn alias_display(&self) -> String {
self.value.unwrap_or(0).to_string() + &self.alias
}
pub fn full_display(&self) -> String {
let mut display = self.value.unwrap_or(0).to_string() + " ";
if self.value.unwrap_or(0) > 1 {
match &self.plural {
&Some(ref plural) => display = display + &plural,
&None => display = display + &self.name,
}
} else {
display = display + &self.name;
}
display
}
}
// impl Display for Currency {
// fn
// }
impl Ord for Currency {
fn cmp(&self, other: &Currency) -> Ordering {
self.value.cmp(&other.value)
}
}
impl PartialOrd for Currency {
fn partial_cmp(&self, other: &Currency) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for Currency {
fn eq(&self, other: &Currency) -> bool {
self.value == other.value
}
}

View file

@ -1,50 +1,87 @@
#[macro_use] #[macro_use]
extern crate clap; extern crate clap;
#[allow(unused_imports)]
#[macro_use] #[macro_use]
extern crate lazy_static; extern crate lazy_static;
extern crate regex; extern crate regex;
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_yaml;
use std::fmt; mod config;
use std::ops::Add; mod currency;
use std::io::ErrorKind;
use std::process; use std::process;
use config::ConfigError;
use currency::Currency;
use regex::Regex; use regex::Regex;
fn main() { fn main() {
lazy_static! {
static ref RE: Regex = Regex::new(r"(\d+)([cegps])?").unwrap();
}
let app = clap_app!(sterling => let app = clap_app!(sterling =>
(version: "0.1.0") (version: "0.2.0")
(about: "Converts a given D&D 5e currency value to the Silver Standard.") (about: "Converts a given D&D 5e currency value to the Silver Standard.")
(@arg VALUE: ... "The value to be converted; should be suffixed with the coin's short-hand abbreviation, i.e. p, g, e, s, or c. Defaults coin type to 'g'.") (@arg CONFIG: -c --config +takes_value "Specify location of config file; defaults to './sterling-conf.yml'.")
(@arg PRINT_FULL: -f --full "Print currencies with full name, rather than with alias.")
(@arg VALUE: ... "The value to be converted; should be suffixed with the coin's short-hand abbreviation, i.e. p, g, e, s, or c.")
); );
if let Some(values) = app.get_matches().values_of("VALUE") { let matches = app.get_matches();
let config_result = config::load_config(match matches.value_of("CONFIG") {
Some(file) => file,
None => "./sterling-conf.yml",
});
let currencies = match parse_currency_config(config_result, matches.value_of("CONFIG")) {
Ok(currencies) => currencies,
Err(error) => {
eprintln!("{}", error);
process::exit(1);
}
};
if let Some(values) = matches.values_of("VALUE") {
let coins: Vec<&str> = values.collect(); let coins: Vec<&str> = values.collect();
let total_copper_value = match calculate_total_copper_value(coins) {
Ok(total_copper_value) => total_copper_value,
Err(err) => {
eprintln!("{}", err);
process::exit(1);
}
};
let total_copper_value: usize = coins let converted_currencies = convert_currencies(total_copper_value, currencies);
.iter() let display_strings: Vec<String> =
.map(|coin| { create_display_strings(converted_currencies, matches.is_present("PRINT_FULL"));
if let Some(captures) = RE.captures(coin) {
let amount: usize = captures.get(1).unwrap().as_str().parse().unwrap();
let denomination = captures.get(2).map_or("g", |d| d.as_str());
convert_to_copper(amount, denomination) println!("{}", (&display_strings).join(", "));
} else {
panic!("Error: Invalid coin value \"{}\"", coin);
}
})
.fold(0 as usize, |total, value| total + value);
println!("{}", exchange_copper(total_copper_value));
} else { } else {
println!("Please enter at least one value; should be suffixed with the coin's short-hand abbreviation, i.e. p, g, e, s, or c. Defaults coin type to 'g'."); eprintln!("Please enter at least one value; should be suffixed with the coin's short-hand abbreviation, i.e. p, g, e, s, or c.");
process::exit(1); process::exit(1);
} }
} }
fn parse_currency_config(
config_result: Result<Vec<Currency>, ConfigError>,
config_file_path: Option<&str>,
) -> Result<Vec<Currency>, String> {
match config_result {
Ok(values) => Ok(values),
Err(error) => match error.kind {
ErrorKind::NotFound => {
if let Some(file_path) = config_file_path {
Err(format!("Sterling Error: Can't find configuration file: \"{}\"", &file_path))
} else {
Ok(config::default_config())
}
},
_ => Err(format!("Sterling Error: {}", error)),
},
}
}
fn convert_to_copper(amount: usize, coin_denomination: &str) -> usize { fn convert_to_copper(amount: usize, coin_denomination: &str) -> usize {
match coin_denomination { match coin_denomination {
"p" => amount * 1000, "p" => amount * 1000,
@ -56,65 +93,157 @@ fn convert_to_copper(amount: usize, coin_denomination: &str) -> usize {
} }
} }
fn exchange_copper(copper: usize) -> CurrencyValue { fn calculate_total_copper_value(coins: Vec<&str>) -> Result<usize, &'static str> {
CurrencyValue { let regex: Regex = Regex::new(r"(\d+)([cegps])").unwrap();
platinum: copper / 1000000, for coin in coins.iter() {
gold: (copper % 1000000) / 10000, if let None = regex.captures(coin) {
silver: ((copper % 1000000) % 10000) / 100, return Err(
copper: ((copper % 1000000) % 10000) % 100, "Sterling Error: Invalid coin value. Make sure all coins are denoted properly."
} )
}
#[derive(Debug)]
struct CurrencyValue {
platinum: usize,
gold: usize,
silver: usize,
copper: usize,
}
impl fmt::Display for CurrencyValue {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let &CurrencyValue {
platinum,
gold,
silver,
copper,
} = self;
let mut output = String::new();
if platinum > 0 {
output = output + &format!("{}p ", platinum);
}
if gold > 0 {
output = output + &format!("{}g ", gold);
}
if silver > 0 {
output = output + &format!("{}s ", silver);
}
if copper > 0 {
output = output + &format!("{}c", copper);
} else if output.is_empty() {
output.push_str("0cp");
}
write!(f, "{}", output)
}
}
impl Add for CurrencyValue {
type Output = CurrencyValue;
fn add(self, other: CurrencyValue) -> CurrencyValue {
CurrencyValue {
platinum: self.platinum + other.platinum,
gold: self.gold + other.gold,
silver: self.silver + other.silver,
copper: self.copper + other.copper,
} }
} }
let converted_values = coins.iter().map(|coin| {
let captures = regex.captures(coin).unwrap();
let amount: usize = captures[1].parse().unwrap();
let denomination = captures[2].to_owned();
convert_to_copper(amount, &denomination)
});
Ok(converted_values.fold(0 as usize, |total, value| total + value))
}
fn exchange(copper: usize, mut currencies: Vec<Currency>) -> Vec<Currency> {
let mut val = copper;
currencies
.iter_mut()
.map(|currency| {
let value = val / currency.rate;
val = val % currency.rate;
currency.with_value(value)
})
.collect()
}
fn convert_currencies(copper_value: usize, currencies: Vec<Currency>) -> Vec<Currency> {
exchange(copper_value, currencies)
.iter()
.filter(|c| (*c).value.unwrap_or(0) > 0)
.cloned()
.collect()
}
fn create_display_strings(converted_currencies: Vec<Currency>, is_print_full: bool) -> Vec<String> {
converted_currencies
.iter()
.map(|c| {
if is_print_full {
c.full_display()
} else {
c.alias_display()
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use currency::Currency;
lazy_static! {
static ref STANDARD_CURRENCIES: [Currency; 4] = [
Currency::new("platinum", 1000000, "p", None),
Currency::new("gold", 10000, "g", None),
Currency::new("silver", 100, "s", None),
Currency::new("copper", 1, "c", None),
];
}
#[test]
fn test_convert_copper_to_copper() {
assert_eq!(1, convert_to_copper(1, "c"));
}
#[test]
fn test_convert_silver_to_copper() {
assert_eq!(10, convert_to_copper(1, "s"));
}
#[test]
fn test_convert_electrum_to_copper() {
assert_eq!(50, convert_to_copper(1, "e"));
}
#[test]
fn test_convert_gold_to_copper() {
assert_eq!(100, convert_to_copper(1, "g"));
}
#[test]
fn test_convert_platinum_to_copper() {
assert_eq!(1000, convert_to_copper(1, "p"));
}
#[test]
fn test_calculate_total_copper_value() {
let values = vec!["1p", "1g", "1e", "1s", "1c"];
assert_eq!(1161, calculate_total_copper_value(values).unwrap());
}
#[test]
#[should_panic]
fn test_calculate_total_copper_value_bad_inputs() {
let values = vec!["1p", "1g", "1f", "1s", "1c"];
assert_eq!(1161, calculate_total_copper_value(values).unwrap());
}
#[test]
fn test_exchange_to_copper() {
let currencies = vec![
Currency::new("platinum", 1000000, "p", None).with_value(0),
Currency::new("gold", 10000, "g", None).with_value(0),
Currency::new("silver", 100, "s", None).with_value(0),
Currency::new("copper", 1, "c", None).with_value(1),
];
assert_eq!(currencies, exchange(1, STANDARD_CURRENCIES.to_vec()));
}
#[test]
fn test_exchange_to_silver() {
let currencies = vec![
Currency::new("platinum", 1000000, "p", None).with_value(0),
Currency::new("gold", 10000, "g", None).with_value(0),
Currency::new("silver", 100, "s", None).with_value(1),
Currency::new("copper", 1, "c", None).with_value(0),
];
assert_eq!(currencies, exchange(100, STANDARD_CURRENCIES.to_vec()));
}
#[test]
fn test_exchange_to_gold() {
let currencies = vec![
Currency::new("platinum", 1000000, "p", None).with_value(0),
Currency::new("gold", 10000, "g", None).with_value(1),
Currency::new("silver", 100, "s", None).with_value(0),
Currency::new("copper", 1, "c", None).with_value(0),
];
assert_eq!(currencies, exchange(10000, STANDARD_CURRENCIES.to_vec()));
}
#[test]
fn test_exchange_to_platinum() {
let currencies = vec![
Currency::new("platinum", 1000000, "p", None).with_value(1),
Currency::new("gold", 10000, "g", None).with_value(0),
Currency::new("silver", 100, "s", None).with_value(0),
Currency::new("copper", 1, "c", None).with_value(0),
];
assert_eq!(currencies, exchange(1000000, STANDARD_CURRENCIES.to_vec()));
}
} }