context("calendrical computations - business days, day count fractions") # ################################################################### # test sample size NN <- 100L # test sample y <- sample(1990L:2020L, NN, replace = TRUE) m <- sample.int(12L, size = NN, replace = TRUE) d <- sample.int(31L, size = NN, replace = TRUE) dd <- suppressWarnings(tind(y = y, m = m, d = d)) mm <- as.month(dd) qq <- as.quarter(dd) yy <- as.year(dd) errmiscnv <- paste0("^", sQuote("convention"), " argument missing; ", "expected one of the following: ", dQuote("[a-zA-Z0-9/]+"), " \\([- a-zA-Z0-9/,\\.\\(\\)]+\\)(, ", dQuote("[a-zA-Z0-9/]+"), " \\([- a-zA-Z0-9/,\\.\\(\\)]+\\))+$") errinvcnv <- paste0("^invalid ", sQuote("convention"), " argument; ", "expected one of the following: ", dQuote("[a-zA-Z0-9/]+"), " \\([- a-zA-Z0-9/,\\.\\(\\)]+\\)(, ", dQuote("[a-zA-Z0-9/]+"), " \\([- a-zA-Z0-9/,\\.\\(\\)]+\\))+$") warnclnd <- paste0("^", sQuote("calendar"), " argument missing; using default settings ", "\\([- _[:alnum:]]+\\)$") warnlsmult <- "longer object length is not a multiple of shorter object length" # Polish calendar for tests .calendar_PL <- function(dd) { y <- year(dd) m <- month(dd) d <- day(dd) # public holidays newyear <- (m == 1) & (d == 1) epiphany <- (y >= 2011) & (m == 1) & (d == 6) easter <- easter(dd) == dd eastermon <- easter(dd) + 1 == dd labour <- (m == 5) & (d == 1) constitution <- (m == 5) & (d == 3) pentecost <- easter(dd) + 49 == dd corpuschristi <- easter(dd) + 60 == dd assumption <- (m == 8) & (d == 15) allsaints <- (m == 11) & (d == 1) independence <- (m == 11) & (d == 11) christmas <- (m == 12) & (d == 25) christmas2 <- (m == 12) & (d == 26) holiday <- newyear | epiphany | easter | eastermon | labour | constitution | pentecost | corpuschristi | assumption | allsaints | independence | christmas | christmas2 # holiday names names(holiday) <- rep("", length(holiday)) holnms <- c(newyear = "New Year", epiphany = "Epiphany", easter = "Easter", eastermon = "Easter Monday", labour = "Labour Day", constitution = "Constitution Day", pentecost = "Pentecost", corpuschristi = "Corpus Christi", assumption = "Assumption of Mary", allsaints = "All Saints Day", independence = "Independence Day", christmas = "Christmas", christmas2 = "2nd day of Christmas") lapply(names(holnms), function(nm) names(holiday)[get(nm)] <<- holnms[nm]) # working days work <- !holiday & (day_of_week(dd) <= 5L) return (list(work = work, holiday = holiday)) } # Mon-Fri .calendar_MonFri <- function(d) (day_of_week(d) <= 5L) test_that("'bizday' works correctly", { expect_identical(bizday(tind(type = "d"), "p"), tind(type = "d")) expect_identical(bizday(tind(length = 1L, type = "d"), "p"), tind(length = 1L, type = "d")) ina <- is.na(dd) ibd <- !ina & .calendar_PL(dd)[[1L]] inb <- !ina & !.calendar_PL(dd)[[1L]] for (conv in c("p", "mp", "f", "mf", "mf2")) { bd <- bizday(dd, conv, .calendar_PL) expect_identical(is.na(bd), ina) expect_identical(bd[ibd], dd[ibd]) expect_true(all(.calendar_PL(bd[!ina])[[1L]])) } bdp <- bizday(dd, "p", .calendar_PL)[!ina] bdmp <- bizday(dd, "mp", .calendar_PL)[!ina] bdf <- bizday(dd, "f", .calendar_PL)[!ina] bdmf <- bizday(dd, "mf", .calendar_PL)[!ina] bdmf2 <- bizday(dd, "mf2", .calendar_PL)[!ina] dd <- dd[!ina] # preceding expect_true(all(bdp <= dd)) expect_false(any(bizday_diff(bdp, dd, .calendar_PL, FALSE, TRUE) != 0)) # following expect_true(all(bdf >= dd)) expect_false(any(bizday_diff(dd, bdf, .calendar_PL, TRUE, FALSE) != 0)) # modified preceding impf <- (bdmp == bdf) & (bdmp != dd) expect_true(all(bdmp[!impf] == bdp[!impf])) expect_true(all(month(bdp[impf]) != month(dd[impf]))) # modified following imfp <- (bdmf == bdp) & (bdmf != dd) expect_true(all(bdmf[!imfp] == bdf[!imfp])) expect_true(all(month(bdf[imfp]) != month(dd[imfp]))) # modified following bimonthly imf2p <- (bdmf2 == bdp) & (bdmf2 != dd) expect_true(all(bdmf2[!imf2p] == bdf[!imf2p])) expect_true(all((month(bdf[imf2p]) != month(dd[imf2p])) | (day(bdf[imf2p]) > 15L) & (day(dd[imf2p]) <= 15L))) expect_error(bizday(dd), errmiscnv) expect_error(bizday(dd, "x"), errinvcnv) expect_warning(bdd <- bizday(dd, "p"), warnclnd) expect_identical(bdd, bizday(dd, "p", .calendar_MonFri)) err <- paste0("^invalid ", sQuote("convention"), " argument; ", "expected one of the following: ", dQuote("[a-z]"), " \\([- a-z]+\\)(, ", dQuote("[a-z0-9]+"), " \\([- a-z]+\\))+$") expect_error(bizday(dd, 1), err) }) test_that("'bizday_advance' works correctly", { nn <- sample(-NN:NN, NN, replace = TRUE) nn[sample.int(NN, 1L)] <- NA_integer_ err <- paste0("invalid ", sQuote("n"), " argument; expected an integer vector") expect_error(bizday_advance(dd, as.character(nn)), err, fixed = TRUE) expect_warning(bda <- bizday_advance(dd[1L:3L], nn[1L:2L], .calendar_PL), warnlsmult) expect_identical(bda, bizday_advance(dd[1L:3L], nn[c(1L:2L, 1L)], .calendar_PL)) expect_warning(bda <- bizday_advance(dd[1L:2L], nn[1L:3L], .calendar_PL), warnlsmult) expect_identical(bda, bizday_advance(dd[c(1L:2L, 1L)], nn[1L:3L], .calendar_PL)) expect_warning(bda <- bizday_advance(dd, nn), warnclnd) expect_identical(bda, bizday_advance(dd, nn, .calendar_MonFri)) expect_identical(bizday_advance(dd, nn[0L], .calendar_PL), tind(type = "d")) expect_identical(bizday_advance(dd[0L], nn, .calendar_PL), tind(type = "d")) expect_identical(bizday_advance(dd, nn[NA_integer_], .calendar_PL), tind(length = NN, type = "d")) expect_identical(bizday_advance(dd[NA_integer_], nn, .calendar_PL), tind(length = NN, type = "d")) bda <- bizday_advance(dd, nn, .calendar_PL) idb <- unname(.calendar_PL(dd)[[1L]]) expect_identical(bizday_diff(dd, bda, .calendar_PL, FALSE, TRUE), nn[ifelse(is.na(dd), NA, TRUE)] - (!nn & !idb)) }) test_that("'next_bizdays' works correctly", { errd <- paste0("invalid ", sQuote("d"), " argument; single non-NA date expected") errn <- paste0("invalid ", sQuote("n"), " argument; single non-NA integer expected") expect_error(next_bizdays(today() + 0:1, 1L), errd, fixed = TRUE) expect_error(next_bizdays(today()[NA], 1L), errd, fixed = TRUE) expect_error(next_bizdays(today(), -1L:1L), errn, fixed = TRUE) expect_error(next_bizdays(today(), NA), errn, fixed = TRUE) for (n in c(-sample.int(NN, 2L), 0L, sample.int(NN, 2L))) { td <- today() nb <- next_bizdays(td, n, .calendar_PL) expect_identical(length(nb), abs(n)) if (n > 0) { expect_false(is.unsorted(nb, strictly = TRUE)) sd <- seq(td + 1L, nb[length(nb)]) expect_identical(sum(.calendar_PL(sd)[[1L]]), n) } if (n < 0) { expect_false(is.unsorted(nb, strictly = TRUE)) sd <- seq(nb[1L], td - 1L) expect_identical(sum(.calendar_PL(sd)[[1L]]), -n) } } expect_warning(nbd <- next_bizdays(td, n), warnclnd) expect_identical(nbd, next_bizdays(td, n, .calendar_MonFri)) }) test_that("'first/last_bizday_in_month/quarter' work correctly", { fbm <- first_bizday_in_month(mm, .calendar_PL) lbm <- last_bizday_in_month(mm, .calendar_PL) expect_identical(as.month(fbm), mm) expect_identical(as.month(lbm), mm) expect_identical(is.na(fbm), is.na(mm)) expect_identical(is.na(lbm), is.na(mm)) expect_true(all(.calendar_PL(fbm[!is.na(fbm)])[[1L]])) expect_true(all(.calendar_PL(lbm[!is.na(fbm)])[[1L]])) iif <- fbm == as.date(mm) iif <- !is.na(iif) & iif expect_true(all(.calendar_PL(as.date(mm)[iif])[[1L]])) iil <- lbm == last_day_in_month(mm) iil <- !is.na(iil) & iil expect_true(all(.calendar_PL((as.date(mm + 1L) - 1L)[iil])[[1L]])) # quarters expect_identical(first_bizday_in_quarter(qq, .calendar_PL), first_bizday_in_month(as.month(qq), .calendar_PL)) expect_identical(last_bizday_in_quarter(qq, .calendar_PL), last_bizday_in_month(as.month(qq + 1L) - 1L, .calendar_PL)) }) test_that("'bizday_diff' works correctly", { expect_identical(bizday_diff(dd, tind(type = "d"), .calendar_PL), integer()) expect_identical(bizday_diff(tind(type = "d"), dd, .calendar_PL), integer()) expect_identical(bizday_diff(dd, tind(length = 1L, type = "d"), .calendar_PL), rep(NA_integer_, NN)) expect_identical(bizday_diff(tind(length = 1L, type = "d"), dd, .calendar_PL), rep(NA_integer_, NN)) expect_warning(bdd <- bizday_diff(dd[1L:2L], dd[1L:3L], .calendar_PL), warnlsmult) expect_identical(bdd, bizday_diff(dd[c(1L:2L, 1L)], dd[1L:3L], .calendar_PL)) expect_warning(bdd <- bizday_diff(dd[1L:3L], dd[1L:2L], .calendar_PL), warnlsmult) expect_identical(bdd, bizday_diff(dd[1L:3L], dd[c(1L:2L, 1L)], .calendar_PL)) expect_warning(bizday_diff(dd, dd), warnclnd) # simple, slow scalar implementation assuming `calendar` returns a list .bday_diff0 <- function(d0, d1, calendar, start.incl, end.incl) { if (is.na(d0) || is.na(d1)) return (NA_integer_) neg <- d0 > d1 dd <- d0 + 0L:as.integer(d1 - d0) bd <- calendar(dd)[[1L]] if (!start.incl) bd <- tail(bd, -1L) if (!end.incl) bd <- head(bd, -1L) return (if (neg) -sum(bd) else sum(bd)) } M <- 7L d0 <- head(dd, M) d1 <- tail(dd, M) for (si in c(FALSE, TRUE)) { for (ei in c(FALSE, TRUE)) { bddf <- sapply(1L:M, function(i) .bday_diff0(d0[i], d1[i], .calendar_PL, si, ei)) expect_identical(bizday_diff(d0, d1, .calendar_PL, si, ei), bddf) expect_identical(bizday_diff(d0, d1, .calendar_PL, si, ei), ifelse(!is.na(d0) & (d0 == d1), 1L, -1L) * bizday_diff(d1, d0, .calendar_PL, ei, si)) if (si && ei) { expect_identical(bizday_diff(d0, d0, .calendar_PL, si, ei), as.integer(.calendar_PL(d0)[[1L]])) } else { expect_identical(bizday_diff(d0, d0, .calendar_PL, si, ei), ifelse(is.na(d0), NA_integer_, 0L)) } } } }) test_that("'bizdays_in_month/quarter/year' work correctly", { M <- 7L bdm <- integer(M) for (i in 1:M) { d0 <- as.date(mm[i]) d1 <- as.date(mm[i] + 1L) - 1L dd <- if (!is.na(d0) && !is.na(d0)) d0 + 0L:as.integer(d1 - d0) else as.date(NA) bdm[i] <- sum(.calendar_PL(dd)[[1L]]) } expect_identical(bdm, bizdays_in_month(mm[1L:M], .calendar_PL)) bdq <- integer(M) for (i in 1:M) { d0 <- as.date(qq[i]) d1 <- as.date(qq[i] + 1L) - 1L dd <- if (!is.na(d0) && !is.na(d0)) d0 + 0L:as.integer(d1 - d0) else as.date(NA) bdq[i] <- sum(.calendar_PL(dd)[[1L]]) } expect_identical(bdq, bizdays_in_quarter(qq[1L:M], .calendar_PL)) bdy <- integer(M) for (i in 1:M) { d0 <- as.date(yy[i]) d1 <- as.date(yy[i] + 1L) - 1L dd <- if (!is.na(d0) && !is.na(d0)) d0 + 0L:as.integer(d1 - d0) else as.date(NA) bdy[i] <- sum(.calendar_PL(dd)[[1L]]) } expect_identical(bdy, bizdays_in_year(yy[1L:M], .calendar_PL)) }) test_that("'daycount_frac' works correctly", { d1 <- dd[-NN] d2 <- dd[-1L] expect_error(daycount_frac(d1, d2), errmiscnv) expect_error(daycount_frac(d1, d2, "44/777"), errinvcnv) # 30/360, 30E/360 expect_identical(bdd <- daycount_frac(tind(type = "d"), d2, "30/360"), numeric()) expect_identical(bdd <- daycount_frac(d1, tind(type = "d"), "30/360"), numeric()) expect_identical(bdd <- daycount_frac(tind(length = 1L, type = "d"), d2, "30/360"), rep(NA_real_, NN - 1L)) expect_identical(bdd <- daycount_frac(d1, tind(length = 1L, type = "d"), "30/360"), rep(NA_real_, NN - 1L)) expect_warning(bdd <- daycount_frac(d1[1L:2L], d2[1L:3L], "30/360"), warnlsmult) expect_identical(bdd, daycount_frac(d1[c(1L:2L, 1L)], d2[1L:3L], "30/360")) expect_warning(bdd <- daycount_frac(d1[1L:3L], d2[1L:2L], "30/360"), warnlsmult) expect_identical(bdd, daycount_frac(d1[1L:3L], d2[c(1L:2L, 1L)], "30/360")) df30u <- daycount_frac(d1, d2, "30/360 Bond Basis") df30e <- daycount_frac(d1, d2, "Eurobond basis") ii30na <- (day(d1) <= 30) & (day(d2) <= 30) | is.na(d1) | is.na(d2) expect_identical(df30u[ii30na], df30e[ii30na]) expect_equal(daycount_frac("2023-01-31", "2023-03-31", "30/360"), 1/6) expect_equal(daycount_frac("2023-01-31", "2023-03-31", "30E/360"), 1/6) expect_equal(daycount_frac("2023-01-31", "2023-03-30", "30/360"), 1/6) expect_equal(daycount_frac("2023-01-31", "2023-03-30", "30E/360"), 1/6) expect_equal(daycount_frac("2023-01-29", "2023-03-31", "30/360"), 1/6 + 2/360) expect_equal(daycount_frac("2023-01-29", "2023-03-31", "30E/360"), 1/6 + 1/360) # ACT/360, ACT/365F df30a0 <- daycount_frac(d1, d2, "ACT/360") df30a5 <- daycount_frac(d1, d2, "ACT/365F") expect_equal(df30a0, df30a5 * (365 / 360)) expect_equal(df30a0 * 360, as.numeric(d2 - d1)) # ACT/ACT # trivial non-vectorised implementation of ACT/ACT .actact <- function(d1, d2) { if (is.na(d1) || is.na(d2)) return (NA_real_) if (d1 == d2) return (0) if (d2 < d1) return (-.actact(d2, d1)) dd <- d1 + 0L:as.integer(d2 - d1 - 1L) dd6 <- is.leap_year(dd) dd5 <- !dd6 return (sum(dd6) / 366 + sum(dd5) / 365) } M <- 7L dfaa <- numeric(M) for (i in 1L:M) dfaa[i] <- .actact(dd[i], dd[i + 1L]) expect_equal(dfaa, daycount_frac(dd[1L:M], dd[1L + (1L:M)], "ACT/ACT")) })