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:
parent
f7a6ec53a0
commit
90a3f41c2d
15 changed files with 706 additions and 363 deletions
|
@ -9,16 +9,4 @@ cargo:test:
|
||||||
- master
|
- master
|
||||||
script:
|
script:
|
||||||
- cargo test --verbose
|
- cargo test --verbose
|
||||||
|
- cargo bench
|
||||||
cargo:build:
|
|
||||||
stage: deploy
|
|
||||||
only:
|
|
||||||
- master
|
|
||||||
script:
|
|
||||||
- cargo build --release
|
|
||||||
cache:
|
|
||||||
paths:
|
|
||||||
- target/
|
|
||||||
artifacts:
|
|
||||||
paths:
|
|
||||||
- target/release/sterling
|
|
73
.travis.yml
73
.travis.yml
|
@ -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
|
language: rust
|
||||||
os:
|
services: docker
|
||||||
- osx
|
sudo: required
|
||||||
branches:
|
|
||||||
only:
|
env:
|
||||||
- master
|
global:
|
||||||
rust:
|
# TODO Update this to match the name of your project.
|
||||||
- stable
|
- 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:
|
script:
|
||||||
- cargo test --verbose
|
- bash ci/script.sh
|
||||||
- cargo build --release
|
|
||||||
|
after_script: set +e
|
||||||
|
|
||||||
before_deploy:
|
before_deploy:
|
||||||
- git config --local user.name "Zachary Dziura"
|
- sh ci/before_deploy.sh
|
||||||
- git config --local user.email "zcdziura@gmail.com"
|
|
||||||
- git tag "$(date +'%Y%m%d%H%M%S')-$(git log --format=%h -1)-osx"
|
|
||||||
deploy:
|
deploy:
|
||||||
provider: releases
|
|
||||||
api_key:
|
api_key:
|
||||||
secure: "NVH5d3CH0QUyFSu0MbeB3WvSo52qwjxH98wIL7kieD/kbTrTzTCbTWiTCV+/OM41nWOdxTy9T5TLqDsrh4k4xCkb4VbKTcsfIpBaQqwMvT6Co/GgLr4xSiHBI/ENBVwbnyDavMxh/E5AAAPF/HgGci2tEqzNuu9V7jon6uhb8+WovbfZeEA4tSNLsWV5g3MwssMfdaWzDPTHsiWXFPn6AVhkmy4fKAIHoUtp37A7bqx1hGPpFD3OGYN1oDxtJK5jRBSXegyWh08RQkLQ74PJTWD6Xw+Hvp1ewP1vitP69VJgsBC496jPasqAEOVeD3KogtcmBEyaIG+I5LZWLTibs41qF83cxJDdWxw69H827IXSQobM+7Sc51chWJR0H3OA1yDPQvorI1C17zvXd4wPpDfSUeY5ZqAplnYMOxk3jDbbX099bEyRE/skWHRaqL99fV7i5bO3aHDFP/BDjp03hnzpvfKs9zm05e87LStriNYQ5NsCPkdX+W18Q15DLhS2D9cp37PPAUA5jLNUFiEY5x9fwl5XEpefBqrqmE8qbmkc9GTr3MZikmTfB51Nx5NvkybCTKhMoKw5AhNLmw0fnkaqxrei7Uif7WqxTkngJep6VLidmt2pRJ9Qj3AWOXsLZJPm0ZQuo71dWC049EeEVtfQkyz/9K2J+iNVRgdiEeg="
|
secure: "NVH5d3CH0QUyFSu0MbeB3WvSo52qwjxH98wIL7kieD/kbTrTzTCbTWiTCV+/OM41nWOdxTy9T5TLqDsrh4k4xCkb4VbKTcsfIpBaQqwMvT6Co/GgLr4xSiHBI/ENBVwbnyDavMxh/E5AAAPF/HgGci2tEqzNuu9V7jon6uhb8+WovbfZeEA4tSNLsWV5g3MwssMfdaWzDPTHsiWXFPn6AVhkmy4fKAIHoUtp37A7bqx1hGPpFD3OGYN1oDxtJK5jRBSXegyWh08RQkLQ74PJTWD6Xw+Hvp1ewP1vitP69VJgsBC496jPasqAEOVeD3KogtcmBEyaIG+I5LZWLTibs41qF83cxJDdWxw69H827IXSQobM+7Sc51chWJR0H3OA1yDPQvorI1C17zvXd4wPpDfSUeY5ZqAplnYMOxk3jDbbX099bEyRE/skWHRaqL99fV7i5bO3aHDFP/BDjp03hnzpvfKs9zm05e87LStriNYQ5NsCPkdX+W18Q15DLhS2D9cp37PPAUA5jLNUFiEY5x9fwl5XEpefBqrqmE8qbmkc9GTr3MZikmTfB51Nx5NvkybCTKhMoKw5AhNLmw0fnkaqxrei7Uif7WqxTkngJep6VLidmt2pRJ9Qj3AWOXsLZJPm0ZQuo71dWC049EeEVtfQkyz/9K2J+iNVRgdiEeg="
|
||||||
file_glob: true
|
file_glob: true
|
||||||
file: target/release/sterling
|
file: $CRATE_NAME-$TRAVIS_TAG-$TARGET.*
|
||||||
skip_cleanup: true
|
|
||||||
on:
|
on:
|
||||||
repo: zcdziura/sterling
|
tags: true
|
||||||
branch: master
|
provider: releases
|
||||||
addons:
|
skip_cleanup: true
|
||||||
artifacts: 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
|
21
Cargo.toml
21
Cargo.toml
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "sterling"
|
name = "sterling"
|
||||||
version = "0.3.0"
|
version = "1.0.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"
|
||||||
|
@ -8,14 +8,31 @@ license = "Unlicense/MIT"
|
||||||
repository = "https://gitlab.com/zcdziura/sterling"
|
repository = "https://gitlab.com/zcdziura/sterling"
|
||||||
keywords = ["dnd", "coins", "converter", "currency", "5e"]
|
keywords = ["dnd", "coins", "converter", "currency", "5e"]
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "sterling_ops"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "sterling"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = "2.31"
|
clap = "2.32"
|
||||||
|
lazysort = "0.2"
|
||||||
lazy_static = "1.0"
|
lazy_static = "1.0"
|
||||||
regex = "1.0"
|
regex = "1.0"
|
||||||
|
separator = "0.3"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
serde_yaml = "0.7"
|
serde_yaml = "0.7"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
criterion = "0.2"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "bench"
|
||||||
|
harness = false
|
|
@ -92,7 +92,6 @@ within my own campaign!
|
||||||
name: "guilder"
|
name: "guilder"
|
||||||
rate: 896
|
rate: 896
|
||||||
alias: "g"
|
alias: "g"
|
||||||
plural: "guilders"
|
|
||||||
-
|
-
|
||||||
name: "shilling"
|
name: "shilling"
|
||||||
rate: 32
|
rate: 32
|
||||||
|
|
73
appveyor.yml
73
appveyor.yml
|
@ -1,29 +1,60 @@
|
||||||
os: Visual Studio 2015
|
# Based on the "trust" template v0.1.2
|
||||||
branches:
|
# https://github.com/japaric/trust/tree/v0.1.2
|
||||||
only:
|
|
||||||
- master
|
|
||||||
environment:
|
environment:
|
||||||
|
global:
|
||||||
|
RUST_VERSION: stable
|
||||||
|
CRATE_NAME: sterling
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
- channel: stable
|
# MinGW
|
||||||
target: x86_64-pc-windows-gnu
|
- TARGET: x86_64-pc-windows-gnu
|
||||||
artifacts:
|
|
||||||
- path: target/release/sterling.exe
|
|
||||||
name: sterling
|
|
||||||
install:
|
install:
|
||||||
- appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe
|
- ps: >-
|
||||||
- rustup-init -yv --default-toolchain %channel% --default-host %target%
|
If ($Env:TARGET -eq 'x86_64-pc-windows-gnu') {
|
||||||
- set PATH=%PATH%;%USERPROFILE%\.cargo\bin
|
$Env:PATH += ';C:\msys64\mingw64\bin'
|
||||||
- rustc -vV
|
} ElseIf ($Env:TARGET -eq 'i686-pc-windows-gnu') {
|
||||||
- cargo -vV
|
$Env:PATH += ';C:\msys64\mingw32\bin'
|
||||||
build: false
|
}
|
||||||
|
- 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:
|
test_script:
|
||||||
- cargo test --verbose
|
# we don't run the "test phase" when doing deploys
|
||||||
- cargo build --release
|
- if [%APPVEYOR_REPO_TAG%]==[true] (
|
||||||
|
cargo test --target %TARGET% &&
|
||||||
|
cargo build --target %TARGET% --release
|
||||||
|
)
|
||||||
|
|
||||||
|
before_deploy:
|
||||||
|
- ps: ci\before_deploy.ps1
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
provider: GitHub
|
artifact: /.*\.zip/
|
||||||
description: ''
|
|
||||||
auth_token:
|
auth_token:
|
||||||
secure: bvA/4J1T0h65ur6tsg6k/wlZFjP3qr2QsyRsmGMEmm7DOF61xmzTnjuBcPjQYrba
|
secure: bvA/4J1T0h65ur6tsg6k/wlZFjP3qr2QsyRsmGMEmm7DOF61xmzTnjuBcPjQYrba
|
||||||
artifact: target/release/sterling.exe
|
|
||||||
on:
|
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
27
benches/bench.rs
Normal 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
22
ci/before_deploy.ps1
Normal 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
32
ci/before_deploy.sh
Normal 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
14
ci/install.sh
Normal 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
18
ci/script.sh
Normal 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
|
|
@ -10,16 +10,47 @@ use currency::Currency;
|
||||||
|
|
||||||
pub fn load_config(filename: &str) -> Result<Vec<Currency>, ConfigError> {
|
pub fn load_config(filename: &str) -> Result<Vec<Currency>, ConfigError> {
|
||||||
let config_file = File::open(filename)?;
|
let config_file = File::open(filename)?;
|
||||||
let mut configs: Vec<Currency> = serde_yaml::from_reader(BufReader::new(config_file))?;
|
let config: Vec<Currency> = serde_yaml::from_reader(BufReader::new(config_file))?;
|
||||||
configs.sort_by(|a, b| b.cmp(a));
|
|
||||||
|
|
||||||
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![
|
vec![
|
||||||
Currency::new("platinum", 1000000, "p", None, None),
|
Currency::new("platinum", 1000, "p", None, None),
|
||||||
Currency::new("gold", 10000, "g", 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("silver", 100, "s", None, None),
|
||||||
Currency::new("copper", 1, "c", None, None),
|
Currency::new("copper", 1, "c", None, None),
|
||||||
]
|
]
|
||||||
|
|
208
src/convert.rs
208
src/convert.rs
|
@ -1,155 +1,109 @@
|
||||||
use currency::Currency;
|
use std::collections::HashMap;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
use separator::Separatable;
|
||||||
|
|
||||||
pub fn convert_to_copper(amount: usize, coin_denomination: &str) -> usize {
|
use currency::Currency;
|
||||||
match coin_denomination {
|
use lazysort::Sorted;
|
||||||
"p" => amount * 1000,
|
|
||||||
"g" => amount * 100,
|
pub fn calculate_total_copper_value(
|
||||||
"e" => amount * 50,
|
values: &str,
|
||||||
"s" => amount * 10,
|
currency_regex: &Regex,
|
||||||
"c" => amount,
|
rates: HashMap<String, usize>,
|
||||||
_ => unreachable!("Invalid coin type; must be a valid coin found in the PHB."),
|
) -> 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> {
|
pub fn exchange_currencies(
|
||||||
let regex: Regex = Regex::new(r"(\d+)([cegps])").unwrap();
|
copper_value: usize,
|
||||||
for coin in coins.iter() {
|
currencies: &[Currency],
|
||||||
if let None = regex.captures(coin) {
|
print_full_name: bool,
|
||||||
return Err(
|
) -> Vec<String> {
|
||||||
"Sterling Error: Invalid coin value. Make sure all coins are denoted properly.",
|
let mut val = copper_value;
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
currencies
|
||||||
.iter_mut()
|
.iter()
|
||||||
|
.sorted()
|
||||||
|
.filter(|currency| currency.optional.unwrap_or(true))
|
||||||
.map(|currency| {
|
.map(|currency| {
|
||||||
let value = val / currency.rate;
|
let value = val / currency.rate;
|
||||||
val = val % currency.rate;
|
val = val % currency.rate;
|
||||||
|
|
||||||
currency.with_value(value)
|
(
|
||||||
|
value,
|
||||||
|
if print_full_name {
|
||||||
|
if value > 1 {
|
||||||
|
match (¤cy).plural {
|
||||||
|
Some(ref plural) => String::from_str(plural).unwrap(),
|
||||||
|
None => format!("{}s", ¤cy.name),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String::from_str(¤cy.name).unwrap()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String::from_str(¤cy.alias).unwrap()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.filter(|tuple| tuple.0 > 0)
|
||||||
|
.map(|tuple| {
|
||||||
|
format!(
|
||||||
|
"{}{}{}",
|
||||||
|
tuple.0.separated_string(),
|
||||||
|
if tuple.1.len() > 1 { " " } else { "" },
|
||||||
|
tuple.1
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use convert::*;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use super::{calculate_total_copper_value, exchange_currencies};
|
||||||
use currency::Currency;
|
use currency::Currency;
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref STANDARD_CURRENCIES: [Currency; 4] = [
|
static ref STANDARD_CURRENCIES: Vec<Currency> = vec![
|
||||||
Currency::new("platinum", 1000000, "p", None, None),
|
Currency::new("guilder", 10_000, "g", None, None),
|
||||||
Currency::new("gold", 10000, "g", None, None),
|
Currency::new("shilling", 100, "s", Some("sterling".to_owned()), None),
|
||||||
Currency::new("silver", 100, "s", None, None),
|
Currency::new("penny", 1, "p", Some("pence".to_owned()), None),
|
||||||
Currency::new("copper", 1, "c", None, None),
|
|
||||||
];
|
];
|
||||||
}
|
static ref CURRENCY_REGEX: Regex = Regex::new(r"(\d+)([Ngsp])").unwrap();
|
||||||
|
|
||||||
#[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]
|
#[test]
|
||||||
fn test_calculate_total_copper_value() {
|
fn test_calculate_total_copper_value() {
|
||||||
let values = vec!["1p", "1g", "1e", "1s", "1c"];
|
let rates: HashMap<String, usize> = vec![
|
||||||
assert_eq!(1161, calculate_total_copper_value(values).unwrap());
|
("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]
|
#[test]
|
||||||
#[should_panic]
|
fn test_exchange_currencies() {
|
||||||
fn test_calculate_total_copper_value_bad_inputs() {
|
let result = vec!["1g".to_owned(), "1s".to_owned(), "1p".to_owned()];
|
||||||
let values = vec!["1p", "1g", "1f", "1s", "1c"];
|
assert_eq!(
|
||||||
assert_eq!(1161, calculate_total_copper_value(values).unwrap());
|
result,
|
||||||
}
|
exchange_currencies(10101, &STANDARD_CURRENCIES, false)
|
||||||
|
);
|
||||||
#[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()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ use std::cmp::Ordering;
|
||||||
pub struct Currency {
|
pub struct Currency {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub rate: usize,
|
pub rate: usize,
|
||||||
pub value: Option<usize>,
|
|
||||||
pub alias: String,
|
pub alias: String,
|
||||||
pub plural: Option<String>,
|
pub plural: Option<String>,
|
||||||
pub optional: Option<bool>,
|
pub optional: Option<bool>,
|
||||||
|
@ -21,54 +20,23 @@ impl Currency {
|
||||||
Currency {
|
Currency {
|
||||||
name: name.to_owned(),
|
name: name.to_owned(),
|
||||||
rate,
|
rate,
|
||||||
value: None,
|
|
||||||
alias: alias.to_owned(),
|
alias: alias.to_owned(),
|
||||||
plural,
|
plural,
|
||||||
optional,
|
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 {
|
pub fn is_optional(&self) -> bool {
|
||||||
match self.optional {
|
match self.optional {
|
||||||
Some(optional) => optional,
|
Some(optional) => optional,
|
||||||
None => false,
|
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 {
|
impl Ord for Currency {
|
||||||
fn cmp(&self, other: &Currency) -> Ordering {
|
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 {
|
impl PartialEq for Currency {
|
||||||
fn eq(&self, other: &Currency) -> bool {
|
fn eq(&self, other: &Currency) -> bool {
|
||||||
self.value == other.value
|
self.rate == other.rate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
244
src/lib.rs
Normal file
244
src/lib.rs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
239
src/main.rs
239
src/main.rs
|
@ -1,45 +1,46 @@
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate clap;
|
extern crate clap;
|
||||||
#[allow(unused_imports)]
|
|
||||||
#[macro_use]
|
|
||||||
extern crate lazy_static;
|
|
||||||
extern crate regex;
|
extern crate regex;
|
||||||
extern crate serde;
|
extern crate sterling_ops;
|
||||||
#[macro_use]
|
|
||||||
extern crate serde_derive;
|
|
||||||
extern crate serde_yaml;
|
|
||||||
|
|
||||||
mod config;
|
|
||||||
mod convert;
|
|
||||||
mod currency;
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::io::ErrorKind;
|
|
||||||
use std::process;
|
use std::process;
|
||||||
|
|
||||||
use config::ConfigError;
|
|
||||||
use currency::Currency;
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
use sterling_ops::config;
|
||||||
|
use sterling_ops::currency::Currency;
|
||||||
|
use sterling_ops::*;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let app = clap_app!(sterling =>
|
let app = clap_app!(sterling =>
|
||||||
(version: env!("CARGO_PKG_VERSION"))
|
(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 their full name, rather than with their alias")
|
||||||
(@arg OPTIONAL: -o --optional "Include currencies marked as optional when converting.")
|
(@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 =>
|
(@subcommand add =>
|
||||||
(about: "Add two currency amounts together; uses the currencies defined in your config file")
|
(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 AUGEND: +required "The augend of the addition function")
|
||||||
(@arg ADDEND: +required "The addend of the addition function; i.e. the right side")
|
(@arg ADDEND: +required "The addend of the addition function")
|
||||||
(@arg PRINT_FULL: -f --full "Print currencies with full name, rather than with alias.")
|
|
||||||
)
|
)
|
||||||
(@subcommand sub =>
|
(@subcommand sub =>
|
||||||
(about: "Subtract two currency amounts from one another; uses the currencies defined in your config file")
|
(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 MINUEND: +required "The minuend of the subtraction function")
|
||||||
(@arg SUBTRAHEND: +required "The subtrahend of the subtraction function; i.e. the right side")
|
(@arg SUBTRAHEND: +required "The subtrahend of the subtraction function")
|
||||||
(@arg PRINT_FULL: -f --full "Print currencies with full name, rather than with alias.")
|
)
|
||||||
|
(@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> =
|
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
|
Ok(currencies) => currencies
|
||||||
.iter()
|
.into_iter()
|
||||||
.filter(|c| {
|
.filter(|c| {
|
||||||
let has_add_subcommand = match matches.subcommand_matches("add") {
|
let is_sub_command = match matches.subcommand_name() {
|
||||||
Some(_) => true,
|
Some(_) => true,
|
||||||
None => false,
|
None => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
if has_add_subcommand {
|
if is_sub_command {
|
||||||
true
|
true
|
||||||
} else if !matches.is_present("OPTIONAL") {
|
|
||||||
!c.is_optional()
|
|
||||||
} else {
|
} else {
|
||||||
true
|
(!matches.is_present("OPTIONAL") && !c.is_optional())
|
||||||
|
|| matches.is_present("OPTIONAL")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.cloned()
|
|
||||||
.collect(),
|
.collect(),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
eprintln!("{}", error);
|
eprintln!("{}", error);
|
||||||
|
@ -75,117 +74,81 @@ fn main() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(matches) = matches.subcommand_matches("add") {
|
let custom_currency_regex: Regex = Regex::new(&format!(
|
||||||
let (lhs, rhs) = get_copper_value(
|
"(\\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,
|
||||||
¤cies,
|
¤cies,
|
||||||
matches.value_of("AUGEND").unwrap(),
|
matches.is_present("PRINT_FULL"),
|
||||||
matches.value_of("ADDEND").unwrap(),
|
),
|
||||||
);
|
("sub", Some(command)) => sub_operation(
|
||||||
|
command.value_of("MINUEND").unwrap(),
|
||||||
let converted_currencies = convert::convert_currencies(lhs + rhs, currencies);
|
command.value_of("SUBTRAHEND").unwrap(),
|
||||||
let display_strings: Vec<String> =
|
&custom_currency_regex,
|
||||||
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,
|
¤cies,
|
||||||
matches.value_of("MINUEND").unwrap(),
|
matches.is_present("PRINT_FULL"),
|
||||||
matches.value_of("SUBTRAHEND").unwrap(),
|
),
|
||||||
);
|
("mul", Some(command)) => mul_operation(
|
||||||
|
&command
|
||||||
let difference = if lhs > rhs { lhs - rhs } else { rhs - lhs };
|
.values_of("MULTIPLICAND")
|
||||||
|
.unwrap()
|
||||||
let converted_currencies = convert::convert_currencies(difference, currencies);
|
.collect::<Vec<&str>>()
|
||||||
let display_strings: Vec<String> =
|
.join(" "),
|
||||||
create_display_strings(converted_currencies, matches.is_present("PRINT_FULL"));
|
command
|
||||||
|
.value_of("MULTIPLIER")
|
||||||
println!("{}", (&display_strings).join(", "));
|
.unwrap()
|
||||||
} else if let Some(values) = matches.values_of("VALUE") {
|
.parse::<usize>()
|
||||||
let coins: Vec<&str> = values.collect();
|
.unwrap(),
|
||||||
let total_copper_value = match convert::calculate_total_copper_value(coins) {
|
&custom_currency_regex,
|
||||||
Ok(total_copper_value) => total_copper_value,
|
¤cies,
|
||||||
Err(err) => {
|
matches.is_present("PRINT_FULL"),
|
||||||
eprintln!("{}", err);
|
),
|
||||||
|
("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,
|
||||||
|
¤cies,
|
||||||
|
matches.is_present("PRINT_FULL"),
|
||||||
|
),
|
||||||
|
("copper", Some(command)) => copper_operation(
|
||||||
|
&command
|
||||||
|
.values_of("VALUE")
|
||||||
|
.unwrap()
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.join(" "),
|
||||||
|
&custom_currency_regex,
|
||||||
|
¤cies,
|
||||||
|
),
|
||||||
|
_ => {
|
||||||
|
if let Some(values) = matches.values_of("VALUE") {
|
||||||
|
default_operation(
|
||||||
|
&values.collect::<Vec<&str>>().join(" "),
|
||||||
|
¤cies,
|
||||||
|
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);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let converted_currencies = convert::convert_currencies(total_copper_value, currencies);
|
println!("{}", operation_result);
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue