use currency::{self, Currency};
use std::collections::HashMap;
use std::fmt;
use std::error::Error;
const NBSP: char = '\u{a0}';
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum FormatPart {
Amount,
CurrencySymbol,
OptionalMinus,
OptionalMinusOpenParenthesis,
OptionalMinusCloseParenthesis,
NonBreakingSpace,
#[doc(hidden)]
__Nonexhaustive,
}
#[derive(PartialEq, Eq, Clone)]
pub struct FormatSpec {
thousands_separator: char,
decimal_separator: char,
short_currency_symbols: HashMap<Currency, String>,
template: Vec<FormatPart>,
}
impl FormatSpec {
pub fn new(thousands_sep: char, decimal_sep: char, template: Vec<FormatPart>) -> FormatSpec {
FormatSpec {
thousands_separator: thousands_sep,
decimal_separator: decimal_sep,
short_currency_symbols: HashMap::new(),
template: template,
}
}
pub fn with_short_symbol(&self, currency: Currency, symbol: String) -> FormatSpec {
let mut result = self.clone();
result.short_currency_symbols.insert(currency, symbol);
result
}
pub fn display_for<'a, 'b, T: FormattableMoney>(&'a self,
money: &'b T)
-> MoneyDisplay<'b, 'a, T> {
MoneyDisplay {
money: money,
spec: self,
}
}
pub fn parser(&self) -> Parser {
let mut parser = Parser::new(self.thousands_separator, self.decimal_separator, self.template.clone());
for (currency, symbol) in &self.short_currency_symbols {
parser = parser.with_short_symbol(currency.clone(), symbol.clone());
}
parser
}
}
lazy_static!{
static ref STYLE_GENERIC: FormatSpec = FormatSpec::new(
',', '.', vec![FormatPart::OptionalMinus,
FormatPart::Amount,
FormatPart::NonBreakingSpace,
FormatPart::CurrencySymbol]);
static ref STYLE_FRANCE: FormatSpec = FormatSpec::new(
NBSP, ',', vec![FormatPart::OptionalMinus,
FormatPart::Amount,
FormatPart::NonBreakingSpace,
FormatPart::CurrencySymbol])
.with_short_symbol(currency::EUR, String::from("€"));
static ref STYLE_UK: FormatSpec = FormatSpec::new(
',', '.', vec![FormatPart::OptionalMinus, FormatPart::CurrencySymbol, FormatPart::Amount])
.with_short_symbol(currency::GBP, String::from("£"));
static ref STYLE_US: FormatSpec = FormatSpec::new(
',', '.', vec![FormatPart::OptionalMinusOpenParenthesis,
FormatPart::CurrencySymbol,
FormatPart::Amount,
FormatPart::OptionalMinusCloseParenthesis])
.with_short_symbol(currency::USD, String::from("$"));
}
pub fn generic_style() -> &'static FormatSpec {
&*STYLE_GENERIC
}
pub fn france_style() -> &'static FormatSpec {
&*STYLE_FRANCE
}
pub fn uk_style() -> &'static FormatSpec {
&*STYLE_UK
}
pub fn us_style() -> &'static FormatSpec {
&*STYLE_US
}
pub trait FormattableMoney {
fn unformatted_minor_amount(&self) -> String;
fn currency(&self) -> Currency;
}
pub struct MoneyDisplay<'a, 'b, T: 'a + FormattableMoney> {
money: &'a T,
spec: &'b FormatSpec,
}
impl<'a, 'b, T: FormattableMoney> fmt::Display for MoneyDisplay<'a, 'b, T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", format(&self.spec, self.money))
}
}
pub fn format<T: FormattableMoney>(spec: &FormatSpec, money: &T) -> String {
let currency = money.currency();
let unformatted_amount = money.unformatted_minor_amount();
let negative = unformatted_amount.starts_with('-');
let mut result = String::new();
for part in &spec.template {
match *part {
FormatPart::OptionalMinus => {
if negative {
result.push('-');
}
}
FormatPart::OptionalMinusOpenParenthesis => {
if negative {
result.push('(');
}
}
FormatPart::OptionalMinusCloseParenthesis => {
if negative {
result.push(')');
}
}
FormatPart::NonBreakingSpace => {
result.push(NBSP);
}
FormatPart::Amount => {
push_formatted_amount(&mut result,
currency,
if negative {
&unformatted_amount[1..]
} else {
unformatted_amount.as_str()
},
spec);
}
FormatPart::CurrencySymbol => {
result.push_str(spec.short_currency_symbols
.get(¤cy)
.unwrap_or(¤cy.code())
.as_str());
}
ref x => {
panic!("Don't know how to format FormatPart: {:?}", x);
}
}
}
result
}
fn push_formatted_amount(result: &mut String,
currency: Currency,
amount: &str,
spec: &FormatSpec) {
let decimal_places = currency.decimal_places() as usize;
if amount.len() > decimal_places {
let major_len = amount.len() - decimal_places;
let mut index = major_len % 3;
result.push_str(&amount[0..index]);
while index < major_len {
if index != 0 {
result.push(spec.thousands_separator);
}
result.push_str(&amount[index..(index + 3)]);
index += 3;
}
result.push(spec.decimal_separator);
result.push_str(&amount[index..]);
} else {
result.push('0');
result.push(spec.decimal_separator);
for _ in 0..(decimal_places - amount.len()) {
result.push('0');
}
result.push_str(amount);
}
}
#[derive(PartialEq, Eq, Clone)]
pub struct Parser {
thousands_separator: char,
decimal_separator: char,
short_currency_symbols: HashMap<String, Currency>,
template: Vec<FormatPart>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct ParseError {
kind: ParseErrorKind,
pub loc: usize,
}
impl ParseError {
fn new(kind: ParseErrorKind, loc: usize) -> Self {
ParseError { kind: kind, loc: loc }
}
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.description().fmt(f)
}
}
impl Error for ParseError {
fn description(&self) -> &str {
use self::ParseErrorKind::*;
match self.kind {
MisusedThousandsSeparator => "Thousands separator in wrong place",
UnmatchedNegationParen => "Unmatched negation parenthesis",
NonWhitespace => "Expected whitespace",
UnknownCurrencySymbol => "Unknown currency symbol",
UnparseableNumber => "Unparseable number",
ExtraCharacters => "String contains extra characters",
WrongNumberDecimalPlaces => "Wrong number of decimal places for currency",
EmptyInputString => "Empty input string",
FormatSpecMissingCurrencySymbol => "FormatSpec template is missing FormatPart::CurrencySymbol. To parse strings with no currency symbol, try adding the empty string to your FormatSpec as a short symbol",
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum ParseErrorKind {
MisusedThousandsSeparator,
UnmatchedNegationParen,
NonWhitespace,
UnknownCurrencySymbol,
UnparseableNumber,
ExtraCharacters,
WrongNumberDecimalPlaces,
EmptyInputString,
FormatSpecMissingCurrencySymbol,
}
impl Parser {
fn new(thousands_sep: char, decimal_sep: char, template: Vec<FormatPart>) -> Self {
Parser {
thousands_separator: thousands_sep,
decimal_separator: decimal_sep,
short_currency_symbols: HashMap::new(),
template: template,
}
}
pub fn with_short_symbol(&self, currency: Currency, symbol: String) -> Self {
let mut result = self.clone();
result.short_currency_symbols.insert(symbol, currency);
result
}
pub fn parse<T: ParseableMoney>(&self, s: &str) -> Result<T, ParseError> {
use self::ParseErrorKind::*;
let mut negated = false;
let mut currency: Option<Currency> = None;
let mut decimal_places = 0_usize;
let mut minor_amount = String::new();
let mut buffer_pos = 0_usize;
let chars: Vec<char> = s.chars().collect();
if !self.template.contains(&FormatPart::CurrencySymbol) {
return Err(ParseError::new(FormatSpecMissingCurrencySymbol, 0));
}
if chars.is_empty() {
return Err(ParseError::new(EmptyInputString, 0));
}
for item in &self.template {
if buffer_pos >= chars.len() {
break;
}
let mut c = chars[buffer_pos];
match item {
&FormatPart::OptionalMinus => {
if c == '-' {
negated = true;
buffer_pos += 1;
}
},
&FormatPart::OptionalMinusOpenParenthesis => {
if c == '(' {
negated = true;
buffer_pos += 1;
}
},
&FormatPart::CurrencySymbol => {
let start_pos = buffer_pos;
let mut sym = String::new();
while !c.is_whitespace() && !c.is_digit(10)
&& c != '(' && c != ')' && c != '-'
&& c != self.thousands_separator && c != self.decimal_separator {
sym.push(c);
buffer_pos += 1;
if buffer_pos < chars.len() {
c = chars[buffer_pos];
} else {
break;
}
}
currency = self.short_currency_symbols.get(&sym)
.map(|r| r.clone())
.or_else(|| currency::with_code(&sym));
if currency.is_none() {
return Err(ParseError::new(UnknownCurrencySymbol, start_pos));
}
},
&FormatPart::Amount => {
let mut found_decimal = false;
let mut decimal_pos = 0_usize;
loop {
if buffer_pos >= chars.len() {
break;
}
c = chars[buffer_pos];
if c.is_digit(10) {
minor_amount.push(c);
buffer_pos += 1;
} else if !found_decimal
&& (c == self.thousands_separator
|| (c.is_whitespace() && self.thousands_separator.is_whitespace())) {
for i in 1..4 {
if (buffer_pos + i) >= chars.len() {
return Err(ParseError::new(MisusedThousandsSeparator, buffer_pos));
}
c = chars[buffer_pos + i];
if !c.is_digit(10) {
return Err(ParseError::new(MisusedThousandsSeparator, buffer_pos));
}
minor_amount.push(c);
}
buffer_pos += 4;
} else if !found_decimal && c == self.decimal_separator {
found_decimal = true;
decimal_pos = buffer_pos;
buffer_pos += 1;
} else {
break;
}
}
decimal_places = buffer_pos - decimal_pos - 1;
},
&FormatPart::OptionalMinusCloseParenthesis => {
if c == ')' {
if negated {
buffer_pos += 1;
} else {
return Err(ParseError::new(UnmatchedNegationParen, buffer_pos));
}
}
},
&FormatPart::NonBreakingSpace => {
if c.is_whitespace() {
buffer_pos += 1;
} else {
return Err(ParseError::new(NonWhitespace, buffer_pos));
}
},
ref x => {
panic!("Don't know how to parse {:?} yet", x);
},
}
}
if currency.unwrap().decimal_places() as usize != decimal_places {
return Err(ParseError::new(WrongNumberDecimalPlaces, 0));
}
if buffer_pos < chars.len() {
return Err(ParseError::new(ExtraCharacters, buffer_pos));
}
if negated {
minor_amount.insert(0, '-');
}
ParseableMoney::from_unformatted_minor_amount(currency.unwrap(), minor_amount.as_str())
.map_err(|_| ParseError::new(UnparseableNumber, 0))
}
}
pub trait ParseableMoney {
fn from_unformatted_minor_amount(currency: Currency, unformatted_minor_amount: &str)
-> Result<Self, ::std::num::ParseIntError>
where Self: ::std::marker::Sized;
}
#[cfg(test)]
mod tests {
use money::Money;
use currency;
use super::*;
use super::ParseErrorKind;
#[test]
fn positive_us_money_in_us_format() {
assert_eq!("$0.01",
format(us_style(), &Money::of_minor(currency::USD, 1)).as_str());
assert_eq!("$0.10",
format(us_style(), &Money::of_minor(currency::USD, 10)).as_str());
assert_eq!("$1.00",
format(us_style(), &Money::of_minor(currency::USD, 100)).as_str());
assert_eq!("$100.00",
format(us_style(), &Money::of_minor(currency::USD, 100_00)).as_str());
assert_eq!("$12,345,678.90",
format(us_style(), &Money::of_minor(currency::USD, 12_345_678_90)).as_str());
}
#[test]
fn negative_us_money_in_us_format() {
assert_eq!("($0.01)",
format(us_style(), &Money::of_minor(currency::USD, -1)).as_str());
assert_eq!("($1,234.56)",
format(us_style(), &Money::of_minor(currency::USD, -123456)).as_str());
}
#[test]
fn uk_money_in_us_format() {
assert_eq!("GBP1,234.56",
format(us_style(), &Money::of_minor(currency::GBP, 123456)).as_str());
assert_eq!("(GBP1,234.56)",
format(us_style(), &Money::of_minor(currency::GBP, -123456)).as_str());
}
#[test]
fn uk_money_in_uk_format() {
assert_eq!("£1,234.56",
format(uk_style(), &Money::of_minor(currency::GBP, 123456)).as_str());
assert_eq!("-£1,234.56",
format(uk_style(), &Money::of_minor(currency::GBP, -123456)).as_str());
}
#[test]
fn us_money_in_uk_format() {
assert_eq!("USD12,345,678.90",
format(uk_style(), &Money::of_minor(currency::USD, 1234567890)).as_str());
assert_eq!("-USD12,345,678.90",
format(uk_style(), &Money::of_minor(currency::USD, -1234567890)).as_str());
}
#[test]
fn fr_money_in_fr_format() {
assert_eq!("12\u{a0}345\u{a0}678,90\u{a0}€",
format(france_style(), &Money::of_minor(currency::EUR, 1234567890)).as_str());
assert_eq!("-12\u{a0}345\u{a0}678,90\u{a0}€",
format(france_style(), &Money::of_minor(currency::EUR, -1234567890)).as_str());
}
#[test]
fn us_money_in_fr_format() {
assert_eq!("12\u{a0}345\u{a0}678,90\u{a0}USD",
format(france_style(), &Money::of_minor(currency::USD, 1234567890)).as_str());
assert_eq!("-12\u{a0}345\u{a0}678,90\u{a0}USD",
format(france_style(), &Money::of_minor(currency::USD, -1234567890)).as_str());
}
#[test]
fn parse_us_style_usd() {
let parser = us_style().parser();
assert_eq!(Ok(Money::of_minor(currency::USD, 1)), parser.parse("$0.01"));
assert_eq!(Ok(Money::of_minor(currency::USD, 10)), parser.parse("$0.10"));
assert_eq!(Ok(Money::of_minor(currency::USD, 1_00)), parser.parse("$1.00"));
assert_eq!(Ok(Money::of_minor(currency::USD, 12_345_678_90)), parser.parse("$12,345,678.90"));
assert_eq!(Ok(Money::of_minor(currency::USD, -1_00)), parser.parse("($1.00)"));
}
#[test]
fn parse_us_style_gbp() {
let parser = us_style().parser();
assert_eq!(Ok(Money::of_minor(currency::GBP, 1)), parser.parse("GBP0.01"));
assert_eq!(Ok(Money::of_minor(currency::GBP, 10)), parser.parse("GBP0.10"));
assert_eq!(Ok(Money::of_minor(currency::GBP, 1_00)), parser.parse("GBP1.00"));
assert_eq!(Ok(Money::of_minor(currency::GBP, 12_345_678_90)), parser.parse("GBP12,345,678.90"));
assert_eq!(Ok(Money::of_minor(currency::GBP, -1_00)), parser.parse("(GBP1.00)"));
}
#[test]
fn parse_uk_style_usd() {
let parser = uk_style().parser();
assert_eq!(Ok(Money::of_minor(currency::USD, 1)), parser.parse("USD0.01"));
assert_eq!(Ok(Money::of_minor(currency::USD, 10)), parser.parse("USD0.10"));
assert_eq!(Ok(Money::of_minor(currency::USD, 1_00)), parser.parse("USD1.00"));
assert_eq!(Ok(Money::of_minor(currency::USD, 12_345_678_90)), parser.parse("USD12,345,678.90"));
assert_eq!(Ok(Money::of_minor(currency::USD, -1_00)), parser.parse("-USD1.00"));
}
#[test]
fn parse_uk_style_gbp() {
let parser = uk_style().parser();
assert_eq!(Ok(Money::of_minor(currency::GBP, 1)), parser.parse("£0.01"));
assert_eq!(Ok(Money::of_minor(currency::GBP, 10)), parser.parse("£0.10"));
assert_eq!(Ok(Money::of_minor(currency::GBP, 1_00)), parser.parse("£1.00"));
assert_eq!(Ok(Money::of_minor(currency::GBP, 12_345_678_90)), parser.parse("£12,345,678.90"));
assert_eq!(Ok(Money::of_minor(currency::GBP, -1_00)), parser.parse("-£1.00"));
}
#[test]
fn parse_france_style_usd() {
let parser = france_style().parser();
assert_eq!(Ok(Money::of_minor(currency::USD, 1)), parser.parse("0,01\u{a0}USD"));
assert_eq!(Ok(Money::of_minor(currency::USD, 10)), parser.parse("0,10\u{a0}USD"));
assert_eq!(Ok(Money::of_minor(currency::USD, 1_00)), parser.parse("1,00\u{a0}USD"));
assert_eq!(Ok(Money::of_minor(currency::USD, 12_345_678_90)), parser.parse("12\u{a0}345\u{a0}678,90\u{a0}USD"));
assert_eq!(Ok(Money::of_minor(currency::USD, -1_00)), parser.parse("-1,00\u{a0}USD"));
assert_eq!(Ok(Money::of_minor(currency::USD, 1_00)), parser.parse("1,00 USD"));
assert_eq!(Ok(Money::of_minor(currency::USD, 12_345_678_90)), parser.parse("12 345 678,90 USD"));
}
#[test]
fn parse_france_style_eur() {
let parser = france_style().parser();
assert_eq!(Ok(Money::of_minor(currency::EUR, 1)), parser.parse("0,01\u{a0}€"));
assert_eq!(Ok(Money::of_minor(currency::EUR, 10)), parser.parse("0,10\u{a0}€"));
assert_eq!(Ok(Money::of_minor(currency::EUR, 1_00)), parser.parse("1,00\u{a0}€"));
assert_eq!(Ok(Money::of_minor(currency::EUR, 12_345_678_90)), parser.parse("12\u{a0}345\u{a0}678,90\u{a0}€"));
assert_eq!(Ok(Money::of_minor(currency::EUR, -1_00)), parser.parse("-1,00\u{a0}€"));
assert_eq!(Ok(Money::of_minor(currency::EUR, 1_00)), parser.parse("1,00 €"));
assert_eq!(Ok(Money::of_minor(currency::EUR, 12_345_678_90)), parser.parse("12 345 678,90 €"));
}
#[test]
fn parse_with_no_currency_symbol_using_custom_spec() {
let parser = FormatSpec::new(',', '.', vec![FormatPart::OptionalMinus, FormatPart::CurrencySymbol, FormatPart::Amount])
.with_short_symbol(currency::USD, "".to_string())
.parser();
assert_eq!(Ok(Money::of_minor(currency::USD, 1)), parser.parse("0.01"));
assert_eq!(Ok(Money::of_minor(currency::USD, 1_234_56)), parser.parse("1,234.56"));
assert_eq!(Ok(Money::of_minor(currency::USD, -15_08)), parser.parse("-15.08"));
}
#[test]
fn parse_with_spec_having_currency_symbol_before_optional_minus() {
let parser = FormatSpec::new(',', '.', vec![FormatPart::CurrencySymbol, FormatPart::OptionalMinus, FormatPart::Amount])
.with_short_symbol(currency::USD, "$".to_string())
.parser();
assert_eq!(Ok(Money::of_minor(currency::USD, 1)), parser.parse("$0.01"));
assert_eq!(Ok(Money::of_minor(currency::USD, 1_234_56)), parser.parse("$1,234.56"));
assert_eq!(Ok(Money::of_minor(currency::USD, -15_08)), parser.parse("$-15.08"));
}
#[test]
fn parsing_failures() {
let us_parser = us_style().parser();
let fr_parser = france_style().parser();
assert_eq!(Err(ParseError::new(ParseErrorKind::UnknownCurrencySymbol, 0)), us_parser.parse::<Money>("garbage"));
assert_eq!(Err(ParseError::new(ParseErrorKind::WrongNumberDecimalPlaces, 0)), us_parser.parse::<Money>("$1.234,56"));
assert_eq!(Err(ParseError::new(ParseErrorKind::ExtraCharacters, 5)), us_parser.parse::<Money>("$1.00 "));
assert_eq!(Err(ParseError::new(ParseErrorKind::WrongNumberDecimalPlaces, 0)), us_parser.parse::<Money>("$1.0000"));
assert_eq!(Err(ParseError::new(ParseErrorKind::WrongNumberDecimalPlaces, 0)), us_parser.parse::<Money>("$1.0"));
assert_eq!(Err(ParseError::new(ParseErrorKind::MisusedThousandsSeparator, 2)), us_parser.parse::<Money>("$1,23.45"));
assert_eq!(Err(ParseError::new(ParseErrorKind::UnmatchedNegationParen, 5)), us_parser.parse::<Money>("$1.00)"));
assert_eq!(Err(ParseError::new(ParseErrorKind::NonWhitespace, 4)), fr_parser.parse::<Money>("1,00€"));
assert_eq!(Err(ParseError::new(ParseErrorKind::EmptyInputString, 0)), us_parser.parse::<Money>(""));
let bad_parser = FormatSpec::new(',', '.', vec![FormatPart::OptionalMinus, FormatPart::Amount]).parser();
assert_eq!(Err(ParseError::new(ParseErrorKind::FormatSpecMissingCurrencySymbol, 0)), bad_parser.parse::<Money>("1.00"));
}
#[test]
fn should_be_parsing_failures() {
let parser = us_style().parser();
assert_eq!(Ok(Money::of_minor(currency::USD, -1_00)), parser.parse("($1.00"));
assert_eq!(Ok(Money::of_minor(currency::USD, 100_000_00)), parser.parse("$1,00000.00"));
}
}