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
**/*.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

View file

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

View file

@ -7,15 +7,20 @@ and [I make Silver Standard for 5th Edition (Spreadsheets.)](https://www.reddit.
## Usage
```
sterling [VALUE]...
USAGE:
sterling.exe [FLAGS] [OPTIONS] [VALUE]...
FLAGS:
-f, --full Print currencies with full name, rather than with alias.
-h, --help Prints help information
-V, --version Prints version information
OPTIONS:
-c, --config <CONFIG> Specify location of config file; defaults to './sterling-conf.yml'.
ARGS:
<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
@ -25,16 +30,52 @@ ARGS:
sterling 100p // 10g
// Convert one hundred platinum, fifty gold coins:
sterling 100p 50g // 10g 5s
sterling 100p 50g // 10g, 5s
// Convert fifteen thousand copper coins:
sterling 15000c // 1g 50s
// Convert fifteen thousand copper coins, printing the full names of the coins:
sterling -f 15000c // 1 gold, 50 silver
// Convert one platinum, thirty-six gold, twelve electrum, eighty-two silver, and four hundred
// sixty-nine copper coins
sterling 1p 36g 12e 82s 469c // 64s 89c
// sixty-nine copper coins, printing the full names of the coins
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
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
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
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.

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]
extern crate clap;
#[allow(unused_imports)]
#[macro_use]
extern crate lazy_static;
extern crate regex;
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_yaml;
use std::fmt;
use std::ops::Add;
mod config;
mod currency;
use std::io::ErrorKind;
use std::process;
use config::ConfigError;
use currency::Currency;
use regex::Regex;
fn main() {
lazy_static! {
static ref RE: Regex = Regex::new(r"(\d+)([cegps])?").unwrap();
}
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.")
(@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 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
.iter()
.map(|coin| {
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());
let converted_currencies = convert_currencies(total_copper_value, currencies);
let display_strings: Vec<String> =
create_display_strings(converted_currencies, matches.is_present("PRINT_FULL"));
convert_to_copper(amount, denomination)
} else {
panic!("Error: Invalid coin value \"{}\"", coin);
}
})
.fold(0 as usize, |total, value| total + value);
println!("{}", exchange_copper(total_copper_value));
println!("{}", (&display_strings).join(", "));
} 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);
}
}
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 {
match coin_denomination {
"p" => amount * 1000,
@ -56,65 +93,157 @@ fn convert_to_copper(amount: usize, coin_denomination: &str) -> usize {
}
}
fn exchange_copper(copper: usize) -> CurrencyValue {
CurrencyValue {
platinum: copper / 1000000,
gold: (copper % 1000000) / 10000,
silver: ((copper % 1000000) % 10000) / 100,
copper: ((copper % 1000000) % 10000) % 100,
}
}
#[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,
fn calculate_total_copper_value(coins: Vec<&str>) -> Result<usize, &'static str> {
let regex: Regex = Regex::new(r"(\d+)([cegps])").unwrap();
for coin in coins.iter() {
if let None = regex.captures(coin) {
return Err(
"Sterling Error: Invalid coin value. Make sure all coins are denoted properly."
)
}
}
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()));
}
}