Release v1.0.0

This is a monumental release in the development of sterling, culminating
in the finalization of the command-line API and a stabilization of
sterling's features. Woo-hoo!!

Sterling's functionality is very basic: conversion of PHB currencies
into user-defined ones, basic arithmetic operations, and converting
custom currency amounts to the equivalent amount in PHB copper. It's
unlikely that I will be adding any additional features ontop of this
set, as it already provides functionality appropriate enough for most
usecases.

Congrats to me on finally finishing a side-project!
This commit is contained in:
Zachary Dziura 2018-08-06 18:01:40 -04:00
parent f7a6ec53a0
commit 90a3f41c2d
15 changed files with 706 additions and 363 deletions

View file

@ -9,16 +9,4 @@ cargo:test:
- master
script:
- cargo test --verbose
cargo:build:
stage: deploy
only:
- master
script:
- cargo build --release
cache:
paths:
- target/
artifacts:
paths:
- target/release/sterling
- cargo bench

View file

@ -1,27 +1,62 @@
# Based on the "trust" template v0.1.2
# https://github.com/japaric/trust/tree/v0.1.2
dist: trusty
language: rust
os:
- osx
branches:
only:
- master
rust:
- stable
services: docker
sudo: required
env:
global:
# TODO Update this to match the name of your project.
- CRATE_NAME=sterling
matrix:
include:
# Linux
- env: TARGET=x86_64-unknown-linux-gnu
# OSX
- env: TARGET=x86_64-apple-darwin
os: osx
before_install:
- set -e
- rustup self update
install:
- sh ci/install.sh
- source ~/.cargo/env || true
script:
- cargo test --verbose
- cargo build --release
- bash ci/script.sh
after_script: set +e
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"
- sh ci/before_deploy.sh
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
file: $CRATE_NAME-$TRAVIS_TAG-$TARGET.*
on:
repo: zcdziura/sterling
branch: master
addons:
artifacts: true
tags: true
provider: releases
skip_cleanup: true
cache: cargo
before_cache:
# Travis can't cache files that are not readable by "others"
- chmod -R a+r $HOME/.cargo
branches:
only:
# release tags
- /^v\d+\.\d+\.\d+.*$/
# - master
notifications:
email:
on_success: never

View file

@ -1,6 +1,6 @@
[package]
name = "sterling"
version = "0.3.0"
version = "1.0.0"
description = "Converts a given D&D 5e currency value to the Silver Standard."
authors = ["Zachary Dziura <zcdziura@gmail.com>"]
readme = "README.md"
@ -8,14 +8,31 @@ license = "Unlicense/MIT"
repository = "https://gitlab.com/zcdziura/sterling"
keywords = ["dnd", "coins", "converter", "currency", "5e"]
[lib]
name = "sterling_ops"
path = "src/lib.rs"
[[bin]]
name = "sterling"
path = "src/main.rs"
[dependencies]
clap = "2.31"
clap = "2.32"
lazysort = "0.2"
lazy_static = "1.0"
regex = "1.0"
separator = "0.3"
serde = "1.0"
serde_derive = "1.0"
serde_yaml = "0.7"
[dev-dependencies]
criterion = "0.2"
[profile.release]
lto = true
panic = "abort"
panic = "abort"
[[bench]]
name = "bench"
harness = false

View file

@ -92,7 +92,6 @@ within my own campaign!
name: "guilder"
rate: 896
alias: "g"
plural: "guilders"
-
name: "shilling"
rate: 32

View file

