172 lines
5.3 KiB
Ruby
172 lines
5.3 KiB
Ruby
|
|
# frozen_string_literal: true
|
||
|
|
|
||
|
|
require_relative "../../helper"
|
||
|
|
require "provider/adapters/imf"
|
||
|
|
|
||
|
|
class Provider < Sequel::Model(:providers)
|
||
|
|
module Adapters
|
||
|
|
describe IMF do
|
||
|
|
before do
|
||
|
|
VCR.insert_cassette("imf", match_requests_on: [:method, :uri])
|
||
|
|
end
|
||
|
|
|
||
|
|
after do
|
||
|
|
VCR.eject_cassette
|
||
|
|
end
|
||
|
|
|
||
|
|
let(:adapter) { IMF.new }
|
||
|
|
|
||
|
|
it "fetches rates across both Currency blocks in the TSV response" do
|
||
|
|
dataset = adapter.fetch(after: Date.new(2026, 3, 1), upto: Date.new(2026, 3, 31))
|
||
|
|
dates = dataset.map { |r| r[:date] }.uniq
|
||
|
|
|
||
|
|
_(dates).must_include(Date.new(2026, 3, 17))
|
||
|
|
_(dates.size).must_be(:>, 11)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "fetches multiple currencies per date without duplicating records" do
|
||
|
|
dataset = adapter.fetch(after: Date.new(2026, 3, 1), upto: Date.new(2026, 3, 31))
|
||
|
|
dates = dataset.map { |r| r[:date] }.uniq
|
||
|
|
sample = dataset.select { |r| r[:date] == dates.first }
|
||
|
|
|
||
|
|
_(sample.size).must_be(:>, 1)
|
||
|
|
|
||
|
|
keys = dataset.map { |r| [r[:date], r[:base], r[:quote]] }
|
||
|
|
|
||
|
|
_(keys.size).must_equal(keys.uniq.size)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "parses indirect quotes with (1) suffix" do
|
||
|
|
tsv = <<~TSV
|
||
|
|
Representative Exchange Rates for Selected Currencies for January 2026
|
||
|
|
Currency\tJanuary 02, 2026\tJanuary 05, 2026
|
||
|
|
Euro(1)\t1.1698\t1.1606
|
||
|
|
Japanese yen\t156.40\t157.41
|
||
|
|
U.S. dollar\t1.0000\t1.0000
|
||
|
|
TSV
|
||
|
|
|
||
|
|
records = adapter.parse(tsv)
|
||
|
|
|
||
|
|
eur = records.select { |r| r[:base] == "EUR" || r[:quote] == "EUR" }
|
||
|
|
|
||
|
|
_(eur.first[:base]).must_equal("EUR")
|
||
|
|
_(eur.first[:quote]).must_equal("USD")
|
||
|
|
|
||
|
|
jpy = records.select { |r| r[:base] == "USD" && r[:quote] == "JPY" }
|
||
|
|
|
||
|
|
_(jpy.first[:rate]).must_equal(156.4)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "parses KWD as direct quote (units per USD)" do
|
||
|
|
tsv = <<~TSV
|
||
|
|
Representative Exchange Rates for Selected Currencies for March 2026
|
||
|
|
Currency\tMarch 02, 2026
|
||
|
|
Kuwaiti dinar\t0.306600
|
||
|
|
TSV
|
||
|
|
|
||
|
|
records = adapter.parse(tsv)
|
||
|
|
kwd = records.find { |r| r[:quote] == "KWD" || r[:base] == "KWD" }
|
||
|
|
|
||
|
|
_(kwd[:base]).must_equal("USD")
|
||
|
|
_(kwd[:quote]).must_equal("KWD")
|
||
|
|
_(kwd[:rate]).must_equal(0.3066)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "skips USD rows" do
|
||
|
|
tsv = <<~TSV
|
||
|
|
Representative Exchange Rates for Selected Currencies for January 2026
|
||
|
|
Currency\tJanuary 02, 2026
|
||
|
|
U.S. dollar\t1.0000
|
||
|
|
Euro(1)\t1.1698
|
||
|
|
TSV
|
||
|
|
|
||
|
|
records = adapter.parse(tsv)
|
||
|
|
|
||
|
|
_(records.none? { |r| r[:base] == "USD" && r[:quote] == "USD" }).must_equal(true)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "parses Continued blocks with their own date header" do
|
||
|
|
tsv = <<~TSV
|
||
|
|
Representative Exchange Rates for Selected Currencies for March 2026
|
||
|
|
Currency\tMarch 02, 2026\tMarch 03, 2026
|
||
|
|
Chinese yuan\t6.882900\t6.897100
|
||
|
|
U.S. dollar\t1.000000\t1.000000
|
||
|
|
|
||
|
|
Representative Exchange Rates for Selected Currencies for March 2026 Continued
|
||
|
|
|
||
|
|
Currency\tMarch 17, 2026\tMarch 18, 2026
|
||
|
|
Chinese yuan\t6.888300\t6.875600
|
||
|
|
U.S. dollar\t1.000000\t1.000000
|
||
|
|
TSV
|
||
|
|
|
||
|
|
records = adapter.parse(tsv)
|
||
|
|
cny = records.select { |r| r[:quote] == "CNY" }.sort_by { |r| r[:date] }
|
||
|
|
|
||
|
|
_(cny.map { |r| r[:date] }).must_equal([
|
||
|
|
Date.new(2026, 3, 2),
|
||
|
|
Date.new(2026, 3, 3),
|
||
|
|
Date.new(2026, 3, 17),
|
||
|
|
Date.new(2026, 3, 18),
|
||
|
|
])
|
||
|
|
_(cny.find { |r| r[:date] == Date.new(2026, 3, 17) }[:rate]).must_equal(6.8883)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "does not emit duplicate (date, base, quote) records across blocks" do
|
||
|
|
tsv = <<~TSV
|
||
|
|
Currency\tMarch 02, 2026\tMarch 03, 2026
|
||
|
|
Chinese yuan\t6.882900\t6.897100
|
||
|
|
|
||
|
|
Currency\tMarch 17, 2026\tMarch 18, 2026
|
||
|
|
Chinese yuan\t6.888300\t6.875600
|
||
|
|
TSV
|
||
|
|
|
||
|
|
records = adapter.parse(tsv)
|
||
|
|
keys = records.map { |r| [r[:date], r[:base], r[:quote]] }
|
||
|
|
|
||
|
|
_(keys.size).must_equal(keys.uniq.size)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "emits SDR cross rates only as USD/XDR" do
|
||
|
|
dataset = adapter.fetch(after: Date.new(2026, 3, 1), upto: Date.new(2026, 3, 31))
|
||
|
|
sdr = dataset.select { |r| r[:quote] == "XDR" }
|
||
|
|
|
||
|
|
_(sdr).wont_be_empty
|
||
|
|
_(sdr.map { |r| r[:base] }.uniq).must_equal(["USD"])
|
||
|
|
end
|
||
|
|
|
||
|
|
it "parses USD/XDR from SDR cross rates and ignores other currencies" do
|
||
|
|
tsv = <<~TSV
|
||
|
|
SDRs per Currency unit for April 2026
|
||
|
|
Currency\tApril 01, 2026\tApril 02, 2026
|
||
|
|
Euro\t0.8507900000\t0.8483160000
|
||
|
|
Japanese yen\t0.0046154900\t0.0046392700
|
||
|
|
U.S. dollar\t0.7331240000\t0.7360660000
|
||
|
|
TSV
|
||
|
|
|
||
|
|
records = adapter.parse_sdrcv(tsv)
|
||
|
|
|
||
|
|
_(records.map { |r| r[:base] }.uniq).must_equal(["USD"])
|
||
|
|
_(records.size).must_equal(2)
|
||
|
|
|
||
|
|
usd = records.find { |r| r[:date] == Date.new(2026, 4, 1) }
|
||
|
|
|
||
|
|
_(usd[:quote]).must_equal("XDR")
|
||
|
|
_(usd[:rate]).must_equal(0.7331240)
|
||
|
|
end
|
||
|
|
|
||
|
|
it "skips SDR rows with NA values" do
|
||
|
|
tsv = <<~TSV
|
||
|
|
SDRs per Currency unit for April 2026
|
||
|
|
Currency\tApril 01, 2026\tApril 02, 2026
|
||
|
|
U.S. dollar\tNA\t0.7360660000
|
||
|
|
TSV
|
||
|
|
|
||
|
|
records = adapter.parse_sdrcv(tsv)
|
||
|
|
|
||
|
|
_(records.size).must_equal(1)
|
||
|
|
_(records.first[:date]).must_equal(Date.new(2026, 4, 2))
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|
||
|
|
end
|