Adding international currency support

master
Mike Mattozzi 2013-09-28 17:17:13 -04:00
parent 3def62f15b
commit 2935964d64
3 changed files with 381 additions and 23 deletions

View File

@ -48,6 +48,7 @@ Games::Sudoku::Component = 0.02
Data::RandomPerson = 0.4
URI::Escape = 3.31
Lingua::EN::Words2Nums = 0
Locale::Currency::Format = 1.30
[Prereqs / TestRequires]
Test::More = 0.98

View File

@ -2,6 +2,7 @@ package DDG::Goodie::Loan;
# ABSTRACT: Calculate monthly payment and total interest payment for a conventional mortgage loan
use DDG::Goodie;
use Locale::Currency::Format;
triggers start => 'loan';
@ -26,24 +27,76 @@ sub loan_monthly_payment {
return ($r / (1 - (1 + $r)**(-1 * $n))) * $p;
}
# A map of country code to currency code, filled in below
my %country_to_currency;
# A map of currency symbol to currency code, filled in below
my %symbol_to_currency = ( );
# From the symbol used, guess the currency (this will override any location data that comes in with the user).
# This is pretty imprecise. Assume USD if a $ is used. More assumptions can be added here later to priorize
# currency guessing. Perhaps location data can be used to break ties.
sub convert_symbol_to_currency {
my $symbol = shift;
if ($symbol eq "\$") {
return "USD";
} else {
return $symbol_to_currency{$symbol};
}
}
# Given the country code and currency formatting rules, the input can be made ready to convert
# to a useable number. Examples:
# In USD: 400,000 => 400000
# In EUR: 400.000,00 => 400000.00
sub normalize_formatted_currency_string {
my ($str, $currency_code) = @_;
my $thousands_separator = thousands_separator($currency_code);
my $decimal_separator = decimal_separator($currency_code);
$str =~ s/\Q$thousands_separator//g;
if ($decimal_separator ne ".") {
$str =~ s/\Q$decimal_separator/\./g;
}
return $str;
}
handle remainder => sub {
my $query = $_;
# At a minimum, query should contain some amount of money and a percent interest rate
if ($query =~ /^\$?(\d+)\s(at\s)?(\d+.?\d*)%/) {
my $principal = $1;
my $rate = $3;
if ($query =~ /^(\p{Currency_Symbol})?([\d\.,]+)\s(at\s)?(\d+.?\d*)%/) {
my $symbol = $1;
my $principal = $2;
my $rate = $4;
my $downpayment = 0;
my $years = 30;
# Apply localization, default to US if unknown
my $currency_code = "USD";
if (defined $symbol) {
$currency_code = convert_symbol_to_currency($symbol);
} elsif (defined $loc->country_code) {
$currency_code = $country_to_currency{$loc->country_code} || $country_to_currency{"us"};
$symbol = currency_symbol($currency_code);
}
# Given the country code and currency formatting rules, the input can be made ready to convert
# to a useable number.
$principal = normalize_formatted_currency_string($principal, $currency_code);
# Check if query contains downpayment information
if ($query =~ /\$?(\d+)(%)? down/) {
my $downpaymentIsInDollars = ! (defined $2);
my $downpaymentNoUnits = $1;
if ($downpaymentIsInDollars) {
$downpayment = $downpaymentNoUnits;
if ($query =~ /(\p{Currency_Symbol})?(\d+)(%)? down/) {
my $downpayment_is_in_cash = ! (defined $3);
my $downpayment_without_units = $2;
if ($downpayment_is_in_cash) {
# Downpayment expresses in an amount of currency
$downpayment = normalize_formatted_currency_string($downpayment_without_units, $currency_code);
} else {
$downpayment = $principal * .01 * $downpaymentNoUnits;
# Downpayment expressed as a percentage of principal
$downpayment = $principal * .01 * $downpayment_without_units;
}
}
@ -52,15 +105,281 @@ handle remainder => sub {
$years = $1;
}
my $loanAmt = $principal - $downpayment;
my $monthlyPayment = loan_monthly_payment($loanAmt, $rate / 12 * .01, $years * 12);
my $totalInterest = ($monthlyPayment * 12 * $years) - $loanAmt;
my $loan_amount = $principal - $downpayment;
my $monthly_payment = loan_monthly_payment($loan_amount, $rate / 12 * .01, $years * 12);
my $total_interest = ($monthly_payment * 12 * $years) - $loan_amount;
return "Monthly Payment is \$" . sprintf("%.2f", $monthlyPayment) .
" for $years years. Total interest paid is \$" . sprintf("%.2f", $totalInterest);
return "Monthly Payment is " . currency_format($currency_code, $monthly_payment, FMT_SYMBOL) .
" for $years years. Total interest paid is " .
currency_format($currency_code, $total_interest, FMT_SYMBOL);
}
return;
};
# A map of 2 letter country code to 3 letter currency code. Copied from Locale::Object perl module
# (http://search.cpan.org/~jrobinson/Locale-Object/) which carries some extra baggage with it, and
# has also not been updated since 2007. If the mapping between any country and currency needs to be
# changed, this is where to change it.
%country_to_currency = (
'ad' => 'EUR',
'ae' => 'AED',
'af' => 'AFA',
'ag' => 'XCD',
'ai' => 'XCD',
'al' => 'ALL',
'am' => 'AMD',
'an' => 'ANG',
'ao' => 'AOA',
'aq' => '000',
'ar' => 'ARS',
'as' => 'USD',
'at' => 'EUR',
'au' => 'AUD',
'aw' => 'AWG',
'az' => 'AZM',
'ba' => 'BAM',
'bb' => 'BBD',
'bd' => 'BDT',
'be' => 'EUR',
'bf' => 'XOF',
'bg' => 'BGL',
'bh' => 'BHD',
'bi' => 'BIF',
'bj' => 'XOF',
'bm' => 'BMD',
'bn' => 'BND',
'bo' => 'BOB',
'br' => 'BRL',
'bs' => 'BSD',
'bt' => 'BTN',
'bv' => 'NOK',
'bw' => 'BWP',
'by' => 'BYR',
'bz' => 'BZD',
'ca' => 'CAD',
'cc' => 'AUD',
'cd' => 'CDF',
'cf' => 'XAF',
'cg' => 'XAF',
'ch' => 'CHF',
'ci' => 'XOF',
'ck' => 'NZD',
'cl' => 'CLP',
'cm' => 'XAF',
'cn' => 'CNY',
'co' => 'COP',
'cr' => 'CRC',
'cu' => 'CUP',
'cv' => 'CVE',
'cx' => 'AUD',
'cy' => 'CYP',
'cz' => 'CZK',
'de' => 'EUR',
'dj' => 'DJF',
'dk' => 'DKK',
'dm' => 'XCD',
'do' => 'DOP',
'dz' => 'DZD',
'ec' => 'ECS',
'ee' => 'EEK',
'eg' => 'EGP',
'eh' => 'MAD',
'er' => 'ERN',
'es' => 'EUR',
'et' => 'ETB',
'fi' => 'EUR',
'fj' => 'FJD',
'fk' => 'FKP',
'fm' => 'USD',
'fo' => 'DKK',
'fr' => 'EUR',
'fx' => 'EUR',
'ga' => 'XAF',
'gb' => 'GBP',
'gd' => 'XCD',
'ge' => 'GEL',
'gf' => 'EUR',
'gh' => 'GHC',
'gi' => 'GIP',
'gl' => 'DKK',
'gm' => 'GMD',
'gn' => 'GNF',
'gp' => 'EUR',
'gq' => 'GQE',
'gr' => 'EUR',
'gs' => 'GBP',
'gt' => 'GTQ',
'gu' => 'USD',
'gw' => 'XOF',
'gy' => 'GYD',
'hk' => 'HKD',
'hm' => 'AUD',
'hn' => 'HNL',
'hr' => 'HRK',
'ht' => 'HTG',
'hu' => 'HUF',
'id' => 'IDR',
'ie' => 'EUR',
'il' => 'ILS',
'in' => 'INR',
'io' => 'GBP',
'iq' => 'IQD',
'ir' => 'IRR',
'is' => 'ISK',
'it' => 'EUR',
'jm' => 'JMD',
'jo' => 'JOD',
'jp' => 'JPY',
'ke' => 'KES',
'kg' => 'KGS',
'kh' => 'KHR',
'ki' => 'AUD',
'km' => 'KMF',
'kn' => 'XCD',
'kp' => 'KPW',
'kr' => 'KRW',
'kw' => 'KWD',
'ky' => 'KYD',
'kz' => 'KZT',
'la' => 'LAK',
'lb' => 'LBP',
'lc' => 'XCD',
'li' => 'CHF',
'lk' => 'LKR',
'lr' => 'LRD',
'ls' => 'LSL',
'lt' => 'LTL',
'lu' => 'EUR',
'lv' => 'LVL',
'ly' => 'LYD',
'ma' => 'MAD',
'mc' => 'EUR',
'md' => 'MDL',
'me' => 'YUM',
'mg' => 'MGF',
'mh' => 'USD',
'mk' => 'MKD',
'ml' => 'XOF',
'mm' => 'MMK',
'mn' => 'MNT',
'mo' => 'MOP',
'mp' => 'USD',
'mq' => 'EUR',
'mr' => 'MRO',
'ms' => 'XCD',
'mt' => 'MTL',
'mu' => 'MUR',
'mv' => 'MVR',
'mw' => 'MWK',
'mx' => 'MXN',
'my' => 'MYR',
'mz' => 'MZM',
'na' => 'NAD',
'nc' => 'XPF',
'ne' => 'XOF',
'nf' => 'AUD',
'ng' => 'NGN',
'ni' => 'NIO',
'nl' => 'EUR',
'no' => 'NOK',
'np' => 'NPR',
'nr' => 'AUD',
'nu' => 'NZD',
'nz' => 'NZD',
'om' => 'OMR',
'pa' => 'PAB',
'pe' => 'PEN',
'pf' => 'XPF',
'pg' => 'PGK',
'ph' => 'PHP',
'pk' => 'PKR',
'pl' => 'PLN',
'pm' => 'EUR',
'pn' => 'NZD',
'pr' => 'USD',
'ps' => 'ILS',
'pt' => 'EUR',
'pw' => 'USD',
'py' => 'PYG',
'qa' => 'QAR',
're' => 'EUR',
'ro' => 'ROL',
'rs' => 'YUM',
'ru' => 'RUB',
'rw' => 'RWF',
'sa' => 'SAR',
'sb' => 'SBD',
'sc' => 'SCR',
'sd' => 'SDP',
'se' => 'SEK',
'sg' => 'SGD',
'sh' => 'SHP',
'si' => 'SIT',
'sj' => 'NOK',
'sk' => 'SKK',
'sl' => 'SLL',
'sm' => 'EUR',
'sn' => 'XOF',
'so' => 'SOS',
'sr' => 'SRG',
'st' => 'STD',
'sv' => 'SVC',
'sy' => 'SYP',
'sz' => 'SZL',
'tc' => 'USD',
'td' => 'XAF',
'tf' => 'EUR',
'tg' => 'XOF',
'th' => 'THB',
'tj' => 'TJS',
'tk' => 'NZD',
'tl' => 'IDR',
'tm' => 'TMM',
'tn' => 'TND',
'to' => 'TOP',
'tr' => 'TRL',
'tt' => 'TTD',
'tv' => 'AUD',
'tw' => 'TWD',
'tz' => 'TZS',
'ua' => 'UAH',
'ug' => 'UGX',
'um' => 'USD',
'us' => 'USD',
'uy' => 'UYU',
'uz' => 'UZS',
'va' => 'EUR',
'vc' => 'XCD',
've' => 'VEB',
'vg' => 'USD',
'vi' => 'USD',
'vn' => 'VND',
'vu' => 'VUV',
'wf' => 'XPF',
'ws' => 'WST',
'ye' => 'YER',
'yt' => 'EUR',
'yu' => 'YUM',
'za' => 'ZAR',
'zm' => 'ZMK',
'zr' => 'XAF',
'zw' => 'ZWD');
# Build the mapping of symbol to currency name
foreach my $code (values %country_to_currency) {
my $symbol_for_code = currency_symbol($code);
if (defined $symbol_for_code) {
$symbol_to_currency{$symbol_for_code} = $code;
}
}
# Add in uppercase version of country code to be safe
my %uc_country_to_currency_map = ( );
while (my($k, $v) = each %country_to_currency) {
$uc_country_to_currency_map{uc $k} = $v;
}
@country_to_currency{keys %uc_country_to_currency_map} = values %uc_country_to_currency_map;
1;

View File

@ -2,9 +2,11 @@
use strict;
use warnings;
use Test::More;
use DDG::Test::Goodie;
use DDG::Test::Location;
use DDG::Request;
use utf8;
zci answer_type => 'loan';
zci is_cached => 1;
@ -14,19 +16,55 @@ ddg_goodie_test (
'DDG::Goodie::Loan'
],
'loan 400000 4.5%' =>
test_zci('Monthly Payment is $2026.74 for 30 years. Total interest paid is $329626.85'),
test_zci('Monthly Payment is $2,026.74 for 30 years. Total interest paid is $329,626.85'),
'loan $400000 at 4.5%' =>
test_zci('Monthly Payment is $2026.74 for 30 years. Total interest paid is $329626.85'),
test_zci('Monthly Payment is $2,026.74 for 30 years. Total interest paid is $329,626.85'),
'loan $500000 at 4.5% with 20% down' =>
test_zci('Monthly Payment is $2026.74 for 30 years. Total interest paid is $329626.85'),
test_zci('Monthly Payment is $2,026.74 for 30 years. Total interest paid is $329,626.85'),
'loan $500000 at 4.5% with $100000 down' =>
test_zci('Monthly Payment is $2026.74 for 30 years. Total interest paid is $329626.85'),
test_zci('Monthly Payment is $2,026.74 for 30 years. Total interest paid is $329,626.85'),
'loan $250000 3% interest 15 years' =>
test_zci('Monthly Payment is $1726.45 for 15 years. Total interest paid is $60761.74'),
test_zci('Monthly Payment is $1,726.45 for 15 years. Total interest paid is $60,761.74'),
'loan $300000 at 3% interest with $50000 downpayment for 15 years' =>
test_zci('Monthly Payment is $1726.45 for 15 years. Total interest paid is $60761.74'),
test_zci('Monthly Payment is $1,726.45 for 15 years. Total interest paid is $60,761.74'),
'loan $300000 3% $50000 down 15 year' =>
test_zci('Monthly Payment is $1726.45 for 15 years. Total interest paid is $60761.74')
test_zci('Monthly Payment is $1,726.45 for 15 years. Total interest paid is $60,761.74'),
'loan €400000 at 4.5%' =>
test_zci('Monthly Payment is €2.026,74 for 30 years. Total interest paid is €329.626,85'),
'loan £250000 3% interest 15 years' =>
test_zci('Monthly Payment is £1,726.45 for 15 years. Total interest paid is £60,761.74'),
'loan $400,000.00 at 4.5%' =>
test_zci('Monthly Payment is $2,026.74 for 30 years. Total interest paid is $329,626.85'),
'loan €250.000,00 3% interest 15 years' =>
test_zci('Monthly Payment is €1.726,45 for 15 years. Total interest paid is €60.761,74'),
# Test a few cases of inferring user's location
DDG::Request->new(query_raw => "loan 400000 4.5%", location => test_location("de")) =>
test_zci('Monthly Payment is €2.026,74 for 30 years. Total interest paid is €329.626,85'),
DDG::Request->new(query_raw => "loan 400000 4.5%", location => test_location("in")) =>
test_zci('Monthly Payment is ₨2,026.74 for 30 years. Total interest paid is ₨329,626.85'),
DDG::Request->new(query_raw => "loan 400000 4.5%", location => test_location("my")) =>
test_zci('Monthly Payment is 2,026.74 MYR for 30 years. Total interest paid is 329,626.85 MYR'),
# Test that symbol overrides user's location
DDG::Request->new(query_raw => "loan \$400,000 4.5%", location => test_location("de")) =>
test_zci('Monthly Payment is $2,026.74 for 30 years. Total interest paid is $329,626.85'),
# Imagine a new country later appears, test defaulting to USD because we don't know about it
DDG::Request->new(query_raw => "loan 400000 4.5%", location => DDG::Location->new(
{
country_code => 'LL',
country_code3 => 'LLA',
country_name => 'Llama Land',
region => '9',
region_name => 'Llama Region',
city => 'New Llama City',
latitude => '90.0000',
longitude => '0.0000',
time_zone => 'America/New_York',
area_code => 0,
continent_code => 'NA',
metro_code => 0
}
)) =>
test_zci('Monthly Payment is $2,026.74 for 30 years. Total interest paid is $329,626.85')
);
done_testing;