@ -1,29 +1,60 @@
os: Visual Studio 2015
branches:
only:
- master
# Based on the "trust" template v0.1.2
# https://github.com/japaric/trust/tree/v0.1.2
environment:
global:
RUST_VERSION: stable
CRATE_NAME: sterling
matrix:
- channel: stable
target: x86_64-pc-windows-gnu
artifacts:
- path: target/release/sterling.exe
name: sterling
# MinGW
- TARGET: x86_64-pc-windows-gnu
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
- ps: >-
If ($Env:TARGET -eq 'x86_64-pc-windows-gnu') {
$Env:PATH += ';C:\msys64\mingw64\bin'
} ElseIf ($Env:TARGET -eq 'i686-pc-windows-gnu') {
$Env:PATH += ';C:\msys64\mingw32\bin'
}
- curl -sSf -o rustup-init.exe https://win.rustup.rs/
- rustup-init.exe -y --default-host %TARGET% --default-toolchain %RUST_VERSION%
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
- rustc -Vv
- cargo -V
test_script:
- cargo test --verbose
- cargo build --release
# we don't run the "test phase" when doing deploys
- if [%APPVEYOR_REPO_TAG%]==[true] (
cargo test --target %TARGET% &&
cargo build --target %TARGET% --release
)
before_deploy:
- ps: ci\before_deploy.ps1
deploy:
provider: GitHub
description: ''
artifact: /.*\.zip/
auth_token:
secure: bvA/4J1T0h65ur6tsg6k/wlZFjP3qr2QsyRsmGMEmm7DOF61xmzTnjuBcPjQYrba
artifact: target/release/sterling.exe
on:
branch: master
RUST_VERSION: stable
appveyor_repo_tag: true
provider: GitHub
cache:
- C:\Users\appveyor\.cargo\registry
- target
branches:
only:
# Release tags
- /^v\d+\.\d+\.\d+.*$/
# - master
notifications:
- provider: Email
on_build_success: false
# Building is done in the test phase, so we disable Appveyor's build phase.
build: false

27
benches/bench.rs Normal file
View file

@ -0,0 +1,27 @@
#[macro_use]
extern crate criterion;
#[macro_use]
extern crate lazy_static;
extern crate sterling_ops;
use criterion::Criterion;
use sterling_ops::currency::Currency;
use sterling_ops::*;
lazy_static! {
static ref CURRENCIES: Vec<Currency> = vec![
Currency::new("penny", 1, "p", Some("pence".to_owned()), None),
Currency::new("shilling", 100, "s", Some("sterling".to_owned()), None),
Currency::new("guilder", 10_000, "g", None, None),
Currency::new("note", 1_000_000, "N", None, Some(true)),
];
}
fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("default operation", |b| {
b.iter(|| default_operation("3p 5s 7s 132c", &CURRENCIES, true))
});
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

22
ci/before_deploy.ps1 Normal file
View file

@ -0,0 +1,22 @@
# This script takes care of packaging the build artifacts that will go in the
# release zipfile
$SRC_DIR = $PWD.Path
$STAGE = [System.Guid]::NewGuid().ToString()
Set-Location $ENV:Temp
New-Item -Type Directory -Name $STAGE
Set-Location $STAGE
$ZIP = "$SRC_DIR\$($Env:CRATE_NAME)-$($Env:APPVEYOR_REPO_TAG_NAME)-$($Env:TARGET).zip"
Copy-Item "$SRC_DIR\target\$($Env:TARGET)\release\sterling.exe" '.\'
7z a "$ZIP" *
Push-AppveyorArtifact "$ZIP"
Remove-Item *.* -Force
Set-Location ..
Remove-Item $STAGE
Set-Location $SRC_DIR

32
ci/before_deploy.sh Normal file
View file

@ -0,0 +1,32 @@
# This script takes care of building your crate and packaging it for release
set -ex
main() {
local src=$(pwd) \
stage=
case $TRAVIS_OS_NAME in
linux)
stage=$(mktemp -d)
;;
osx)
stage=$(mktemp -d -t tmp)
;;
esac
test -f Cargo.lock || cargo generate-lockfile
cargo build --release --target $TARGET
# TODO Update this to package the right artifacts
cp target/$TARGET/release/sterling $stage/
cd $stage
tar czf $src/$CRATE_NAME-$TRAVIS_TAG-$TARGET.tar.gz *
cd $src
rm -rf $stage
}
main

14
ci/install.sh Normal file
View file

@ -0,0 +1,14 @@
set -ex
main() {
local target=
if [ $TRAVIS_OS_NAME = linux ]; then
target=x86_64-unknown-linux-gnu
sort=sort
else
target=x86_64-apple-darwin
sort=gsort # for `sort --sort-version`, from brew's coreutils.
fi
}
main

18
ci/script.sh Normal file
View file

@ -0,0 +1,18 @@
# This script takes care of testing your crate
set -ex
main() {
cross build --target $TARGET --release
if [ ! -z $DISABLE_TESTS ]; then
return
fi
cross test --target $TARGET
}
# we don't run the "test phase" when doing deploys
if [ -z $TRAVIS_TAG ]; then
main
fi

View file

