mirror of
https://github.com/hakanensari/frankfurter.git
synced 2024-11-25 04:22:28 +01:00
Implement time series
... along with some minor miscellaneous refactoring This finally completes fixerAPI/fixer#22
This commit is contained in:
parent
fcc5f8504f
commit
a96e56808e
@ -3,6 +3,6 @@ AllCops:
|
|||||||
Documentation:
|
Documentation:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
Metrics/BlockLength:
|
Metrics/BlockLength:
|
||||||
Enabled: false
|
ExcludedMethods: ['describe', 'helpers']
|
||||||
Metrics/LineLength:
|
Metrics/MethodLength:
|
||||||
Enabled: false
|
Max: 11
|
||||||
|
16
README.md
16
README.md
@ -8,10 +8,10 @@ Rates are updated around 4PM CET every working day.
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Get the latest foreign exchange rates.
|
Get the current foreign exchange rates.
|
||||||
|
|
||||||
```http
|
```http
|
||||||
GET /latest
|
GET /current
|
||||||
```
|
```
|
||||||
|
|
||||||
Get historical rates for any day since 1999.
|
Get historical rates for any day since 1999.
|
||||||
@ -23,25 +23,25 @@ GET /2000-01-03
|
|||||||
Rates quote against the Euro by default. Quote against a different currency.
|
Rates quote against the Euro by default. Quote against a different currency.
|
||||||
|
|
||||||
```http
|
```http
|
||||||
GET /latest?from=USD
|
GET /current?from=USD
|
||||||
```
|
```
|
||||||
|
|
||||||
Request specific exchange rates.
|
Request specific exchange rates.
|
||||||
|
|
||||||
```http
|
```http
|
||||||
GET /latest?to=GBP
|
GET /current?to=GBP
|
||||||
```
|
```
|
||||||
|
|
||||||
Change the amount requested.
|
Change the converted amount.
|
||||||
|
|
||||||
```http
|
```http
|
||||||
GET /latest?amount=100
|
GET /current?amount=100
|
||||||
```
|
```
|
||||||
|
|
||||||
Finally, use all the above together.
|
Finally, use all the above together.
|
||||||
|
|
||||||
```http
|
```http
|
||||||
GET /latest?from=EUR&to=GBP&amount=100
|
GET /current?from=EUR&to=GBP&amount=100
|
||||||
```
|
```
|
||||||
|
|
||||||
The primary use case is client side. For instance, with [money.js](https://openexchangerates.github.io/money.js/) in the browser
|
The primary use case is client side. For instance, with [money.js](https://openexchangerates.github.io/money.js/) in the browser
|
||||||
@ -52,7 +52,7 @@ let demo = () => {
|
|||||||
alert("£1 = $" + rate.toFixed(4))
|
alert("£1 = $" + rate.toFixed(4))
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch('https://yourdomain.com/latest')
|
fetch('https://yourdomain.com/current')
|
||||||
.then((resp) => resp.json())
|
.then((resp) => resp.json())
|
||||||
.then((data) => fx.rates = data.rates)
|
.then((data) => fx.rates = data.rates)
|
||||||
.then(demo)
|
.then(demo)
|
||||||
|
@ -4,4 +4,5 @@ require 'pg'
|
|||||||
require 'sequel'
|
require 'sequel'
|
||||||
|
|
||||||
Sequel.single_threaded = true
|
Sequel.single_threaded = true
|
||||||
Sequel.connect(ENV['DATABASE_URL'] || "postgres://localhost/frankfurter_#{App.env}")
|
Sequel.connect(ENV['DATABASE_URL'] ||
|
||||||
|
"postgres://localhost/frankfurter_#{App.env}")
|
||||||
|
@ -7,5 +7,24 @@ class Currency < Sequel::Model
|
|||||||
.order(Sequel.desc(:date))
|
.order(Sequel.desc(:date))
|
||||||
.limit(1))
|
.limit(1))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def between(date_interval)
|
||||||
|
query = where(date: date_interval).order(:date)
|
||||||
|
length = date_interval.last - date_interval.first
|
||||||
|
if length > 365
|
||||||
|
query.sampled('month')
|
||||||
|
elsif length > 90
|
||||||
|
query.sampled('week')
|
||||||
|
else
|
||||||
|
query
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def sampled(precision)
|
||||||
|
sampled_date = Sequel.lit("date_trunc('#{precision}', date)")
|
||||||
|
select(:iso_code).select_append { avg(rate).as(rate) }
|
||||||
|
.select_append(sampled_date.as(:date))
|
||||||
|
.group(:iso_code, sampled_date)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
13
lib/query.rb
13
lib/query.rb
@ -6,19 +6,24 @@ class Query
|
|||||||
end
|
end
|
||||||
|
|
||||||
def amount
|
def amount
|
||||||
@params[:amount].to_f if @params[:amount] # rubocop:disable Style/SafeNavigation
|
return unless @params[:amount]
|
||||||
|
@params[:amount].to_f
|
||||||
end
|
end
|
||||||
|
|
||||||
def base
|
def base
|
||||||
@params.values_at(:base, :from).compact.first&.upcase
|
@params.values_at(:from, :base).compact.first&.upcase
|
||||||
end
|
end
|
||||||
|
|
||||||
def symbols
|
def symbols
|
||||||
@params.values_at(:symbols, :to).compact.first&.split(',')
|
@params.values_at(:to, :symbols).compact.first&.split(',')
|
||||||
end
|
end
|
||||||
|
|
||||||
def date
|
def date
|
||||||
@params[:date]
|
if @params[:date]
|
||||||
|
Date.parse(@params[:date])
|
||||||
|
else
|
||||||
|
(Date.parse(@params[:start_date])..Date.parse(@params[:end_date]))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_h
|
def to_h
|
||||||
|
61
lib/quote.rb
61
lib/quote.rb
@ -1,61 +1,4 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'currency'
|
require 'quote/end_of_day'
|
||||||
|
require 'quote/interval'
|
||||||
class Quote
|
|
||||||
DEFAULT_BASE = 'EUR'
|
|
||||||
|
|
||||||
def initialize(amount: 1.0,
|
|
||||||
base: DEFAULT_BASE,
|
|
||||||
date: Date.today.to_s,
|
|
||||||
symbols: nil)
|
|
||||||
@amount = amount
|
|
||||||
@base = base
|
|
||||||
@date = date
|
|
||||||
@symbols = symbols
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_h
|
|
||||||
{ base: @base, date: date, rates: calculate_rates }
|
|
||||||
end
|
|
||||||
|
|
||||||
def date
|
|
||||||
currencies.first[:date].to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def calculate_rates # rubocop:disable Metrics/AbcSize
|
|
||||||
rates = currencies.each_with_object({}) do |currency, hsh|
|
|
||||||
hsh[currency[:iso_code]] = currency[:rate]
|
|
||||||
end
|
|
||||||
|
|
||||||
return rates if @base == DEFAULT_BASE && @amount == 1.0
|
|
||||||
|
|
||||||
if @symbols.nil? || @symbols.include?(DEFAULT_BASE) || @base == DEFAULT_BASE
|
|
||||||
rates.update(DEFAULT_BASE => 1.0)
|
|
||||||
end
|
|
||||||
divisor = rates.delete(@base)
|
|
||||||
|
|
||||||
rates.sort.map! { |ic, rate| [ic, round(@amount * rate / divisor)] }.to_h
|
|
||||||
end
|
|
||||||
|
|
||||||
def currencies
|
|
||||||
@currencies ||= begin
|
|
||||||
scope = Currency.latest(@date)
|
|
||||||
scope = scope.where(iso_code: @symbols + [@base]) if @symbols
|
|
||||||
|
|
||||||
scope.order(:iso_code).naked
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# To paraphrase Wikipedia, most currency pairs are quoted to four decimal
|
|
||||||
# places. An exception to this is exchange rates with a value of less than
|
|
||||||
# 1.000, which are quoted to five or six decimal places. Exchange rates
|
|
||||||
# greater than around 20 are usually quoted to three decimal places and
|
|
||||||
# exchange rates greater than 80 are quoted to two decimal places.
|
|
||||||
# Currencies over 5000 are usually quoted with no decimal places.
|
|
||||||
def round(rate)
|
|
||||||
Float(format('%.5g', rate))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
82
lib/quote/base.rb
Normal file
82
lib/quote/base.rb
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'roundable'
|
||||||
|
|
||||||
|
module Quote
|
||||||
|
class Base
|
||||||
|
include Roundable
|
||||||
|
|
||||||
|
DEFAULT_BASE = 'EUR'
|
||||||
|
|
||||||
|
attr_reader :amount, :base, :date, :symbols, :result
|
||||||
|
|
||||||
|
def initialize(date:, amount: 1.0, base: 'EUR', symbols: nil)
|
||||||
|
@date = date
|
||||||
|
@amount = amount
|
||||||
|
@base = base
|
||||||
|
@symbols = symbols
|
||||||
|
@result = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform
|
||||||
|
return false if result.frozen?
|
||||||
|
|
||||||
|
prepare_rates
|
||||||
|
rebase_rates if must_rebase?
|
||||||
|
result.freeze
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def must_rebase?
|
||||||
|
base != 'EUR'
|
||||||
|
end
|
||||||
|
|
||||||
|
def formatted
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
def not_found?
|
||||||
|
result.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache_key
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def data
|
||||||
|
@data ||= fetch_data
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_data
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
def prepare_rates
|
||||||
|
data.each_with_object(result) do |currency, result|
|
||||||
|
date = currency[:date].to_date.to_s
|
||||||
|
result[date] ||= {}
|
||||||
|
result[date][currency[:iso_code]] = round(amount * currency[:rate])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def rebase_rates
|
||||||
|
result.each do |date, rates|
|
||||||
|
add_euro(rates)
|
||||||
|
|
||||||
|
divisor = rates.delete(base)
|
||||||
|
result[date] = rates.sort
|
||||||
|
.map! do |iso_code, rate|
|
||||||
|
[iso_code, round(amount * rate / divisor)]
|
||||||
|
end
|
||||||
|
.to_h
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_euro(rates)
|
||||||
|
rates['EUR'] = amount if symbols.nil? || symbols.include?('EUR')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
26
lib/quote/end_of_day.rb
Normal file
26
lib/quote/end_of_day.rb
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'currency'
|
||||||
|
require 'quote/base'
|
||||||
|
|
||||||
|
module Quote
|
||||||
|
class EndOfDay < Base
|
||||||
|
def formatted
|
||||||
|
{ base: base, date: result.keys.first, rates: result.values.first }
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache_key
|
||||||
|
return if not_found?
|
||||||
|
Digest::MD5.hexdigest(result.keys.first)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def fetch_data
|
||||||
|
scope = Currency.latest(date)
|
||||||
|
scope = scope.where(iso_code: symbols + [base]) if symbols
|
||||||
|
|
||||||
|
scope.naked
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
29
lib/quote/interval.rb
Normal file
29
lib/quote/interval.rb
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'currency'
|
||||||
|
require 'quote/base'
|
||||||
|
|
||||||
|
module Quote
|
||||||
|
class Interval < Base
|
||||||
|
def formatted
|
||||||
|
{ base: base,
|
||||||
|
start_date: result.keys.first,
|
||||||
|
end_date: result.keys.last,
|
||||||
|
rates: result }
|
||||||
|
end
|
||||||
|
|
||||||
|
def cache_key
|
||||||
|
return if not_found?
|
||||||
|
Digest::MD5.hexdigest(result.keys.last)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def fetch_data
|
||||||
|
scope = Currency.between(date)
|
||||||
|
scope = scope.where(iso_code: symbols + [base]) if symbols
|
||||||
|
|
||||||
|
scope.naked
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
23
lib/roundable.rb
Normal file
23
lib/roundable.rb
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Roundable
|
||||||
|
# To paraphrase Wikipedia, most currency pairs are quoted to four decimal
|
||||||
|
# places. An exception to this is exchange rates with a value of less than
|
||||||
|
# 1.000, which are quoted to five or six decimal places. Exchange rates
|
||||||
|
# greater than around 20 are usually quoted to three decimal places and
|
||||||
|
# exchange rates greater than 80 are quoted to two decimal places.
|
||||||
|
# Currencies over 5000 are usually quoted with no decimal places.
|
||||||
|
def round(value)
|
||||||
|
if value > 5000
|
||||||
|
value.round
|
||||||
|
elsif value > 80
|
||||||
|
Float(format('%.2f', value))
|
||||||
|
elsif value > 20
|
||||||
|
Float(format('%.3f', value))
|
||||||
|
elsif value > 1
|
||||||
|
Float(format('%.4f', value))
|
||||||
|
else
|
||||||
|
Float(format('%.5f', value))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
BIN
lib/web/public/images/favicon.ico
Normal file
BIN
lib/web/public/images/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
Before Width: | Height: | Size: 10 KiB |
Binary file not shown.
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
@ -2,25 +2,44 @@ body {
|
|||||||
font-family: 'Open Sans', sans-serif;
|
font-family: 'Open Sans', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar .navbar-nav .nav-link {
|
strong {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
background-color: #337cf6;
|
||||||
font-size: .875rem;
|
font-size: .875rem;
|
||||||
|
padding-bottom: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .nav-link {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar .nav-link:hover {
|
||||||
|
color: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 720px;
|
max-width: 600px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
padding-top: 1em;
|
padding-top: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
margin: 2em 0;
|
||||||
|
}
|
||||||
|
|
||||||
.hero .container {
|
.hero .container {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero .logo {
|
.hero .logo {
|
||||||
padding-bottom: 1em;
|
padding-bottom: 1em;
|
||||||
width: 120px;
|
width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer .container {
|
.footer .container {
|
||||||
@ -30,12 +49,10 @@ body {
|
|||||||
|
|
||||||
.hljs {
|
.hljs {
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
border: 2px solid #f8f9fa;
|
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hljs:hover {
|
.hljs:hover {
|
||||||
border-bottom-color: #dee2e6;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,41 +61,49 @@ body {
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
#carbonads {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
@media screen and (max-width: 767px) {
|
@media screen and (max-width: 767px) {
|
||||||
|
.hero .logo {
|
||||||
|
width: 75px;
|
||||||
|
}
|
||||||
|
|
||||||
#carbonads {
|
#carbonads {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
@media screen and (min-width: 768px) {
|
||||||
#carbonads {
|
#carbonads {
|
||||||
background-color: #f0f0f0;
|
background-color: #f8f9fa;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 24px;
|
right: 24px;
|
||||||
top: 36px;
|
top: 24px;
|
||||||
width: 150px;
|
width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.carbon-text {
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
.carbon-img,
|
.carbon-img,
|
||||||
.carbon-poweredby,
|
.carbon-poweredby,
|
||||||
.carbon-text {
|
.carbon-text {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.carbon-text,
|
.carbon-text,
|
||||||
.carbon-poweredby {
|
.carbon-poweredby {
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.carbon-text {
|
.carbon-text {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.carbon-poweredby {
|
.carbon-poweredby {
|
||||||
|
color: #6c757d;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #999;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
@ -15,6 +15,10 @@ use Rack::Cors do
|
|||||||
end
|
end
|
||||||
|
|
||||||
configure :development do
|
configure :development do
|
||||||
|
require 'rack-livereload'
|
||||||
|
|
||||||
|
use Rack::LiveReload
|
||||||
|
|
||||||
set :show_exceptions, :after_handler
|
set :show_exceptions, :after_handler
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -27,13 +31,30 @@ configure :test do
|
|||||||
end
|
end
|
||||||
|
|
||||||
helpers do
|
helpers do
|
||||||
def quote
|
def end_of_day_quote
|
||||||
@quote ||= begin
|
@end_of_day_quote ||= begin
|
||||||
query = Query.new(params)
|
quote = Quote::EndOfDay.new(query)
|
||||||
Quote.new(query.to_h)
|
quote.perform
|
||||||
|
halt 404 if quote.not_found?
|
||||||
|
|
||||||
|
quote
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def interval_quote
|
||||||
|
@interval_quote ||= begin
|
||||||
|
quote = Quote::Interval.new(query)
|
||||||
|
quote.perform
|
||||||
|
halt 404 if quote.not_found?
|
||||||
|
|
||||||
|
quote
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def query
|
||||||
|
Query.new(params).to_h
|
||||||
|
end
|
||||||
|
|
||||||
def json(data)
|
def json(data)
|
||||||
json = Oj.dump(data, mode: :compat)
|
json = Oj.dump(data, mode: :compat)
|
||||||
callback = params.delete('callback')
|
callback = params.delete('callback')
|
||||||
@ -52,28 +73,25 @@ options '*' do
|
|||||||
200
|
200
|
||||||
end
|
end
|
||||||
|
|
||||||
get '*' do
|
|
||||||
cache_control :public
|
|
||||||
pass
|
|
||||||
end
|
|
||||||
|
|
||||||
get '/' do
|
get '/' do
|
||||||
erb :index
|
erb :index
|
||||||
end
|
end
|
||||||
|
|
||||||
get '/(?:latest|current)', mustermann_opts: { type: :regexp } do
|
get '/(?:latest|current)', mustermann_opts: { type: :regexp } do
|
||||||
last_modified quote.date
|
params[:date] = Date.today.to_s
|
||||||
json quote.to_h
|
etag end_of_day_quote.cache_key
|
||||||
|
json end_of_day_quote.formatted
|
||||||
end
|
end
|
||||||
|
|
||||||
get '/(?<date>\d{4}-\d{2}-\d{2})', mustermann_opts: { type: :regexp } do
|
get '/(?<date>\d{4}-\d{2}-\d{2})', mustermann_opts: { type: :regexp } do
|
||||||
last_modified quote.date
|
etag end_of_day_quote.cache_key
|
||||||
json quote.to_h
|
json end_of_day_quote.formatted
|
||||||
end
|
end
|
||||||
|
|
||||||
get '/(?<start_date>\d{4}-\d{2}-\d{2})\.\.(?<end_date>\d{4}-\d{2}-\d{2})', mustermann_opts: { type: :regexp } do
|
get '/(?<start_date>\d{4}-\d{2}-\d{2})\.\.(?<end_date>\d{4}-\d{2}-\d{2})',
|
||||||
last_modified quote.end_date
|
mustermann_opts: { type: :regexp } do
|
||||||
json quote.to_h
|
etag interval_quote.cache_key
|
||||||
|
json interval_quote.formatted
|
||||||
end
|
end
|
||||||
|
|
||||||
not_found do
|
not_found do
|
||||||
|
@ -7,43 +7,43 @@
|
|||||||
<title>Frankfurter</title>
|
<title>Frankfurter</title>
|
||||||
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/github.min.css">
|
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/github.min.css">
|
||||||
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.1/css/bootstrap.min.css">
|
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.1/css/bootstrap.min.css">
|
||||||
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Open+Sans:300,400">
|
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Open+Sans:300,400,600">
|
||||||
<link rel="stylesheet" href="/stylesheets/application.css">
|
<link rel="stylesheet" href="/stylesheets/application.css">
|
||||||
<link rel="shortcut icon" href="/images/frankfurter-icon.png">
|
<link rel="shortcut icon" href="/images/favicon.ico">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav class="navbar navbar-expand">
|
<nav class="navbar navbar-expand">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<ul class="navbar-nav mx-sm-auto">
|
<ul class="navbar-nav mx-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="https://github.com/hakanensari/frankfurter" target="_blank">Source code</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="https://www.ecb.europa.eu/stats/exchange/eurofxref/html/index.en.html" target="_blank">Data sets</a>
|
<a class="nav-link" href="https://www.ecb.europa.eu/stats/exchange/eurofxref/html/index.en.html" target="_blank">Data sets</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="https://status.frankfurter.app/" target="_blank">Status</a>
|
<a class="nav-link" href="https://status.frankfurter.app/" target="_blank">Status</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="https://github.com/hakanensari/frankfurter" target="_blank">Source</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<section class="hero section">
|
<section class="hero section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<image class="logo" src="/images/frankfurter.png" alt="">
|
||||||
<h1 class="display-4">Frankfurter</h1>
|
<h1 class="display-4">Frankfurter</h1>
|
||||||
<p class="lead">
|
<p class="lead">
|
||||||
Foreign exchange rates and currency conversion API
|
Foreign exchange rates and currency conversion API
|
||||||
</p>
|
</p>
|
||||||
<img alt="" class="logo" src="/images/frankfurter.png">
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="introduction section">
|
<section class="introduction section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<p>
|
<p>
|
||||||
Frankfurter is a lightweight API for querying current and historical forex rates published by the European Central Bank.
|
Frankfurter is an open-source API for current and historical forex rates published by the European Central Bank.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Rates update around 4PM CET on every working day. Historical data goes back to early 1999.
|
The API updates rates around 4PM CET every working day. Historical data goes back to early 1999.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -58,7 +58,11 @@
|
|||||||
<p>
|
<p>
|
||||||
Get historical rates for any day since 1999.
|
Get historical rates for any day since 1999.
|
||||||
</p>
|
</p>
|
||||||
<pre><code class="http hljs small">GET /1999-12-31 <i>HTTP/1.1</i></code></pre>
|
<pre><code class="http hljs small">GET /2000-01-01 <i>HTTP/1.1</i></code></pre>
|
||||||
|
<p>
|
||||||
|
Get historical exchange rates for a given time period.
|
||||||
|
</p>
|
||||||
|
<pre><code class="http hljs small">GET /2000-01-01..2000-12-31 <i>HTTP/1.1</i></code></pre>
|
||||||
<p>
|
<p>
|
||||||
Rates are quoted against the Euro by default. Quote against a different
|
Rates are quoted against the Euro by default. Quote against a different
|
||||||
currency by setting the <strong>from</strong> parameter in your request.
|
currency by setting the <strong>from</strong> parameter in your request.
|
||||||
@ -69,9 +73,9 @@
|
|||||||
</p>
|
</p>
|
||||||
<pre><code class="http hljs small">GET /current?to=USD,GBP <i>HTTP/1.1</i></code></pre>
|
<pre><code class="http hljs small">GET /current?to=USD,GBP <i>HTTP/1.1</i></code></pre>
|
||||||
<p>
|
<p>
|
||||||
Convert a specific value.
|
Convert a specific value using <strong>amount</strong>.
|
||||||
</p>
|
</p>
|
||||||
<pre><code class="http hljs small">GET /current?amount=1000&from=GBP&to=EUR <i>HTTP/1.1</i></code></pre>
|
<pre><code class="http hljs small">GET /current?amount=1000 <i>HTTP/1.1</i></code></pre>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="best-practices section">
|
<section class="best-practices section">
|
||||||
@ -110,9 +114,9 @@ fetch('/current?from=GBP&to=USD')
|
|||||||
<iframe src="https://ghbtns.com/github-btn.html?user=hakanensari&repo=frankfurter&type=star&count=true" frameborder="0" scrolling="0" width="120px" height="20px"></iframe>
|
<iframe src="https://ghbtns.com/github-btn.html?user=hakanensari&repo=frankfurter&type=star&count=true" frameborder="0" scrolling="0" width="120px" height="20px"></iframe>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js"></script>
|
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js"></script>
|
||||||
<script src="/javascripts/application.js"></script>
|
<script type="text/javascript" src="/javascripts/application.js"></script>
|
||||||
<script async type="text/javascript" src="//cdn.carbonads.com/carbon.js?zoneid=1673&serve=C6AILKT&placement=fixerio" id="_carbonads_js"></script>
|
<script async type="text/javascript" src="//cdn.carbonads.com/carbon.js?serve=CK7D45QM&placement=frankfurterapp" id="_carbonads_js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
@ -4,37 +4,52 @@ require_relative 'helper'
|
|||||||
require 'currency'
|
require 'currency'
|
||||||
|
|
||||||
describe Currency do
|
describe Currency do
|
||||||
around do |test|
|
describe '.current' do
|
||||||
Currency.db.transaction do
|
it 'returns current rates' do
|
||||||
test.call
|
date = Currency.order(Sequel.desc(:date)).first.date
|
||||||
raise Sequel::Rollback
|
rates = Currency.latest
|
||||||
|
rates.first.date.must_equal date
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns latest rates before given date' do
|
||||||
|
date = Date.parse('2010-01-01')
|
||||||
|
rates = Currency.latest(date)
|
||||||
|
rates.first.date.must_be :<=, date
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns nothing if there are no rates before given date' do
|
||||||
|
rates = Currency.latest(Date.parse('1998-01-01'))
|
||||||
|
rates.must_be_empty
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
describe '.between' do
|
||||||
Currency.dataset.delete
|
it 'returns rates between given dates' do
|
||||||
@earlier = [
|
start_date = Date.parse('2010-01-01')
|
||||||
Currency.create(iso_code: 'EUR', rate: 1, date: '2014-01-01'),
|
end_date = Date.parse('2010-01-31')
|
||||||
Currency.create(iso_code: 'USD', rate: 2, date: '2014-01-01')
|
dates = Currency.between((start_date..end_date)).map(:date).uniq.sort
|
||||||
]
|
dates.first.must_be :>=, start_date
|
||||||
@later = [
|
dates.last.must_be :<=, end_date
|
||||||
Currency.create(iso_code: 'EUR', rate: 1, date: '2015-01-01'),
|
end
|
||||||
Currency.create(iso_code: 'USD', rate: 2, date: '2015-01-01')
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns latest rates' do
|
it 'returns nothing if there are no rates between given dates' do
|
||||||
data = Currency.latest.to_a
|
date_interval = (Date.parse('1998-01-01')..Date.parse('1998-01-31'))
|
||||||
data.sample.date.must_equal @later.sample.date
|
Currency.between(date_interval).must_be_empty
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns latest rates before given date' do
|
it 'returns all rates up to 90 days' do
|
||||||
data = Currency.latest(@later.sample.date - 1).to_a
|
date_interval = (Date.parse('2010-01-01')..Date.parse('2010-03-01'))
|
||||||
data.sample.date.must_equal @earlier.sample.date
|
Currency.between(date_interval).map(:date).uniq.count.must_be :>, 30
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns nothing if there are no rates before given date' do
|
it 'samples weeks over 90 days and below 366 days' do
|
||||||
data = Currency.latest(@earlier.sample.date - 1).to_a
|
date_interval = (Date.parse('2010-01-01')..Date.parse('2010-12-31'))
|
||||||
data.must_be_empty
|
Currency.between(date_interval).map(:date).uniq.count.must_be :<=, 52
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'samples months over 365 days' do
|
||||||
|
date_interval = (Date.parse('2001-01-01')..Date.parse('2010-12-31'))
|
||||||
|
Currency.between(date_interval).map(:date).uniq.count.must_be :<=, 120
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -25,7 +25,7 @@ describe 'the API' do
|
|||||||
|
|
||||||
it 'will not process a date before 2000' do
|
it 'will not process a date before 2000' do
|
||||||
get '/1999-01-01'
|
get '/1999-01-01'
|
||||||
last_response.must_be :unprocessable?
|
last_response.must_be :not_found?
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'will not process an invalid base' do
|
it 'will not process an invalid base' do
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
ENV['APP_ENV'] ||= 'test'
|
||||||
|
|
||||||
require_relative '../config/environment'
|
require_relative '../config/environment'
|
||||||
|
|
||||||
require 'minitest/autorun'
|
require 'minitest/autorun'
|
||||||
|
@ -9,7 +9,7 @@ describe Query do
|
|||||||
query.amount.must_equal 100.0
|
query.amount.must_equal 100.0
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'defaults amount to nothin' do
|
it 'defaults amount to nothing' do
|
||||||
query = Query.new
|
query = Query.new
|
||||||
query.amount.must_be_nil
|
query.amount.must_be_nil
|
||||||
end
|
end
|
||||||
@ -24,7 +24,7 @@ describe Query do
|
|||||||
query.base.must_be_nil
|
query.base.must_be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'aliases base as from' do
|
it 'aliases base with from' do
|
||||||
query = Query.new(from: 'USD')
|
query = Query.new(from: 'USD')
|
||||||
query.base.must_equal 'USD'
|
query.base.must_equal 'USD'
|
||||||
end
|
end
|
||||||
@ -34,7 +34,7 @@ describe Query do
|
|||||||
query.symbols.must_equal %w[USD GBP]
|
query.symbols.must_equal %w[USD GBP]
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'aliases symbols to to' do
|
it 'aliases symbols with to' do
|
||||||
query = Query.new(to: 'USD')
|
query = Query.new(to: 'USD')
|
||||||
query.symbols.must_equal ['USD']
|
query.symbols.must_equal ['USD']
|
||||||
end
|
end
|
||||||
@ -45,12 +45,15 @@ describe Query do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'returns given date' do
|
it 'returns given date' do
|
||||||
query = Query.new(date: '2014-01-01')
|
date = '2014-01-01'
|
||||||
query.date.must_equal '2014-01-01'
|
query = Query.new(date: date)
|
||||||
|
query.date.must_equal Date.parse(date)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'defaults date to nothing' do
|
it 'returns given date interval' do
|
||||||
query = Query.new
|
start_date = '2014-01-01'
|
||||||
query.date.must_be_nil
|
end_date = '2014-12-31'
|
||||||
|
query = Query.new(start_date: start_date, end_date: end_date)
|
||||||
|
query.date.must_equal((Date.parse(start_date)..Date.parse(end_date)))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
55
spec/quote/base_spec.rb
Normal file
55
spec/quote/base_spec.rb
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative '../helper'
|
||||||
|
require 'quote/base'
|
||||||
|
|
||||||
|
module Quote
|
||||||
|
describe Base do
|
||||||
|
let(:klass) do
|
||||||
|
Class.new(Base)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:quote) do
|
||||||
|
klass.new(date: Date.today)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'requires data' do
|
||||||
|
-> { quote.perform }.must_raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not know how to format result' do
|
||||||
|
-> { quote.formatted }.must_raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not know how to generate a cache key' do
|
||||||
|
-> { quote.cache_key }.must_raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'defaults base to Euro' do
|
||||||
|
quote.base.must_equal 'EUR'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'defaults amount to 1' do
|
||||||
|
quote.amount.must_equal 1
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'when given data' do
|
||||||
|
let(:klass) do
|
||||||
|
Class.new(Base) do
|
||||||
|
def fetch_data
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'performs' do
|
||||||
|
assert quote.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'performs only once' do
|
||||||
|
quote.perform
|
||||||
|
refute quote.perform
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
83
spec/quote/end_of_day_spec.rb
Normal file
83
spec/quote/end_of_day_spec.rb
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative '../helper'
|
||||||
|
require 'quote/end_of_day'
|
||||||
|
|
||||||
|
module Quote
|
||||||
|
describe EndOfDay do
|
||||||
|
let(:date) do
|
||||||
|
Date.parse('2010-10-10')
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:quote) do
|
||||||
|
EndOfDay.new(date: date)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
quote.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns rates' do
|
||||||
|
quote.formatted[:rates].wont_be :empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'quotes given date' do
|
||||||
|
Date.parse(quote.formatted[:date]).must_be :<=, date
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'quotes against the Euro' do
|
||||||
|
quote.formatted[:rates].keys.wont_include 'EUR'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sorts rates' do
|
||||||
|
rates = quote.formatted[:rates]
|
||||||
|
rates.keys.must_equal rates.keys.sort
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has a cache key' do
|
||||||
|
quote.cache_key.wont_be :empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'given a new base' do
|
||||||
|
let(:quote) do
|
||||||
|
EndOfDay.new(date: date, base: 'USD')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'quotes against that base' do
|
||||||
|
quote.formatted[:rates].keys.wont_include 'USD'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sorts rates' do
|
||||||
|
rates = quote.formatted[:rates]
|
||||||
|
rates.keys.must_equal rates.keys.sort
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'given symbols' do
|
||||||
|
let(:quote) do
|
||||||
|
EndOfDay.new(date: date, symbols: %w[USD GBP JPY])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'quotes only for those symbols' do
|
||||||
|
rates = quote.formatted[:rates]
|
||||||
|
rates.keys.must_include 'USD'
|
||||||
|
rates.keys.wont_include 'CAD'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sorts rates' do
|
||||||
|
rates = quote.formatted[:rates]
|
||||||
|
rates.keys.must_equal rates.keys.sort
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'when given an amount' do
|
||||||
|
let(:quote) do
|
||||||
|
EndOfDay.new(date: date, amount: 100)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'calculates quotes for that amount' do
|
||||||
|
quote.formatted[:rates]['USD'].must_be :>, 10
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
94
spec/quote/interval_spec.rb
Normal file
94
spec/quote/interval_spec.rb
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative '../helper'
|
||||||
|
require 'quote/interval'
|
||||||
|
|
||||||
|
module Quote
|
||||||
|
describe Interval do
|
||||||
|
let(:date_interval) do
|
||||||
|
(Date.parse('2010-01-01')..Date.parse('2010-12-31'))
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:quote) do
|
||||||
|
Interval.new(date: date_interval)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
quote.perform
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns rates' do
|
||||||
|
quote.formatted[:rates].wont_be :empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'quotes given date interval' do
|
||||||
|
Date.parse(quote.formatted[:start_date]).must_be :>=, date_interval.first
|
||||||
|
Date.parse(quote.formatted[:end_date]).must_be :<=, date_interval.last
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'quotes against the Euro' do
|
||||||
|
quote.formatted[:rates].each_value do |rates|
|
||||||
|
rates.keys.wont_include 'EUR'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sorts rates' do
|
||||||
|
quote.formatted[:rates].each_value do |rates|
|
||||||
|
rates.keys.must_equal rates.keys.sort
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has a cache key' do
|
||||||
|
quote.cache_key.wont_be :empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'given a new base' do
|
||||||
|
let(:quote) do
|
||||||
|
Interval.new(date: date_interval, base: 'USD')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'quotes against that base' do
|
||||||
|
quote.formatted[:rates].each_value do |rates|
|
||||||
|
rates.keys.wont_include 'USD'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sorts rates' do
|
||||||
|
quote.formatted[:rates].each_value do |rates|
|
||||||
|
rates.keys.must_equal rates.keys.sort
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'given symbols' do
|
||||||
|
let(:quote) do
|
||||||
|
Interval.new(date: date_interval, symbols: %w[USD GBP JPY])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'quotes only for those symbols' do
|
||||||
|
quote.formatted[:rates].each_value do |rates|
|
||||||
|
rates.keys.must_include 'USD'
|
||||||
|
rates.keys.wont_include 'CAD'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sorts rates' do
|
||||||
|
quote.formatted[:rates].each_value do |rates|
|
||||||
|
rates.keys.must_equal rates.keys.sort
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'when given an amount' do
|
||||||
|
let(:quote) do
|
||||||
|
Interval.new(date: date_interval, amount: 100)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'calculates quotes for that amount' do
|
||||||
|
quote.formatted[:rates].each_value do |rates|
|
||||||
|
rates['USD'].must_be :>, 10
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -1,53 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require_relative 'helper'
|
|
||||||
require 'quote'
|
|
||||||
|
|
||||||
describe Quote do
|
|
||||||
describe 'by default' do
|
|
||||||
it 'quotes against the Euro' do
|
|
||||||
quote = Quote.new
|
|
||||||
rates = quote.to_h[:rates]
|
|
||||||
rates.keys.wont_include 'EUR'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'when given a base' do
|
|
||||||
it 'quotes against that base' do
|
|
||||||
quote = Quote.new(base: 'USD')
|
|
||||||
rates = quote.to_h[:rates]
|
|
||||||
rates.keys.wont_include 'USD'
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'sorts rates' do
|
|
||||||
quote = Quote.new(base: 'USD')
|
|
||||||
rates = quote.to_h[:rates]
|
|
||||||
rates.keys.must_equal rates.keys.sort
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'when given symbols' do
|
|
||||||
it 'quotes rates only for given symbols' do
|
|
||||||
quote = Quote.new(symbols: ['USD'])
|
|
||||||
rates = quote.to_h[:rates]
|
|
||||||
rates.keys.must_include 'USD'
|
|
||||||
rates.keys.wont_include 'GBP'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'when given an amount' do
|
|
||||||
it 'quotes for that amount' do
|
|
||||||
quote = Quote.new(amount: 100)
|
|
||||||
rates = quote.to_h[:rates]
|
|
||||||
rates['USD'].must_be :>, 10
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'when given an amount and symbols' do
|
|
||||||
it 'quotes for that amount' do
|
|
||||||
quote = Quote.new(amount: 100, symbols: ['USD'])
|
|
||||||
rates = quote.to_h[:rates]
|
|
||||||
rates['USD'].must_be :>, 10
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
31
spec/roundable_spec.rb
Normal file
31
spec/roundable_spec.rb
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative 'helper'
|
||||||
|
require 'roundable'
|
||||||
|
|
||||||
|
describe Roundable do
|
||||||
|
include Roundable
|
||||||
|
|
||||||
|
it 'rounds values over 5,000 to zero decimal places' do
|
||||||
|
round(5000.123456).must_equal 5000.0
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'rounds values over 80 and below 5,000 to two decimal places' do
|
||||||
|
round(80.123456).must_equal 80.12
|
||||||
|
round(4999.123456).must_equal 4999.12
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'rounds values over 20 and below 80 to three decimal places' do
|
||||||
|
round(79.123456).must_equal 79.123
|
||||||
|
round(20.123456).must_equal 20.123
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'rounds values over 1 and below 20 to four decimal places' do
|
||||||
|
round(19.123456).must_equal 19.1235
|
||||||
|
round(1.123456).must_equal 1.1235
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'rounds values below 1 to five decimal places' do
|
||||||
|
round(0.123456).must_equal 0.12346
|
||||||
|
end
|
||||||
|
end
|
@ -49,17 +49,10 @@ describe 'the server' do
|
|||||||
json['rates'].wont_be :empty?
|
json['rates'].wont_be :empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns a cache control header' do
|
it 'returns an ETag' do
|
||||||
%w[/ /current /2012-11-20].each do |path|
|
|
||||||
get path
|
|
||||||
headers['Cache-Control'].wont_be_nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns a last modified header' do
|
|
||||||
%w[/current /2012-11-20].each do |path|
|
%w[/current /2012-11-20].each do |path|
|
||||||
get path
|
get path
|
||||||
headers['Last-Modified'].wont_be_nil
|
headers['ETag'].wont_be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -88,8 +81,8 @@ describe 'the server' do
|
|||||||
|
|
||||||
it 'returns rates for a given period' do
|
it 'returns rates for a given period' do
|
||||||
get '/2010-01-01..2010-12-31'
|
get '/2010-01-01..2010-12-31'
|
||||||
json['start_date'].must_equal '2010-01-01'
|
json['start_date'].wont_be :empty?
|
||||||
json['end_date'].must_equal '2010-12-31'
|
json['end_date'].wont_be :empty?
|
||||||
json['rates'].wont_be empty
|
json['rates'].wont_be :empty?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
Loading…
Reference in New Issue
Block a user