Currency Converter Perl Script
— ny_wk

Building a currency converter in Perl means fetching live exchange rates from a web API, decoding the JSON response, and applying a simple cross-rate calculation to convert any amount between two currencies. This walkthrough rebuilds a classic hardcoded-rate script into a modern, accurate command-line tool that pulls real rates over HTTPS.
The Problem: Why Hardcoded Rates Fail
A common first version of a currency converter in Perl stores a table of fixed conversion rates inside the script. It prompts for a source currency, a target currency, and an amount, then divides one rate by another to produce a result. It is a perfectly good exercise for learning hashes and loops, but it has one fatal flaw for real-world use: the rates are frozen in time.
Foreign exchange rates move constantly during trading hours. A rate baked into your source code is correct only for the moment it was typed and drifts further from reality every single day. Any conversion it produces is illustrative at best and misleading at worst. The numbers in older tutorials (and the script this one modernizes) reference currencies like marks, drachma, and francs that no longer even exist as separate currencies, which is a vivid reminder of how quickly hardcoded data rots.
To be genuinely useful, a converter must fetch live data. That requires three things the original lacked: an HTTP client to talk to a rate provider, a JSON parser to read the response, and proper error handling for when the network or the API misbehaves.
Approach: Static Table vs. Live API
There are two viable designs, and choosing between them is the most important architectural decision in this project.
| Aspect | Static rate table | Live API |
| Accuracy | Stale immediately; wrong within hours | Current to the API's refresh cycle |
| Dependencies | None | HTTP client, JSON module, network |
| Offline use | Works anywhere | Needs connectivity |
| Best for | Teaching, demos, fixed legacy rates | Real conversions, tooling, automation |
The honest takeaway: use the static table only to learn the language mechanics. For anything you would actually trust, query a live exchange-rate API. Most providers require a free API key and give you an endpoint that returns rates as JSON, usually keyed against a single base currency such as USD or EUR.
How cross-rate conversion works
APIs typically return rates relative to one base currency. If the base is USD and you want to convert from GBP to JPY, you cannot read a GBP-to-JPY rate directly. Instead you compute a cross rate:
amount_in_target = amount * (rate_of_target / rate_of_source)
where both rates are quoted against the same base. This is exactly the division the original script used, except the rates now come from the network instead of a literal hash. The math is identical; only the data source changes.
The Code: A Modern Perl Currency Converter
The script below uses HTTP::Tiny (bundled with modern Perl, no install needed) to fetch rates and the JSON::PP core module to decode them. It reads the source currency, target currency, and amount from the command line, validates them, and prints the converted figure. An LWP::UserAgent alternative is shown afterward.
#!/usr/bin/perl
use strict;
use warnings;
use HTTP::Tiny;
use JSON::PP;
# --- Configuration -------------------------------------------------
# Sign up with an exchange-rate provider for a free API key.
# This example targets a typical "latest rates" JSON endpoint.
my $API_KEY = $ENV{EXCHANGE_API_KEY} || die "Set EXCHANGE_API_KEY first\n";
my $BASE = 'USD';
my $ENDPOINT = "https://v6.exchangerate-api.com/v6/$API_KEY/latest/$BASE";
# --- Read and validate command-line arguments ----------------------
my ($from, $to, $amount) = @ARGV;
die usage() unless defined $from && defined $to && defined $amount;
$from = uc $from; # currency codes are upper-case, e.g. GBP
$to = uc $to;
die "Amount must be a number, got '$amount'\n"
unless $amount =~ /^-?\d+(?:\.\d+)?$/;
# --- Fetch live rates ----------------------------------------------
my $http = HTTP::Tiny->new( timeout => 10 );
my $res = $http->get($ENDPOINT);
die "Network error: $res->{status} $res->{reason}\n"
unless $res->{success};
# --- Parse the JSON response ---------------------------------------
my $data = eval { decode_json($res->{content}) };
die "Could not parse API response as JSON\n" if $@;
my $rates = $data->{conversion_rates}
or die "Unexpected response shape (no conversion_rates)\n";
# --- Validate the currency codes against the live table ------------
die "Unknown source currency: $from\n" unless exists $rates->{$from};
die "Unknown target currency: $to\n" unless exists $rates->{$to};
# --- Convert using a cross rate ------------------------------------
my $rate = $rates->{$to} / $rates->{$from};
my $converted = $amount * $rate;
printf "%.2f %s = %.2f %s (rate %.6f)\n",
$amount, $from, $converted, $to, $rate;
sub usage {
return "Usage: currency.pl <FROM> <TO> <AMOUNT>\n"
. "Example: currency.pl GBP JPY 50\n";
}
Code walkthrough
- Strict and warnings.
use strict; use warnings;are non-negotiable. They catch typos in variable names and flag undefined values before they corrupt a calculation. - API key from the environment. The key is read from
$ENV{EXCHANGE_API_KEY}rather than hardcoded. Keeping secrets out of source code is good hygiene and lets the same script run with different keys. - Build the endpoint. The provider returns all rates relative to
$BASE(USD here). Many free APIs follow this.../latest/USDpattern; check your provider's docs for the exact URL and JSON field names. - Read arguments.
my ($from, $to, $amount) = @ARGV;takes the three inputs from the command line instead of interactive prompts, which makes the tool scriptable and pipeline-friendly. - Normalize and validate. Currency codes are upper-cased with
uc, and the amount is checked against a regex so a typo likefiftyfails fast with a clear message rather than producing a silent0. - Fetch over HTTPS.
HTTP::Tinyperforms a GET with a 10-second timeout. The$res->{success}flag is true only for 2xx responses, so a 401 (bad key) or 429 (rate limited) is caught here. - Decode JSON safely.
decode_jsonis wrapped inevalso malformed JSON throws into$@instead of killing the script with an opaque error. - Verify the shape. Before trusting
conversion_rates, the code confirms the key exists. APIs change and error payloads look different from success payloads. - Check both currencies exist in the returned table, mirroring the original script's intent but against real, current data.
- Compute and format. The cross rate is applied and
printfformats money to two decimals, with the effective rate shown for transparency.
Using LWP::UserAgent instead
If you prefer the full-featured LWP::UserAgent (it handles redirects, proxies, and richer headers), swap the fetch block. Install it with cpanm LWP::UserAgent LWP::Protocol::https first; HTTPS support is a separate module.
use LWP::UserAgent;
use JSON::PP;
my $ua = LWP::UserAgent->new( timeout => 10, agent => 'perl-fx/1.0' );
my $res = $ua->get($ENDPOINT);
die "Network error: " . $res->status_line . "\n"
unless $res->is_success;
my $data = decode_json( $res->decoded_content );
The rest of the program is unchanged. HTTP::Tiny is lighter and ships with Perl; LWP::UserAgent is the heavyweight choice when you need its extra capabilities.
Error Handling That Matters
Network code fails in ways that pure arithmetic never does. A production-quality converter must anticipate each failure mode rather than crash with a stack trace.
- No connectivity or timeout. The
$res->{success}/is_successcheck catches DNS failures, refused connections, and timeouts. Always set an explicittimeoutso the script cannot hang forever. - Authentication errors. A missing or invalid API key usually returns HTTP 401 or 403. The status check surfaces this immediately; print the status code so the cause is obvious.
- Rate limiting. Free tiers cap requests. A 429 response means you have exceeded the quota; back off and retry later, or cache results.
- Malformed JSON. Wrapping
decode_jsoninevalturns a fatal parse error into a controlled message. Never assume the body is valid JSON just because the request succeeded. - Missing fields. Checking
exists $rates->{$from}guards against unsupported currency codes and against the API returning an error object instead of rates. - Division by zero. If a provider ever returns a zero or missing source rate, guard it:
die "No rate for $from\n" unless $rates->{$from};before dividing.
A useful pattern for transient failures is a small retry loop with a short pause, so a single dropped packet does not abort the whole run.
Caching to respect rate limits
Because exchange rates do not change second to second for casual use, cache the JSON to a local file with a timestamp and reuse it for a few minutes. This keeps you well under free-tier limits and makes repeated conversions instant. Read the cache file, check its age, and only hit the network when it is stale.
Running and Verifying the Converter
Set your key, then call the script with three arguments: source code, target code, and amount.
- Install prerequisites if needed.
HTTP::TinyandJSON::PPare core in modern Perl. For the LWP path:cpanm LWP::UserAgent LWP::Protocol::https JSON::PP. - Export your API key (Linux/macOS):
export EXCHANGE_API_KEY=your_key_here. On Windows PowerShell:$env:EXCHANGE_API_KEY='your_key_here'. - Run a conversion:
perl currency.pl GBP JPY 50 - Read the output: something like
50.00 GBP = 9543.21 JPY (rate 190.864200). The exact figure depends on the live rate at run time.
To verify correctness, sanity-check three things. First, convert a currency to itself: perl currency.pl USD USD 100 must print exactly 100.00 at a rate of 1.000000. Second, convert A to B and then B back to A with the same amount; you should land back near where you started, with only tiny rounding differences. Third, spot-check one result against a public rate from a search engine or your bank; they should agree to within the spread the provider quotes. If they diverge wildly, your base currency or field name is probably wrong.
Test the failure paths too: run with a bad currency code, an unset key, and a non-numeric amount. Each should exit with a clear, single-line message rather than a Perl error dump. That is the difference between a learning exercise and a tool you can rely on.
Key Takeaways
- Hardcoded rates go stale instantly and should be used only for teaching; a real converter must fetch live data from an API.
- Use core modules first:
HTTP::TinyplusJSON::PPneed no installation; reach forLWP::UserAgentonly when you need its extra features. - Cross-rate math is just division:
amount * (rate_to / rate_from)when both rates share one base currency. - Error handling is the hard part: guard the network call, wrap JSON parsing in
eval, validate currency codes, and avoid division by zero. - Verify with round-trips and self-conversions, keep the API key in an environment variable, and cache responses to respect rate limits.
Frequently Asked Questions
Do I need an API key to convert currencies in Perl?
For live rates, almost always yes. Reputable exchange-rate providers require a free API key to authenticate and meter requests. You can build a converter with no key using a static rate table, but those rates are frozen and unsuitable for real conversions. Sign up for a free tier, store the key in an environment variable, and pass it in the request URL or headers.
Should I use HTTP::Tiny or LWP::UserAgent?
Start with HTTP::Tiny: it ships with Perl, supports HTTPS, and is more than enough for a simple GET request. Choose LWP::UserAgent when you need cookies, proxies, custom redirect handling, or fine-grained header control. Remember that LWP needs LWP::Protocol::https installed separately for secure requests.
How do I convert between two non-base currencies?
Most APIs quote every rate against a single base currency. To go from GBP to JPY when the base is USD, divide the target rate by the source rate: rate = rates{JPY} / rates{GBP}, then multiply by the amount. Both rates must come from the same base for the cross rate to be valid.
Why does my converted amount look slightly off?
Small differences come from rounding (printing two decimals) and from the provider's mid-market versus retail spread. Larger discrepancies usually mean a mismatched base currency, the wrong JSON field name, or stale cached data. Print the effective rate alongside the result so you can debug at a glance.
For more hands-on scripting walkthroughs and tech tutorials, subscribe on YouTube @explorenystream.