@ -10,16 +10,47 @@ 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));
let config: Vec<Currency> = serde_yaml::from_reader(BufReader::new(config_file))?;
Ok(configs)
Ok(config)
}
pub fn default_config() -> Vec<Currency> {
pub 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(silver_standard_config())
}
}
_ => Err(format!("Sterling Error: {}", error)),
},
}
}
pub fn phb_config() -> Vec<Currency> {
vec![
Currency::new("platinum", 1000000, "p", None, None),
Currency::new("gold", 10000, "g", None, None),
Currency::new("platinum", 1000, "p", None, None),
Currency::new("gold", 100, "g", None, None),
Currency::new("electrum", 50, "e", None, Some(true)),
Currency::new("silver", 10, "s", None, None),
Currency::new("copper", 1, "c", None, None),
]
}
fn silver_standard_config() -> Vec<Currency> {
vec![
Currency::new("platinum", 1_000_000, "p", None, None),
Currency::new("gold", 10_000, "g", None, None),
Currency::new("silver", 100, "s", None, None),
Currency::new("copper", 1, "c", None, None),
]

View file

@ -1,155 +1,109 @@
use currency::Currency;
use std::collections::HashMap;
use std::str::FromStr;
use regex::Regex;
use separator::Separatable;
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."),
}
use currency::Currency;
use lazysort::Sorted;
pub fn calculate_total_copper_value(
values: &str,
currency_regex: &Regex,
rates: HashMap<String, usize>,
) -> usize {
currency_regex
.captures_iter(&values)
.fold(0, |sum, capture| {
let value: usize = str::replace(&capture[1], ",", "").parse().unwrap();
let rate: &usize = rates.get(&capture[2]).unwrap();
let product = value * rate;
sum + product
})
}
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;
pub fn exchange_currencies(
copper_value: usize,
currencies: &[Currency],
print_full_name: bool,
) -> Vec<String> {
let mut val = copper_value;
currencies
.iter_mut()
.iter()
.sorted()
.filter(|currency| currency.optional.unwrap_or(true))
.map(|currency| {
let value = val / currency.rate;
val = val % currency.rate;
currency.with_value(value)
(
value,
if print_full_name {
if value > 1 {
match (&currency).plural {
Some(ref plural) => String::from_str(plural).unwrap(),
None => format!("{}s", &currency.name),
}
} else {
String::from_str(&currency.name).unwrap()
}
} else {
String::from_str(&currency.alias).unwrap()
},
)
})
.filter(|tuple| tuple.0 > 0)
.map(|tuple| {
format!(
"{}{}{}",
tuple.0.separated_string(),
if tuple.1.len() > 1 { " " } else { "" },
tuple.1
)
})
.collect()
}
#[cfg(test)]
mod tests {
use convert::*;
use std::collections::HashMap;
use super::{calculate_total_copper_value, exchange_currencies};
use currency::Currency;
use regex::Regex;
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),
static ref STANDARD_CURRENCIES: Vec<Currency> = vec![
Currency::new("guilder", 10_000, "g", None, None),
Currency::new("shilling", 100, "s", Some("sterling".to_owned()), None),
Currency::new("penny", 1, "p", Some("pence".to_owned()), 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"));
static ref CURRENCY_REGEX: Regex = Regex::new(r"(\d+)([Ngsp])").unwrap();
}
#[test]
fn test_calculate_total_copper_value() {
let values = vec!["1p", "1g", "1e", "1s", "1c"];
assert_eq!(1161, calculate_total_copper_value(values).unwrap());
let rates: HashMap<String, usize> = vec![
("g".to_owned(), 10_000usize),
("s".to_owned(), 100usize),
("p".to_owned(), 1usize),
].into_iter()
.collect();
let result = 10101usize;
assert_eq!(
result,
calculate_total_copper_value("1g 1s 1p", &CURRENCY_REGEX, rates)
);
}
#[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()));
fn test_exchange_currencies() {
let result = vec!["1g".to_owned(), "1s".to_owned(), "1p".to_owned()];
assert_eq!(
result,
exchange_currencies(10101, &STANDARD_CURRENCIES, false)
);
}
}

View file

