Release/0.3.0
This commit is contained in:
parent
26aecf26ec
commit
abfb69e82d
12 changed files with 410 additions and 191 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -21,3 +21,5 @@ Cargo.lock
|
||||||
.history
|
.history
|
||||||
|
|
||||||
# End of https://www.gitignore.io/api/rust
|
# End of https://www.gitignore.io/api/rust
|
||||||
|
|
||||||
|
sterling-conf.yml
|
||||||
|
|
24
.gitlab-ci.yml
Normal file
24
.gitlab-ci.yml
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
image: "rust:latest"
|
||||||
|
stages:
|
||||||
|
- test
|
||||||
|
- deploy
|
||||||
|
|
||||||
|
cargo:test:
|
||||||
|
stage: test
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
script:
|
||||||
|
- cargo test --verbose
|
||||||
|
|
||||||
|
cargo:build:
|
||||||
|
stage: deploy
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
script:
|
||||||
|
- cargo build --release
|
||||||
|
cache:
|
||||||
|
paths:
|
||||||
|
- target/
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- target/release/sterling
|
27
.travis.yml
Normal file
27
.travis.yml
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
language: rust
|
||||||
|
os:
|
||||||
|
- osx
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
rust:
|
||||||
|
- stable
|
||||||
|
script:
|
||||||
|
- cargo test --verbose
|
||||||
|
- cargo build --release
|
||||||
|
before_deploy:
|
||||||
|
- git config --local user.name "Zachary Dziura"
|
||||||
|
- git config --local user.email "zcdziura@gmail.com"
|
||||||
|
- git tag "$(date +'%Y%m%d%H%M%S')-$(git log --format=%h -1)-osx"
|
||||||
|
deploy:
|
||||||
|
provider: releases
|
||||||
|
api_key:
|
||||||
|
secure: "NVH5d3CH0QUyFSu0MbeB3WvSo52qwjxH98wIL7kieD/kbTrTzTCbTWiTCV+/OM41nWOdxTy9T5TLqDsrh4k4xCkb4VbKTcsfIpBaQqwMvT6Co/GgLr4xSiHBI/ENBVwbnyDavMxh/E5AAAPF/HgGci2tEqzNuu9V7jon6uhb8+WovbfZeEA4tSNLsWV5g3MwssMfdaWzDPTHsiWXFPn6AVhkmy4fKAIHoUtp37A7bqx1hGPpFD3OGYN1oDxtJK5jRBSXegyWh08RQkLQ74PJTWD6Xw+Hvp1ewP1vitP69VJgsBC496jPasqAEOVeD3KogtcmBEyaIG+I5LZWLTibs41qF83cxJDdWxw69H827IXSQobM+7Sc51chWJR0H3OA1yDPQvorI1C17zvXd4wPpDfSUeY5ZqAplnYMOxk3jDbbX099bEyRE/skWHRaqL99fV7i5bO3aHDFP/BDjp03hnzpvfKs9zm05e87LStriNYQ5NsCPkdX+W18Q15DLhS2D9cp37PPAUA5jLNUFiEY5x9fwl5XEpefBqrqmE8qbmkc9GTr3MZikmTfB51Nx5NvkybCTKhMoKw5AhNLmw0fnkaqxrei7Uif7WqxTkngJep6VLidmt2pRJ9Qj3AWOXsLZJPm0ZQuo71dWC049EeEVtfQkyz/9K2J+iNVRgdiEeg="
|
||||||
|
file_glob: true
|
||||||
|
file: target/release/sterling
|
||||||
|
skip_cleanup: true
|
||||||
|
on:
|
||||||
|
repo: zcdziura/sterling
|
||||||
|
branch: master
|
||||||
|
addons:
|
||||||
|
artifacts: true
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "sterling"
|
name = "sterling"
|
||||||
version = "0.2.0"
|
version = "0.3.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"
|
||||||
|
@ -9,9 +9,9 @@ repository = "https://gitlab.com/zcdziura/sterling"
|
||||||
keywords = ["dnd", "coins", "converter", "currency", "5e"]
|
keywords = ["dnd", "coins", "converter", "currency", "5e"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = "2.31.1"
|
clap = "2.31"
|
||||||
lazy_static = "1.0.0"
|
lazy_static = "1.0"
|
||||||
regex = "0.2"
|
regex = "1.0"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
serde_yaml = "0.7"
|
serde_yaml = "0.7"
|
||||||
|
|
64
README.md
64
README.md
|
@ -8,9 +8,10 @@ and [I make Silver Standard for 5th Edition (Spreadsheets.)](https://www.reddit.
|
||||||
|
|
||||||
```
|
```
|
||||||
USAGE:
|
USAGE:
|
||||||
sterling.exe [FLAGS] [OPTIONS] [VALUE]...
|
sterling [FLAGS] [OPTIONS] [VALUE]... [SUBCOMMAND]
|
||||||
|
|
||||||
FLAGS:
|
FLAGS:
|
||||||
|
-o, --optional Include currencies marked as optional when converting.
|
||||||
-f, --full Print currencies with full name, rather than with alias.
|
-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
|
||||||
|
@ -19,11 +20,16 @@ OPTIONS:
|
||||||
-c, --config <CONFIG> Specify location of config file; defaults to './sterling-conf.yml'.
|
-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,
|
||||||
abbreviation, i.e. p, g, e, s, or c.
|
e, s, or c.
|
||||||
|
|
||||||
|
SUBCOMMANDS:
|
||||||
|
add Add two currency amounts together; uses the currencies defined in your config file
|
||||||
|
help Prints this message or the help of the given subcommand(s)
|
||||||
|
sub Subtract two currency amounts from one another; uses the currencies defined in your config file
|
||||||
```
|
```
|
||||||
|
|
||||||
## Examples
|
## Converting Currency Examples
|
||||||
|
|
||||||
```
|
```
|
||||||
// Convert one hundred platinum coins:
|
// Convert one hundred platinum coins:
|
||||||
|
@ -41,22 +47,43 @@ 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
|
// 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
|
// sixty-nine copper coins, printing the full names of the coins, using the custom config file
|
||||||
// detailed below.
|
// detailed below, including optional currencies
|
||||||
sterling --full -c "~/Documents/D&D/sterling-conf.yml" 1p 36g 12e 82s 469c // 27 sterling, 9 farthing
|
sterling --full -o -c "~/Documents/D&D/sterling-conf.yml" 1p 36g 12e 82s 469c // 27 sterling, 9 farthing
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Adding and Subtracting Currency Examples
|
||||||
|
|
||||||
|
```
|
||||||
|
// Add together ten and twenty pense, using the custom config file detailed below
|
||||||
|
sterling add "10p" "20p" // 1s, 10p
|
||||||
|
|
||||||
|
// Subtract two sterling and ten pence from one florin
|
||||||
|
sterling sub "1F" "2s 10p" --full // 33 sterling, 10 pence
|
||||||
|
|
||||||
|
// Subtract one florin from two sterling and ten pence. Note that, regardless of order, the smaller
|
||||||
|
// value is ALWAYS subtracted from the larger value.
|
||||||
|
sterling sub "2s 10p" "1F" --full // 33 sterling, 10 pence
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that `sterling` doesn't allow for negative currencies. Therefore, when subtracting currencies,
|
||||||
|
the smaller currency value will always be subtracted from the larger currency value, regardless of
|
||||||
|
the order of the currencies in the `sub` command.
|
||||||
|
|
||||||
## Custom Currencies
|
## Custom Currencies
|
||||||
|
|
||||||
`sterling` allows for user-defined currencies, with their own names and conversion rates. By
|
`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
|
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,
|
in whatever location as supplied by the `-c` flag. You can also specify that a currency be optional,
|
||||||
showing the actual currencies that I use within my own campaign!
|
which will prevent that currency from being used when converting values, unless the `-o` flag is
|
||||||
|
passed. Below is an example `sterling-conf.yml` file, showing the actual currencies that I use
|
||||||
|
within my own campaign!
|
||||||
|
|
||||||
```
|
```
|
||||||
-
|
-
|
||||||
name: "florin"
|
name: "florin"
|
||||||
rate: 8640
|
rate: 8640
|
||||||
alias: "F"
|
alias: "F"
|
||||||
|
optional: true
|
||||||
-
|
-
|
||||||
name: "sterling"
|
name: "sterling"
|
||||||
rate: 240
|
rate: 240
|
||||||
|
@ -94,15 +121,16 @@ while a suit of heavy plate armor equals fifteen gold coins, rather than fifteen
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Make sure that you first have `rust` and `cargo` installed onto your computer before downloading
|
The easiest way to install `sterling` is to do so with `cargo`, the build tool that's installed
|
||||||
`sterling`. Just follow the simple
|
along with the `rust` compiler. If you already have `rust` and `cargo` installed onto your computer,
|
||||||
[Installation Guide](https://doc.rust-lang.org/cargo/getting-started/installation.html) on the
|
simply run the following command from a command prompt:
|
||||||
official Rust language website to install both programs.
|
|
||||||
|
|
||||||
Once `rust` and `cargo` are installed onto your computer, run the following command:
|
```
|
||||||
|
$ cargo install sterling
|
||||||
|
```
|
||||||
|
|
||||||
`cargo install sterling`
|
If you do not have the `rust` compiler installed, you can also find pre-built binaries for 64-bit
|
||||||
|
Windows, macOS, and Linux computers in the "Tags" navigation link, which is displayed above this
|
||||||
This will install `sterling` into the `.cargo/bin` directory within your User directory
|
README. Simply download the correct binary for your computer's operating system, extract it
|
||||||
(`/home/YOUR_USER_NAME` on Linux and macOS, `C:\Users\YOUR_USER_NAME` on Windows). Be sure to add
|
somewhere into your file system (such as a "bin" folder within your user directory), and add that
|
||||||
this directory to your PATH.
|
location to your system's PATH.
|
29
appveyor.yml
Normal file
29
appveyor.yml
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
os: Visual Studio 2015
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
environment:
|
||||||
|
matrix:
|
||||||
|
- channel: stable
|
||||||
|
target: x86_64-pc-windows-gnu
|
||||||
|
artifacts:
|
||||||
|
- path: target/release/sterling.exe
|
||||||
|
name: sterling
|
||||||
|
install:
|
||||||
|
- appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe
|
||||||
|
- rustup-init -yv --default-toolchain %channel% --default-host %target%
|
||||||
|
- set PATH=%PATH%;%USERPROFILE%\.cargo\bin
|
||||||
|
- rustc -vV
|
||||||
|
- cargo -vV
|
||||||
|
build: false
|
||||||
|
test_script:
|
||||||
|
- cargo test --verbose
|
||||||
|
- cargo build --release
|
||||||
|
deploy:
|
||||||
|
provider: GitHub
|
||||||
|
description: ''
|
||||||
|
auth_token:
|
||||||
|
secure: bvA/4J1T0h65ur6tsg6k/wlZFjP3qr2QsyRsmGMEmm7DOF61xmzTnjuBcPjQYrba
|
||||||
|
artifact: target/release/sterling.exe
|
||||||
|
on:
|
||||||
|
branch: master
|
|
@ -1,6 +1,6 @@
|
||||||
|
use std::convert::From;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fmt::{self, Display, Formatter};
|
use std::fmt::{self, Display, Formatter};
|
||||||
use std::convert::From;
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{self, BufReader, ErrorKind};
|
use std::io::{self, BufReader, ErrorKind};
|
||||||
|
|
||||||
|
@ -18,10 +18,10 @@ pub fn load_config(filename: &str) -> Result<Vec<Currency>, ConfigError> {
|
||||||
|
|
||||||
pub fn default_config() -> Vec<Currency> {
|
pub fn default_config() -> Vec<Currency> {
|
||||||
vec![
|
vec![
|
||||||
Currency::new("platinum", 1000000, "p", None),
|
Currency::new("platinum", 1000000, "p", None, None),
|
||||||
Currency::new("gold", 10000, "g", None),
|
Currency::new("gold", 10000, "g", None, None),
|
||||||
Currency::new("silver", 100, "s", None),
|
Currency::new("silver", 100, "s", None, None),
|
||||||
Currency::new("copper", 1, "c", None),
|
Currency::new("copper", 1, "c", None, None),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
155
src/convert.rs
Normal file
155
src/convert.rs
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
use currency::Currency;
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
|
pub fn convert_to_copper(amount: usize, coin_denomination: &str) -> usize {
|
||||||
|
match coin_denomination {
|
||||||
|
"p" => amount * 1000,
|
||||||
|
"g" => amount * 100,
|
||||||
|
"e" => amount * 50,
|
||||||
|
"s" => amount * 10,
|
||||||
|
"c" => amount,
|
||||||
|
_ => unreachable!("Invalid coin type; must be a valid coin found in the PHB."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use convert::*;
|
||||||
|
use currency::Currency;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref STANDARD_CURRENCIES: [Currency; 4] = [
|
||||||
|
Currency::new("platinum", 1000000, "p", None, None),
|
||||||
|
Currency::new("gold", 10000, "g", None, None),
|
||||||
|
Currency::new("silver", 100, "s", None, None),
|
||||||
|
Currency::new("copper", 1, "c", None, 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, None).with_value(0),
|
||||||
|
Currency::new("gold", 10000, "g", None, None).with_value(0),
|
||||||
|
Currency::new("silver", 100, "s", None, None).with_value(0),
|
||||||
|
Currency::new("copper", 1, "c", None, 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, None).with_value(0),
|
||||||
|
Currency::new("gold", 10000, "g", None, None).with_value(0),
|
||||||
|
Currency::new("silver", 100, "s", None, None).with_value(1),
|
||||||
|
Currency::new("copper", 1, "c", None, 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, None).with_value(0),
|
||||||
|
Currency::new("gold", 10000, "g", None, None).with_value(1),
|
||||||
|
Currency::new("silver", 100, "s", None, None).with_value(0),
|
||||||
|
Currency::new("copper", 1, "c", None, 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, None).with_value(1),
|
||||||
|
Currency::new("gold", 10000, "g", None, None).with_value(0),
|
||||||
|
Currency::new("silver", 100, "s", None, None).with_value(0),
|
||||||
|
Currency::new("copper", 1, "c", None, None).with_value(0),
|
||||||
|
];
|
||||||
|
|
||||||
|
assert_eq!(currencies, exchange(1000000, STANDARD_CURRENCIES.to_vec()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,16 +7,24 @@ pub struct Currency {
|
||||||
pub value: Option<usize>,
|
pub value: Option<usize>,
|
||||||
pub alias: String,
|
pub alias: String,
|
||||||
pub plural: Option<String>,
|
pub plural: Option<String>,
|
||||||
|
pub optional: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Currency {
|
impl Currency {
|
||||||
pub fn new(name: &str, rate: usize, alias: &str, plural: Option<String>) -> Currency {
|
pub fn new(
|
||||||
|
name: &str,
|
||||||
|
rate: usize,
|
||||||
|
alias: &str,
|
||||||
|
plural: Option<String>,
|
||||||
|
optional: Option<bool>,
|
||||||
|
) -> Currency {
|
||||||
Currency {
|
Currency {
|
||||||
name: name.to_owned(),
|
name: name.to_owned(),
|
||||||
rate,
|
rate,
|
||||||
value: None,
|
value: None,
|
||||||
alias: alias.to_owned(),
|
alias: alias.to_owned(),
|
||||||
plural,
|
plural,
|
||||||
|
optional,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,6 +35,14 @@ impl Currency {
|
||||||
value: Some(value),
|
value: Some(value),
|
||||||
alias: self.alias.clone(),
|
alias: self.alias.clone(),
|
||||||
plural: self.plural.clone(),
|
plural: self.plural.clone(),
|
||||||
|
optional: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_optional(&self) -> bool {
|
||||||
|
match self.optional {
|
||||||
|
Some(optional) => optional,
|
||||||
|
None => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,10 +66,6 @@ impl Currency {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// impl Display for Currency {
|
|
||||||
// fn
|
|
||||||
// }
|
|
||||||
|
|
||||||
impl Ord for Currency {
|
impl Ord for Currency {
|
||||||
fn cmp(&self, other: &Currency) -> Ordering {
|
fn cmp(&self, other: &Currency) -> Ordering {
|
||||||
self.value.cmp(&other.value)
|
self.value.cmp(&other.value)
|
||||||
|
|
244
src/main.rs
244
src/main.rs
|
@ -10,8 +10,10 @@ extern crate serde_derive;
|
||||||
extern crate serde_yaml;
|
extern crate serde_yaml;
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
|
mod convert;
|
||||||
mod currency;
|
mod currency;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
use std::process;
|
use std::process;
|
||||||
|
|
||||||
|
@ -21,11 +23,24 @@ use regex::Regex;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let app = clap_app!(sterling =>
|
let app = clap_app!(sterling =>
|
||||||
(version: "0.2.0")
|
(version: env!("CARGO_PKG_VERSION"))
|
||||||
(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 CONFIG: -c --config +takes_value "Specify location of config file; defaults to './sterling-conf.yml'.")
|
(@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 PRINT_FULL: -f --full "Print currencies with full name, rather than with alias.")
|
||||||
|
(@arg OPTIONAL: -o --optional "Include currencies marked as optional when converting.")
|
||||||
(@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.")
|
(@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.")
|
||||||
|
(@subcommand add =>
|
||||||
|
(about: "Add two currency amounts together; uses the currencies defined in your config file")
|
||||||
|
(@arg AUGEND: +required "The augend of the addition function; i.e. the left side")
|
||||||
|
(@arg ADDEND: +required "The addend of the addition function; i.e. the right side")
|
||||||
|
(@arg PRINT_FULL: -f --full "Print currencies with full name, rather than with alias.")
|
||||||
|
)
|
||||||
|
(@subcommand sub =>
|
||||||
|
(about: "Subtract two currency amounts from one another; uses the currencies defined in your config file")
|
||||||
|
(@arg MINUEND: +required "The minuend of the subtraction function; i.e. the left side")
|
||||||
|
(@arg SUBTRAHEND: +required "The subtrahend of the subtraction function; i.e. the right side")
|
||||||
|
(@arg PRINT_FULL: -f --full "Print currencies with full name, rather than with alias.")
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
let matches = app.get_matches();
|
let matches = app.get_matches();
|
||||||
|
@ -34,17 +49,61 @@ fn main() {
|
||||||
None => "./sterling-conf.yml",
|
None => "./sterling-conf.yml",
|
||||||
});
|
});
|
||||||
|
|
||||||
let currencies = match parse_currency_config(config_result, matches.value_of("CONFIG")) {
|
let currencies: Vec<Currency> =
|
||||||
Ok(currencies) => currencies,
|
match parse_currency_config(config_result, matches.value_of("CONFIG")) {
|
||||||
|
Ok(currencies) => currencies
|
||||||
|
.iter()
|
||||||
|
.filter(|c| {
|
||||||
|
let has_add_subcommand = match matches.subcommand_matches("add") {
|
||||||
|
Some(_) => true,
|
||||||
|
None => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if has_add_subcommand {
|
||||||
|
true
|
||||||
|
} else if !matches.is_present("OPTIONAL") {
|
||||||
|
!c.is_optional()
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect(),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
eprintln!("{}", error);
|
eprintln!("{}", error);
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(values) = matches.values_of("VALUE") {
|
if let Some(matches) = matches.subcommand_matches("add") {
|
||||||
|
let (lhs, rhs) = get_copper_value(
|
||||||
|
¤cies,
|
||||||
|
matches.value_of("AUGEND").unwrap(),
|
||||||
|
matches.value_of("ADDEND").unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let converted_currencies = convert::convert_currencies(lhs + rhs, currencies);
|
||||||
|
let display_strings: Vec<String> =
|
||||||
|
create_display_strings(converted_currencies, matches.is_present("PRINT_FULL"));
|
||||||
|
|
||||||
|
println!("{}", (&display_strings).join(", "));
|
||||||
|
} else if let Some(matches) = matches.subcommand_matches("sub") {
|
||||||
|
let (lhs, rhs) = get_copper_value(
|
||||||
|
¤cies,
|
||||||
|
matches.value_of("MINUEND").unwrap(),
|
||||||
|
matches.value_of("SUBTRAHEND").unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let difference = if lhs > rhs { lhs - rhs } else { rhs - lhs };
|
||||||
|
|
||||||
|
let converted_currencies = convert::convert_currencies(difference, currencies);
|
||||||
|
let display_strings: Vec<String> =
|
||||||
|
create_display_strings(converted_currencies, matches.is_present("PRINT_FULL"));
|
||||||
|
|
||||||
|
println!("{}", (&display_strings).join(", "));
|
||||||
|
} else 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) {
|
let total_copper_value = match convert::calculate_total_copper_value(coins) {
|
||||||
Ok(total_copper_value) => total_copper_value,
|
Ok(total_copper_value) => total_copper_value,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("{}", err);
|
eprintln!("{}", err);
|
||||||
|
@ -52,7 +111,7 @@ fn main() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let converted_currencies = convert_currencies(total_copper_value, currencies);
|
let converted_currencies = convert::convert_currencies(total_copper_value, currencies);
|
||||||
let display_strings: Vec<String> =
|
let display_strings: Vec<String> =
|
||||||
create_display_strings(converted_currencies, matches.is_present("PRINT_FULL"));
|
create_display_strings(converted_currencies, matches.is_present("PRINT_FULL"));
|
||||||
|
|
||||||
|
@ -72,68 +131,19 @@ fn parse_currency_config(
|
||||||
Err(error) => match error.kind {
|
Err(error) => match error.kind {
|
||||||
ErrorKind::NotFound => {
|
ErrorKind::NotFound => {
|
||||||
if let Some(file_path) = config_file_path {
|
if let Some(file_path) = config_file_path {
|
||||||
Err(format!("Sterling Error: Can't find configuration file: \"{}\"", &file_path))
|
Err(format!(
|
||||||
|
"Sterling Error: Can't find configuration file: \"{}\"",
|
||||||
|
&file_path
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
Ok(config::default_config())
|
Ok(config::default_config())
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
_ => Err(format!("Sterling Error: {}", error)),
|
_ => Err(format!("Sterling Error: {}", error)),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_to_copper(amount: usize, coin_denomination: &str) -> usize {
|
|
||||||
match coin_denomination {
|
|
||||||
"p" => amount * 1000,
|
|
||||||
"g" => amount * 100,
|
|
||||||
"e" => amount * 50,
|
|
||||||
"s" => amount * 10,
|
|
||||||
"c" => amount,
|
|
||||||
_ => unreachable!("Invalid coin type; must be a valid coin found in the PHB."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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> {
|
fn create_display_strings(converted_currencies: Vec<Currency>, is_print_full: bool) -> Vec<String> {
|
||||||
converted_currencies
|
converted_currencies
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -147,103 +157,35 @@ fn create_display_strings(converted_currencies: Vec<Currency>, is_print_full: bo
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
fn get_copper_value(currencies: &[Currency], lhs: &str, rhs: &str) -> (usize, usize) {
|
||||||
mod tests {
|
let mut rates: HashMap<String, usize> = HashMap::with_capacity(currencies.len());
|
||||||
use super::*;
|
for currency in currencies {
|
||||||
use currency::Currency;
|
rates.insert(currency.alias.clone(), currency.rate);
|
||||||
|
|
||||||
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]
|
let aliases = currencies
|
||||||
fn test_convert_copper_to_copper() {
|
.iter()
|
||||||
assert_eq!(1, convert_to_copper(1, "c"));
|
.cloned()
|
||||||
}
|
.map(|c| c.alias)
|
||||||
|
.fold(String::new(), |group, a| group + &a);
|
||||||
|
|
||||||
#[test]
|
let regex: Regex = Regex::new(&format!("(\\d+)([{}])", aliases)).unwrap();
|
||||||
fn test_convert_silver_to_copper() {
|
|
||||||
assert_eq!(10, convert_to_copper(1, "s"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
let left_hand_side: usize = regex.captures_iter(lhs).fold(0, |sum, cap| {
|
||||||
fn test_convert_electrum_to_copper() {
|
let value: usize = cap[1].parse().unwrap();
|
||||||
assert_eq!(50, convert_to_copper(1, "e"));
|
let rate: usize = *rates.get(&cap[2]).unwrap();
|
||||||
}
|
let product = value * rate;
|
||||||
|
|
||||||
#[test]
|
sum + product
|
||||||
fn test_convert_gold_to_copper() {
|
});
|
||||||
assert_eq!(100, convert_to_copper(1, "g"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
let right_hand_side: usize = regex.captures_iter(rhs).fold(0, |sum, cap| {
|
||||||
fn test_convert_platinum_to_copper() {
|
let value: usize = cap[1].parse().unwrap();
|
||||||
assert_eq!(1000, convert_to_copper(1, "p"));
|
let rate: usize = *rates.get(&cap[2]).unwrap();
|
||||||
}
|
let product = value * rate;
|
||||||
|
|
||||||
#[test]
|
sum + product
|
||||||
fn test_calculate_total_copper_value() {
|
});
|
||||||
let values = vec!["1p", "1g", "1e", "1s", "1c"];
|
|
||||||
assert_eq!(1161, calculate_total_copper_value(values).unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
(left_hand_side, right_hand_side)
|
||||||
#[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