# frozen_string_literal: true require "json" require "net/http" require "provider/adapters/adapter" class Provider module Adapters # Bank of Japan. Fetches daily spot exchange rates for USD/JPY and EUR/USD # from the Tokyo market via the BOJ Statistics API. No authentication required. # Only 2 pairs — this is the full extent of what BOJ publishes via this API. class BOJ < Adapter API_URL = "https://www.stat-search.boj.or.jp/api/v1/getDataCode" # series_code => { base:, quote: } SERIES = { "FXERD04" => { base: "USD", quote: "JPY" }, "FXERD34" => { base: "EUR", quote: "USD" }, }.freeze def fetch(after: nil, upto: nil) effective_after = after effective_upto = upto || Date.today url = URI(API_URL) params = { format: "json", lang: "en", db: "FM08", code: SERIES.keys.join(","), startDate: effective_after.strftime("%Y%m"), endDate: effective_upto.strftime("%Y%m"), } url.query = URI.encode_www_form(params) response = Net::HTTP.get(url) raw = parse(response) raw.select { |r| r[:date].between?(effective_after, effective_upto) } end def parse(json) data = JSON.parse(json) resultset = data["RESULTSET"] || [] resultset.flat_map do |series| meta = SERIES[series["SERIES_CODE"]] next [] unless meta values_data = series.dig("VALUES") || {} dates = values_data["SURVEY_DATES"] || [] rates = values_data["VALUES"] || [] dates.zip(rates).filter_map do |raw_date, value| next if value.nil? rate = Float(value) next if rate.zero? date = Date.strptime(raw_date.to_s, "%Y%m%d") { date:, base: meta[:base], quote: meta[:quote], rate: } end end end end end end