@ -4,7 +4,6 @@ use std::cmp::Ordering;
pub struct Currency {
pub name: String,
pub rate: usize,
pub value: Option<usize>,
pub alias: String,
pub plural: Option<String>,
pub optional: Option<bool>,
@ -21,54 +20,23 @@ impl Currency {
Currency {
name: name.to_owned(),
rate,
value: None,
alias: alias.to_owned(),
plural,
optional,
}
}
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(),
optional: None,
}
}
pub fn is_optional(&self) -> bool {
match self.optional {
Some(optional) => optional,
None => false,
}
}
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 Ord for Currency {
fn cmp(&self, other: &Currency) -> Ordering {
self.value.cmp(&other.value)
other.rate.cmp(&self.rate)
}
}
@ -80,6 +48,6 @@ impl PartialOrd for Currency {
impl PartialEq for Currency {
fn eq(&self, other: &Currency) -> bool {
self.value == other.value
self.rate == other.rate
}
}

244
src/lib.rs Normal file
View file

@ -0,0 +1,244 @@
extern crate lazysort;
#[allow(unused_imports)]
#[macro_use]
extern crate lazy_static;
extern crate regex;
extern crate separator;
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_yaml;
use std::collections::HashMap;
pub mod config;
mod convert;
pub mod currency;
use currency::Currency;
use regex::Regex;
use separator::Separatable;
pub fn add_operation(
augend: &str,
addend: &str,
custom_currency_regex: &Regex,
currencies: &[Currency],
print_full: bool,
) -> String {
let lhs =
convert::calculate_total_copper_value(augend, custom_currency_regex, get_rates(currencies));
let rhs = convert::calculate_total_copper_value(
addend,
&custom_currency_regex,
get_rates(currencies.as_ref()),
);
convert::exchange_currencies(lhs + rhs, currencies, print_full).join(", ")
}
pub fn sub_operation(
minuend: &str,
subtrahend: &str,
custom_currency_regex: &Regex,
currencies: &[Currency],
print_full: bool,
) -> String {
let lhs = convert::calculate_total_copper_value(
minuend,
custom_currency_regex,
get_rates(currencies),
);
let rhs = convert::calculate_total_copper_value(
subtrahend,
custom_currency_regex,
get_rates(currencies),
);
let difference = if lhs > rhs { lhs - rhs } else { rhs - lhs };
convert::exchange_currencies(difference, currencies, print_full).join(", ")
}
pub fn mul_operation(
multiplicand: &str,
multiplier: usize,
custom_currency_regex: &Regex,
currencies: &[Currency],
print_full: bool,
) -> String {
let lhs = convert::calculate_total_copper_value(
multiplicand,
custom_currency_regex,
get_rates(currencies),
);
convert::exchange_currencies(lhs * multiplier, currencies, print_full).join(", ")
}
pub fn div_operation(
dividend: &str,
divisor: usize,
custom_currency_regex: &Regex,
currencies: &[Currency],
print_full: bool,
) -> String {
let lhs = convert::calculate_total_copper_value(
dividend,
custom_currency_regex,
get_rates(currencies),
);
convert::exchange_currencies(lhs / divisor, currencies, print_full).join(", ")
}
pub fn copper_operation(
values: &str,
custom_currency_regex: &Regex,
currencies: &[Currency],
) -> String {
let copper_value =
convert::calculate_total_copper_value(values, custom_currency_regex, get_rates(currencies));
format!("{}c", copper_value.separated_string())
}
pub fn default_operation(values: &str, currencies: &[Currency], print_full: bool) -> String {
let copper_value = convert::calculate_total_copper_value(
values,
&Regex::new(r"(0|(?:[1-9](?:\d+|\d{0,2}(?:,\d{3})*)))+([cegps])").unwrap(),
get_rates(config::phb_config().as_ref()),
);
let exchanged_currencies = convert::exchange_currencies(copper_value, currencies, print_full);
exchanged_currencies.join(", ")
}
fn get_rates(currencies: &[Currency]) -> HashMap<String, usize> {
currencies
.iter()
.map(|c| c.alias.clone())
.zip(currencies.iter().map(|c| c.rate))
.collect()
}
#[cfg(test)]
mod test {
use std::collections::HashMap;
use currency::Currency;
use regex::Regex;
use super::{
add_operation, copper_operation, default_operation, div_operation, get_rates,
mul_operation, sub_operation,
};
lazy_static! {
static ref CURRENCIES: Vec<Currency> = vec![
Currency::new("penny", 1, "p", Some("pence".to_owned()), None),
Currency::new("shilling", 100, "s", Some("sterling".to_owned()), None),
];
static ref CUSTOM_CURRENCY_REGEX: Regex = Regex::new(&format!(
"(\\d+)([{}])",
CURRENCIES
.iter()
.map(|c| c.alias.clone())
.fold(String::new(), |group, a| group + &a)
)).unwrap();
}
#[test]
fn test_get_rates() {
let rates: HashMap<_, _> = vec![("p".to_owned(), 1usize), ("s".to_owned(), 100usize)]
.into_iter()
.collect();
assert_eq!(rates, get_rates(&CURRENCIES));
}
#[test]
fn test_add_operation_same_currencies() {
let result = "3p".to_owned();
assert_eq!(
result,
add_operation("1p", "2p", &CUSTOM_CURRENCY_REGEX, &CURRENCIES, false)
);
}
#[test]
fn test_add_operation_diff_currencies() {
let result = "1s, 1p".to_owned();
assert_eq!(
result,
add_operation("1s", "1p", &CUSTOM_CURRENCY_REGEX, &CURRENCIES, false)
);
}
#[test]
fn test_sub_operation_smaller_subtrahend() {
let result = "1p".to_owned();
assert_eq!(
result,
sub_operation("2p", "1p", &CUSTOM_CURRENCY_REGEX, &CURRENCIES, false)
);
}
#[test]
fn test_sub_operation_larger_subtrahend() {
let result = "1p".to_owned();
assert_eq!(
result,
sub_operation("1p", "2p", &CUSTOM_CURRENCY_REGEX, &CURRENCIES, false)
);
}
#[test]
fn test_mul_operation() {
let result = "6p".to_owned();
assert_eq!(
result,
mul_operation("3p", 2, &CUSTOM_CURRENCY_REGEX, &CURRENCIES, false)
);
}
#[test]
fn test_div_operation_even_dividend() {
let result = "2p".to_owned();
assert_eq!(
result,
div_operation("4p", 2, &CUSTOM_CURRENCY_REGEX, &CURRENCIES, false)
);
}
#[test]
fn test_div_operation_odd_dividend() {
let result = "1p".to_owned();
assert_eq!(
result,
div_operation("3p", 2, &CUSTOM_CURRENCY_REGEX, &CURRENCIES, false)
);
}
#[test]
fn test_copper_operation() {
let result = "103c".to_owned();
assert_eq!(
result,
copper_operation("1s 3p", &CUSTOM_CURRENCY_REGEX, &CURRENCIES)
);
}
#[test]
fn test_default_operation() {
let result = "1 shilling";
assert_eq!(result, default_operation("1g", &CURRENCIES, true));
}
#[test]
fn test_default_operation_plural_output() {
let result = "2 sterling";
assert_eq!(result, default_operation("2g", &CURRENCIES, true));
}
}

View file

@ -1,45 +1,46 @@
#[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;
extern crate sterling_ops;
mod config;
mod convert;
mod currency;
use std::collections::HashMap;
use std::io::ErrorKind;
use std::process;
use config::ConfigError;
use currency::Currency;
use regex::Regex;
use sterling_ops::config;
use sterling_ops::currency::Currency;
use sterling_ops::*;
fn main() {
let app = clap_app!(sterling =>
(version: env!("CARGO_PKG_VERSION"))
(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 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 PRINT_FULL: -f --full "Print currencies with their full name, rather than with their 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.")
(@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.")
(@arg AUGEND: +required "The augend of the addition function")
(@arg ADDEND: +required "The addend of the addition function")
)
(@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.")
(@arg MINUEND: +required "The minuend of the subtraction function")
(@arg SUBTRAHEND: +required "The subtrahend of the subtraction function")
)
(@subcommand mul =>
(about: "Multiply a scalar multiplicand by a currency amount; uses the currencies defined in your config file")
(@arg MULTIPLIER: +required "The scalar multiplier of the multiplication function")
(@arg MULTIPLICAND: +required ... "The currency values to be multiplied")
)
(@subcommand div =>
(about: "Divide a currency amount by some scalar divisor; uses the currencies defined in your config file")
(@arg DIVISOR: +required "The scalar divisor of the division function")
(@arg DIVIDEND: +required ... "The currency values to be divided")
)
(@subcommand copper =>
(about: "Calculate the copper value of a custom currency")
(@arg VALUE: +required ... "The custom currency value")
)
);
@ -50,24 +51,22 @@ fn main() {
});
let currencies: Vec<Currency> =
match parse_currency_config(config_result, matches.value_of("CONFIG")) {
match config::parse_currency_config(config_result, matches.value_of("CONFIG")) {
Ok(currencies) => currencies
.iter()
.into_iter()
.filter(|c| {
let has_add_subcommand = match matches.subcommand_matches("add") {
let is_sub_command = match matches.subcommand_name() {
Some(_) => true,
None => false,
};
if has_add_subcommand {
if is_sub_command {
true
} else if !matches.is_present("OPTIONAL") {
!c.is_optional()
} else {
true
(!matches.is_present("OPTIONAL") && !c.is_optional())
|| matches.is_present("OPTIONAL")
}
})
.cloned()
.collect(),
Err(error) => {
eprintln!("{}", error);
@ -75,117 +74,81 @@ fn main() {
}
};
if let Some(matches) = matches.subcommand_matches("add") {
let (lhs, rhs) = get_copper_value(
let custom_currency_regex: Regex = Regex::new(&format!(
"(\\d+)([{}])",
currencies
.iter()
.map(|c| c.alias.clone())
.fold(String::new(), |group, a| group + &a)
)).unwrap();
let operation_result = match matches.subcommand() {
("add", Some(command)) => add_operation(
command.value_of("AUGEND").unwrap(),
command.value_of("ADDEND").unwrap(),
&custom_currency_regex,
&currencies,
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(
matches.is_present("PRINT_FULL"),
),
("sub", Some(command)) => sub_operation(
command.value_of("MINUEND").unwrap(),
command.value_of("SUBTRAHEND").unwrap(),
&custom_currency_regex,
&currencies,
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 total_copper_value = match convert::calculate_total_copper_value(coins) {
Ok(total_copper_value) => total_copper_value,
Err(err) => {
eprintln!("{}", err);
matches.is_present("PRINT_FULL"),
),
("mul", Some(command)) => mul_operation(
&command
.values_of("MULTIPLICAND")
.unwrap()
.collect::<Vec<&str>>()
.join(" "),
command
.value_of("MULTIPLIER")
.unwrap()
.parse::<usize>()
.unwrap(),
&custom_currency_regex,
&currencies,
matches.is_present("PRINT_FULL"),
),
("div", Some(command)) => div_operation(
&command
.values_of("DIVIDEND")
.unwrap()
.collect::<Vec<&str>>()
.join(" "),
command
.value_of("DIVISOR")
.unwrap()
.parse::<usize>()
.unwrap(),
&custom_currency_regex,
&currencies,
matches.is_present("PRINT_FULL"),
),
("copper", Some(command)) => copper_operation(
&command
.values_of("VALUE")
.unwrap()
.collect::<Vec<&str>>()
.join(" "),
&custom_currency_regex,
&currencies,
),
_ => {
if let Some(values) = matches.values_of("VALUE") {
default_operation(
&values.collect::<Vec<&str>>().join(" "),
&currencies,
matches.is_present("PRINT_FULL"),
)
} else {
eprintln!("Sterling Error: 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);
}
};
}
};
let converted_currencies = convert::convert_currencies(total_copper_value, currencies);
let display_strings: Vec<String> =
create_display_strings(converted_currencies, matches.is_present("PRINT_FULL"));
println!("{}", (&display_strings).join(", "));
} else {
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 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()
}
fn get_copper_value(currencies: &[Currency], lhs: &str, rhs: &str) -> (usize, usize) {
let mut rates: HashMap<String, usize> = HashMap::with_capacity(currencies.len());
for currency in currencies {
rates.insert(currency.alias.clone(), currency.rate);
}
let aliases = currencies
.iter()
.cloned()
.map(|c| c.alias)
.fold(String::new(), |group, a| group + &a);
let regex: Regex = Regex::new(&format!("(\\d+)([{}])", aliases)).unwrap();
let left_hand_side: usize = regex.captures_iter(lhs).fold(0, |sum, cap| {
let value: usize = cap[1].parse().unwrap();
let rate: usize = *rates.get(&cap[2]).unwrap();
let product = value * rate;
sum + product
});
let right_hand_side: usize = regex.captures_iter(rhs).fold(0, |sum, cap| {
let value: usize = cap[1].parse().unwrap();
let rate: usize = *rates.get(&cap[2]).unwrap();
let product = value * rate;
sum + product
});
(left_hand_side, right_hand_side)
println!("{}", operation_result);
}