Release v0.2.0
This commit is contained in:
parent
01e8097fcc
commit
26aecf26ec
6 changed files with 409 additions and 93 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -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
|
||||
|
|
|
@ -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
|
||||
lto = true
|
||||
panic = "abort"
|
57
README.md
57
README.md
|
@ -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
62
src/config.rs
Normal 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
73
src/currency.rs
Normal 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
|
||||
}
|
||||
}
|
295
src/main.rs
295
src/main.rs
|
@ -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());
|
||||
|
||||
convert_to_copper(amount, denomination)
|
||||
} else {
|
||||
panic!("Error: Invalid coin value \"{}\"", coin);
|
||||
}
|
||||
})
|
||||
.fold(0 as usize, |total, value| total + value);
|
||||
let converted_currencies = convert_currencies(total_copper_value, currencies);
|
||||
let display_strings: Vec<String> =
|
||||
create_display_strings(converted_currencies, matches.is_present("PRINT_FULL"));
|
||||
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue