R/boj.R

Defines functions boj boj_request boj_error_body assert_boj_date parse_boj_date parse_boj_metadata parse_boj_data boj_metadata boj_next_position boj_data

Documented in boj_data boj_metadata

#' Fetch Bank of Japan (BoJ) data
#'
#' Retrieve time series data from the Bank of Japan Statistics API.
#'
#' @param db (`character(1)`)\cr
#'   The database code to query (e.g., `"FM08"` for foreign exchange rates). See the
#'   [API manual](https://www.stat-search.boj.or.jp/info/api_manual_en.pdf) for available databases.
#' @param code (`character()`)\cr
#'   One or more series codes to query (e.g., `"FXERD01"` for USD/JPY spot rate). Maximum 250
#'   codes per request. All codes must have the same frequency. Use [boj_metadata()] to find
#'   available codes.
#' @param start_date (`NULL` | `character(1)` | `integer(1)`)\cr
#'   Start date of the data. Format depends on frequency: `"YYYYMM"` for daily, weekly, and
#'   monthly data, `"YYYYQQ"` for quarterly (where QQ is 01-04), and `"YYYY"` for annual.
#'   If `NULL`, all available data is returned. Default `NULL`.
#' @param end_date (`NULL` | `character(1)` | `integer(1)`)\cr
#'   End date of the data, in the same format as start_date. If `NULL`, data up to the latest
#'   available date is returned. Default `NULL`.
#' @param lang (`character(1)`)\cr
#'   Language for series names, either `"en"` or `"jp"`. Default `"en"`.
#' @returns A [data.table::data.table()] with the requested data.
#' @source <https://www.stat-search.boj.or.jp/index_en.html>
#' @family data
#' @export
#' @examplesIf curl::has_internet()
#' \donttest{
#' # fetch USD/JPY exchange rate
#' boj_data("FM08", "FXERD01", start_date = "202401")
#'
#' # fetch multiple exchange rates
#' boj_data("FM08", c("FXERD01", "FXERD02"), start_date = "202401")
#' }
boj_data = function(db, code, start_date = NULL, end_date = NULL, lang = "en") {
  assert_string(db, min.chars = 1L)
  assert_character(code, min.chars = 1L, min.len = 1L, max.len = 250L)
  assert_boj_date(start_date)
  assert_boj_date(end_date)
  assert_choice(lang, c("en", "jp"))

  req = boj_request(
    "getDataCode",
    db = toupper(db),
    code = code,
    startDate = start_date,
    endDate = end_date,
    lang = lang
  )
  resps = req_perform_iterative(
    req,
    next_req = iterate_with_cursor("startPosition", boj_next_position),
    max_reqs = Inf
  )
  series = resps_data(resps, \(resp) resp_body_json(resp)$RESULTSET)
  parse_boj_data(list(RESULTSET = series))
}

boj_next_position = function(resp) {
  pos = resp_body_json(resp)$NEXTPOSITION
  if (length(pos) == 0L) NULL else pos
}

#' Fetch Bank of Japan (BoJ) metadata
#'
#' Retrieve series metadata from the Bank of Japan Statistics API.
#'
#' @param db (`character(1)`)\cr
#'   The database code to query (e.g., `"FM08"` for foreign exchange rates).
#' @param lang (`character(1)`)\cr
#'   Language for names, either `"en"` or `"jp"`. Default `"en"`.
#' @returns A [data.table::data.table()] with the requested metadata.
#' @source <https://www.stat-search.boj.or.jp/index_en.html>
#' @family metadata
#' @export
#' @examplesIf curl::has_internet()
#' \donttest{
#' boj_metadata("FM08")
#' }
boj_metadata = function(db, lang = "en") {
  assert_string(db, min.chars = 1L)
  assert_choice(lang, c("en", "jp"))

  json = boj("getMetadata", db = toupper(db), lang = lang)
  parse_boj_metadata(json)
}

parse_boj_data = function(json) {
  series = json$RESULTSET
  if (length(series) == 0L) {
    dt = data.table(
      date = as.Date(character()),
      id = character(),
      value = numeric(),
      freq = character(),
      name = character(),
      unit = character()
    )
    setnames(dt, "id", "key")
    return(dt)
  }

  dt = rbindlist(map(series, function(x) {
    dates = unlist(x$VALUES$SURVEY_DATES)
    values = as.numeric(map(x$VALUES$VALUES, \(x) x %||% NA_real_))
    freq = tolower(x$FREQUENCY)

    dt = data.table(
      date = parse_boj_date(dates, freq),
      id = x$SERIES_CODE,
      value = values,
      freq = boj_freq(freq),
      name = x$NAME_OF_TIME_SERIES %||% x$NAME_OF_TIME_SERIES_J %||% NA_character_,
      unit = x$UNIT %||% x$UNIT_J %||% NA_character_
    )
    na.omit(dt, cols = "value")
  }))
  setnames(dt, "id", "key")
  dt[]
}

