From 7c99f214e65d7e4bd1c61ba47080a172cb78fc0f Mon Sep 17 00:00:00 2001 From: Hakan Ensari Date: Wed, 20 Nov 2024 14:13:17 +0100 Subject: [PATCH] Ensure intuitive behavior when querying across holidays (#71) --- lib/bank/eurofxref-hist.xml | 2 +- lib/day.rb | 15 ++++++++++-- spec/day_spec.rb | 47 ++++++++++++++++++++++++++++--------- spec/quote/interval_spec.rb | 12 ++++++++-- 4 files changed, 60 insertions(+), 16 deletions(-) diff --git a/lib/bank/eurofxref-hist.xml b/lib/bank/eurofxref-hist.xml index 2896e5e..ad8228b 100644 --- a/lib/bank/eurofxref-hist.xml +++ b/lib/bank/eurofxref-hist.xml @@ -1 +1 @@ -Reference ratesEuropean Central Bank +Reference ratesEuropean Central Bank \ No newline at end of file diff --git a/lib/day.rb b/lib/day.rb index 6e176a0..ffde2a7 100644 --- a/lib/day.rb +++ b/lib/day.rb @@ -3,13 +3,24 @@ class Day < Sequel::Model dataset_module do def latest(date = Date.today) - where(date: select(:date).where(Sequel.lit("date <= ?", date)) + where(date: select(:date).where(Sequel[:date] <= date) .order(Sequel.desc(:date)) .limit(1)) end + # Returns rates for a given date interval + # + # If the start date falls on a holiday/weekend, rates start from the closest preceding business day. def between(interval) - where(date: interval) + return where(false) if interval.begin > Date.today + + previous_date = select(:date) + .where(Sequel[:date] <= interval.begin) + .order(Sequel.desc(:date)) + .limit(1) + + where(Sequel.expr(:date) >= Sequel.function(:coalesce, previous_date, interval.begin)) + .where(Sequel.expr(:date) <= interval.end) end def currencies diff --git a/spec/day_spec.rb b/spec/day_spec.rb index 0d28a4c..94405aa 100644 --- a/spec/day_spec.rb +++ b/spec/day_spec.rb @@ -5,29 +5,54 @@ require "day" describe Day do describe ".latest" do - it "returns latest rates before given date" do + it "returns latest available rates on given date" do + date = Date.parse("2010-01-04") + data = Day.latest(date) + _(data.to_a.sample.date).must_equal(date) + date = Date.parse("2010-01-01") data = Day.latest(date) - _(data.first.date).must_be(:<=, date) + _(data.to_a.sample.date).must_equal(Date.parse("2009-12-31")) end - it "returns nothing if there are no rates before given date" do - _(Day.latest(Date.parse("1998-01-01"))).must_be_empty + it "returns nothing if date predates dataset" do + _(Day.latest(Date.parse("1901-01-01"))).must_be_empty end end describe ".between" do - it "returns rates between given dates" do - start_date = Date.parse("2010-01-01") - end_date = Date.parse("2010-01-31") + it "returns rates between given working dates" do + start_date = Date.parse("2010-01-04") + end_date = Date.parse("2010-01-29") dates = Day.between((start_date..end_date)).map(:date).sort - _(dates.first).must_be(:>=, start_date) - _(dates.last).must_be(:<=, end_date) + _(dates.first).must_equal(start_date) + _(dates.last).must_equal(end_date) end - it "returns nothing if there are no rates between given dates" do - interval = (Date.parse("1998-01-01")..Date.parse("1998-01-31")) + it "starts on preceding business day if start date is a holiday" do + start_date = Date.parse("2024-11-03") + end_date = Date.parse("2024-11-04") + dates = Day.between((start_date..end_date)).map(:date) + _(dates).must_include(Date.parse("2024-11-01")) + end + + it "returns nothing if end date predates dataset" do + interval = (Date.parse("1901-01-01")..Date.parse("1901-01-31")) _(Day.between(interval)).must_be_empty end + + it "allows start date to predate dataset" do + start_date = Date.parse("1901-01-01") + end_date = Date.parse("2024-01-01") + dates = Day.between((start_date..end_date)).map(:date) + _(dates).wont_be_empty + end + + it "returns nothing if queried for the future" do + start_date = Date.today + 1 + end_date = start_date + 1 + dates = Day.between((start_date..end_date)).map(:date) + _(dates).must_be_empty + end end end diff --git a/spec/quote/interval_spec.rb b/spec/quote/interval_spec.rb index a590ccb..462950c 100644 --- a/spec/quote/interval_spec.rb +++ b/spec/quote/interval_spec.rb @@ -22,8 +22,16 @@ module Quote end it "quotes given date interval" do - _(Date.parse(quote.formatted[:start_date])).must_be(:>=, dates.first) - _(Date.parse(quote.formatted[:end_date])).must_be(:<=, dates.last) + returned_start = Date.parse(quote.formatted[:start_date]) + returned_end = Date.parse(quote.formatted[:end_date]) + + # The returned start date should be the closest working day (before or on) the requested start + _(returned_start).must_be(:<=, dates.first) + # But it shouldn't be too far back (maybe 10 business days) + _(returned_start).must_be(:>, dates.first - 10) + + # End date should still be within the requested range + _(returned_end).must_be(:<=, dates.last) end it "quotes against the Euro" do