parse_boj_metadata = function(json) {
  series = json$RESULTSET
  if (length(series) == 0L) {
    return(data.table(
      code = character(),
      name = character(),
      unit = character(),
      frequency = character(),
      category = character()
    ))
  }

  dt = rbindlist(map(series, function(x) {
    code = x$SERIES_CODE
    if (is.null(code) || !nzchar(code)) {
      return()
    }
    data.table(
      code = code,
      name = x$NAME_OF_TIME_SERIES %||% x$NAME_OF_TIME_SERIES_J %||% NA_character_,
      unit = x$UNIT %||% x$UNIT_J %||% NA_character_,
      frequency = x$FREQUENCY %||% NA_character_,
      category = x$CATEGORY %||% x$CATEGORY_J %||% NA_character_
    )
  }))
  dt[]
}

parse_boj_date = function(dates, freq) {
  switch(
    freq,
    daily = ,
    `weekly(monday)` = ,
    `weekly(tuesday)` = ,
    `weekly(wednesday)` = ,
    `weekly(thursday)` = ,
    `weekly(friday)` = ,
    `weekly(saturday)` = ,
    `weekly(sunday)` = {
      as.Date(as.character(dates), format = "%Y%m%d")
    },
    monthly = as.Date(paste0(dates, "01"), format = "%Y%m%d"),
    quarterly = {
      dates_chr = as.character(dates)
      year = substr(dates_chr, 1L, 4L)
      quarter = as.integer(substr(dates_chr, 5L, 6L))
      month = sprintf("%02d", (quarter - 1L) * 3L + 1L)
      as.Date(paste0(year, month, "01"), format = "%Y%m%d")
    },
    semiannual = {
      dates_chr = as.character(dates)
      year = substr(dates_chr, 1L, 4L)
      half = as.integer(substr(dates_chr, 5L, 6L))
      month = sprintf("%02d", (half - 1L) * 6L + 1L)
      as.Date(paste0(year, month, "01"), format = "%Y%m%d")
    },
    # the fiscal half-year starts in April: 01 = April-September, 02 = October-March
    `semiannual(sep)` = {
      dates_chr = as.character(dates)
      year = substr(dates_chr, 1L, 4L)
      half = as.integer(substr(dates_chr, 5L, 6L))
      month = sprintf("%02d", (half - 1L) * 6L + 4L)
      as.Date(paste0(year, month, "01"), format = "%Y%m%d")
    },
    annual = as.Date(sprintf("%s-01-01", dates), format = "%Y-%m-%d"),
    # the fiscal year starts in April
    `annual(mar)` = as.Date(sprintf("%s-04-01", dates), format = "%Y-%m-%d"),
    as.character(dates)
  )
}

boj_freq = function(freq) {
  switch(
    freq,
    daily = "daily",
    monthly = "monthly",
    quarterly = "quarterly",
    semiannual = ,
    `semiannual(sep)` = "semi-annual",
    annual = ,
    `annual(mar)` = "annual",
    `weekly(monday)` = ,
    `weekly(tuesday)` = ,
    `weekly(wednesday)` = ,
    `weekly(thursday)` = ,
    `weekly(friday)` = ,
    `weekly(saturday)` = ,
    `weekly(sunday)` = "weekly",
    freq
  )
}

assert_boj_date = function(x, .var.name = vname(x)) {
  assert(
    check_null(x),
    check_string(x, min.chars = 4L, pattern = "^\\d+$"),
    check_count(x, positive = TRUE),
    .var.name = .var.name
  )
  invisible(x)
}

boj_error_body = function(resp) {
  json = resp_body_json(resp)
  json$MESSAGE
}

boj_request = function(resource, ...) {
  base_request("https://www.stat-search.boj.or.jp/api/v1") |>
    req_url_path_append(resource) |>
    req_url_query(..., .multi = "comma") |>
    req_error(body = boj_error_body)
}

boj = function(resource, ...) {
  boj_request(resource, ...) |>
    req_perform() |>
    resp_body_json()
}

Try the bbk package in your browser

Any scripts or data that you put into this service are public.

bbk documentation built on Aug. 28, 2026, 5:07 p.m.