tests/testthat/test-gaps.R

test_that("gapless_Datetimes works", {
  dataset <- 
    tibble::tibble(Id = c("A", "A", "A", "B", "B", "B"),
                   Datetime = lubridate::as_datetime(1) +
                     lubridate::days(c(0:2, 4, 6, 8)))
  
  expect_equal(dataset %>%
                 dplyr::group_by(Id) %>%
                 gapless_Datetimes() %>% nrow(),
               6)
  expect_equal(dataset %>%
                 dplyr::group_by(Id) %>%
                 gapless_Datetimes(epoch = "1 day") %>% nrow(),
               8)
  expect_equal(dataset %>%
                 gapless_Datetimes() %>% nrow(),
               5)  
  expect_equal(dataset %>%
                 gapless_Datetimes(epoch = "1 day") %>% nrow(),
               9)
  expect_equal(dataset %>%
                 gapless_Datetimes(epoch = "1 day") %>% ncol(),
               1)
  expect_equal(dataset %>%
                 dplyr::group_by(Id) %>%
                 gapless_Datetimes(epoch = "1 day") %>% ncol(),
               2)
  
})

test_that("gapless_Datetimes throws appropriate errors", {
  dataset <- 
    tibble::tibble(Id = c("A", "A", "A", "B", "B", "B"),
                   Datetime = lubridate::as_datetime(1) +
                     lubridate::days(c(0:2, 4, 6, 8)))
  expect_error(dataset$Id %>% gapless_Datetimes(),
               "dataset is not a dataframe")
  expect_error(dataset %>% gapless_Datetimes(Datetime.colname = Status),
               "Datetime.colname must be part of the dataset")
  expect_error(dataset %>% gapless_Datetimes(Datetime.colname = Id),
               "Datetime.colname must be a Datetime")
  expect_error(dataset %>% gapless_Datetimes(epoch = 5),
               "epoch must either be a duration or a string")
})


test_that("gap_handler works", {
  dataset <-
  tibble::tibble(Id = c("A", "A", "A", "B", "B", "B"),
                Datetime = lubridate::as_datetime(1) +
                           lubridate::days(c(0:2, 4, 6, 8)) +
                           lubridate::hours(c(0,12,rep(0,4))))
  
  expect_equal(dataset %>% gap_handler(epoch = "1 day") %>% dim(),c(10,3))
  expect_equal(dataset %>% 
                 gap_handler(epoch = "1 day", behavior = "irregulars") %>% 
                 dim(),c(1,3))
  expect_equal(dataset %>% 
                 gap_handler(epoch = "1 day", behavior = "gaps") %>% dim(),
               c(4,1))
  expect_equal(dataset %>% 
                 gap_handler(epoch = "1 day", behavior = "regulars") %>% dim(),
               c(5,3))
  expect_equal(dataset %>% dplyr::group_by(Id) %>% 
                 gap_handler(epoch = "1 day") %>% dim(),c(9,3))
  expect_equal(dataset %>% dplyr::group_by(Id) %>% 
                 gap_handler(epoch = "1 day", behavior = "irregulars") %>% 
                 dim(),c(1,3))
  expect_equal(dataset %>% dplyr::group_by(Id) %>% 
                 gap_handler(epoch = "1 day", behavior = "gaps") %>% dim(),
               c(3,2))
  expect_equal(dataset %>% dplyr::group_by(Id) %>% 
                 gap_handler(epoch = "1 day", behavior = "regulars") %>% dim(),
               c(5,3))

})

test_that("gapless_Datetimes throws appropriate errors", {
  dataset <- 
    tibble::tibble(Id = c("A", "A", "A", "B", "B", "B"),
                   Datetime = lubridate::as_datetime(1) +
                     lubridate::days(c(0:2, 4, 6, 8)))
  expect_error(dataset$Id %>% gap_handler(),
               "dataset is not a dataframe")
  expect_error(dataset %>% gap_handler(Datetime.colname = Status),
               "Datetime.colname must be part of the dataset")
  expect_error(dataset %>% gap_handler(Datetime.colname = Id),
               "Datetime.colname must be a Datetime")
  expect_error(dataset %>% gap_handler(epoch = 5),
               "epoch must either be a duration or a string")
  expect_error(dataset %>% gap_handler(behavior = "foo"),
               "'arg' should be one of ")
  
})

test_that("gap_finder works", {
  dataset <-
    tibble::tibble(Id = c("A", "A", "A", "B", "B", "B"),
                   Datetime = lubridate::as_datetime(1) +
                     lubridate::days(c(0:2, 4, 6, 8)) +
                     lubridate::hours(c(0,12,rep(0,4)))) %>% 
    dplyr::group_by(Id)
  
  expect_equal(gap_finder(dataset, gap.data = TRUE, silent = TRUE) %>% 
                 dim(),c(2,3))
  testthat::expect_message(gap_finder(dataset), 
                           regexp = "Found 2 gaps. 6 Datetimes fall into the regular sequence.")
  testthat::expect_message(gap_finder(dataset, epoch = "1 day"), 
                           regexp = "Found 3 gaps. 5 Datetimes fall into the regular sequence.")
})

#-----------

test_that("has_gaps works with ungrouped data", {
  # No gaps
  data_no_gaps <- tibble::tibble(
    Datetime = lubridate::as_datetime(0) + lubridate::hours(0:5)
  )
  expect_false(has_gaps(data_no_gaps, epoch = "1 hour"))
  
  # With gaps
  data_with_gaps <- tibble::tibble(
    Datetime = lubridate::as_datetime(0) + lubridate::hours(c(0, 1, 3, 5)) # gap between 1 and 3, 3 and 5
  )
  expect_true(has_gaps(data_with_gaps, epoch = "1 hour"))
  
  # Custom Datetime column name
  data_custom_dt <- tibble::tibble(
    MyTime = lubridate::as_datetime(0) + lubridate::days(c(0, 2))
  )
  expect_true(has_gaps(data_custom_dt, Datetime.colname = MyTime, epoch = "1 day"))
})

test_that("has_gaps works with grouped data", {
  # No gaps within groups
  data_grouped_no_gaps <- tibble::tibble(
    Id = rep(c("A", "B"), each = 3),
    Datetime = lubridate::as_datetime(0) + lubridate::minutes(c(0:2, 0:2))
  ) %>% dplyr::group_by(Id)
  expect_false(has_gaps(data_grouped_no_gaps, epoch = "1 min"))
  
  # Gaps in one group
  data_grouped_gaps_one <- tibble::tibble(
    Id = rep(c("A", "B"), each = 3),
    Datetime = lubridate::as_datetime(0) + lubridate::minutes(c(0, 1, 3, 0, 1, 2)) # gap in A
  ) %>% dplyr::group_by(Id)
  expect_true(has_gaps(data_grouped_gaps_one, epoch = "1 min"))
  
  # Gaps in all groups
  data_grouped_gaps_all <- tibble::tibble(
    Id = rep(c("A", "B"), each = 4),
    Datetime = lubridate::as_datetime(0) + lubridate::minutes(c(0, 2, 4, 8, 10, 12, 14, 18)) # gaps in A and B
  ) %>% dplyr::group_by(Id)
  expect_true(has_gaps(data_grouped_gaps_all, epoch = "2 mins"))
  
  
  # Groups span unconnected parts - considered a gap by default epoch
  data_unconnected_groups <- tibble::tibble(
    State = c("X", "X", "Y", "Y", "X", "X"),
    Datetime = lubridate::as_datetime(0) + lubridate::hours(c(0, 1, 10, 11, 20, 21))
  ) %>% dplyr::group_by(State)
  # dominant epoch for X will be 1 hour, but there's a large jump
  # dominant epoch for Y will be 1 hour.
  expect_true(has_gaps(data_unconnected_groups))
  
  
  # Single observation in a group
  dataset_single_obs_group <- tibble::tibble(
    Id = c("A", "A", "B"),
    Datetime = lubridate::as_datetime(0) + lubridate::days(c(0, 1, 0))
  ) %>% dplyr::group_by(Id)
  # Group B has only one observation, gap_handler filters it out, so no gaps reported for it.
  # Group A has no gaps.
  expect_false(has_gaps(dataset_single_obs_group, epoch = "1 day"))
  
  dataset_single_obs_group_with_gap <- tibble::tibble(
    Id = c("A", "A", "B", "A"),
    Datetime = lubridate::as_datetime(0) + lubridate::days(c(0, 2, 0, 3)) # A: 0,2,3 (gap); B: 0
  ) %>% dplyr::group_by(Id)
  expect_true(has_gaps(dataset_single_obs_group_with_gap, epoch = "1 day"))
  
})

test_that("has_gaps handles epoch correctly", {
  data <- tibble::tibble(
    Datetime = lubridate::as_datetime(0) + lubridate::days(c(0, 2, 3)) # gap if epoch is 1 day
  )
  expect_true(has_gaps(data, epoch = "1 day"))
  expect_true(has_gaps(data, epoch = "dominant.epoch"))
  
  data2 <- tibble::tibble(Datetime = lubridate::as_datetime(0) + lubridate::hours(c(0,2,4)))
  expect_false(has_gaps(data2, epoch = "2 hours"))
  expect_true(has_gaps(data2, epoch = "1 hour"))
  expect_false(has_gaps(data2, epoch = lubridate::duration(2, "hours")))
})

test_that("has_gaps error handling", {
  expect_error(has_gaps(list()), "dataset is not a dataframe")
  data <- tibble::tibble(D = lubridate::as_datetime(0))
  expect_error(has_gaps(data, Datetime.colname = NotDatetime), "Datetime.colname must be part of the dataset")
  data_wrong_type <- tibble::tibble(Datetime = 1:3)
  expect_error(has_gaps(data_wrong_type), "Datetime.colname must be a Datetime")
  data_ok <- tibble::tibble(Datetime = lubridate::as_datetime(0))
  expect_error(has_gaps(data_ok, epoch = 123), "epoch must either be a duration or a string")
})

# Tests for has_irregulars

test_that("has_irregulars works with ungrouped data", {
  # No irregulars
  data_no_irregulars <- tibble::tibble(
    Datetime = lubridate::as_datetime(0) + lubridate::hours(0:5)
  )
  expect_false(has_irregulars(data_no_irregulars, epoch = "1 hour"))
  
  # With irregulars
  data_with_irregulars <- tibble::tibble(
    Datetime = lubridate::as_datetime(0) + lubridate::dhours(c(0, 1.5, 2, 3.1, 4))
  )
  expect_true(has_irregulars(data_with_irregulars, epoch = "1 hour"))
  
  # Custom Datetime column name
  data_custom_dt <- tibble::tibble(
    MyTime = lubridate::as_datetime(0) + lubridate::ddays(c(0, 1.2, 2))
  )
  expect_true(has_irregulars(data_custom_dt, Datetime.colname = MyTime, epoch = "1 day"))
})

test_that("has_irregulars works with grouped data", {
  # No irregulars within groups
  data_grouped_no_irregulars <- tibble::tibble(
    Id = rep(c("A", "B"), each = 3),
    Datetime = lubridate::as_datetime(0) + lubridate::minutes(c(0:2, 10:12))
  ) %>% dplyr::group_by(Id)
  expect_false(has_irregulars(data_grouped_no_irregulars, epoch = "1 min"))
  
  # Irregulars in one group
  data_grouped_irregulars_one <- tibble::tibble(
    Id = rep(c("A", "B"), each = 3),
    Datetime = lubridate::as_datetime(0) + lubridate::dminutes(c(0, 1.5, 2, 10, 11, 12)) # irregular in A
  ) %>% dplyr::group_by(Id)
  expect_true(has_irregulars(data_grouped_irregulars_one, epoch = "1 min"))
  
  # Irregulars in all groups
  data_grouped_irregulars_all <- tibble::tibble(
    Id = rep(c("A", "B"), each = 3),
    Datetime = lubridate::as_datetime(0) + lubridate::dminutes(c(0, 2.2, 4, 10.1, 12, 14.9))
  ) %>% dplyr::group_by(Id)
  expect_true(has_irregulars(data_grouped_irregulars_all, epoch = "2 mins"))
  
  # Single observation in a group (cannot be irregular by itself with gapless_Datetimes current logic)
  dataset_single_obs_group <- tibble::tibble(
    Id = c("A", "A", "B"),
    Datetime = lubridate::as_datetime(0) + lubridate::ddays(c(0, 1, 0.5)) # B has 0.5
  ) %>% dplyr::group_by(Id)
  # Group B will be filtered by gapless_Datetimes, but remains in the output of gap_handler. Group A is regular.
  expect_true(has_irregulars(dataset_single_obs_group, epoch = "1 day"))
  
  
  dataset_single_obs_group_irreg <- tibble::tibble(
    Id = c("A", "A", "B", "A"),
    Datetime = lubridate::as_datetime(0) + lubridate::ddays(c(0, 1.5, 0, 2)) # A: 0, 1.5, 2 (1.5 is irreg); B: 0
  ) %>% dplyr::group_by(Id)
  # Group B is filtered by gapless_Datetimes. Group A has 1.5 which is irregular.
  expect_true(has_irregulars(dataset_single_obs_group_irreg, epoch = "1 day"))
})


test_that("has_irregulars handles epoch correctly", {
  data <- tibble::tibble(
    Datetime = lubridate::as_datetime(0) + lubridate::dhours(c(0, 1, 2.5, 4)) # 2.5 is irregular if epoch is 1 hour
  )
  expect_true(has_irregulars(data, epoch = "1 hour"))
  expect_false(has_irregulars(data, epoch = "30 mins")) # 0,1,2.5,4 -> 0, 60, 150, 240 mins. All are multiples of 30min
  expect_true(has_irregulars(data, epoch = lubridate::duration(1, "hour")))
  
  data2 <- tibble::tibble(Datetime = lubridate::as_datetime(0) + lubridate::dhours(c(0,2,4.1)))
  expect_true(has_irregulars(data2, epoch = "2 hours")) # 4.1 is irregular
  expect_true(has_irregulars(data2, epoch = "1 hour")) # 4.1 is irregular
})

test_that("has_irregulars error handling", {
  expect_error(has_irregulars(list()), "dataset is not a dataframe")
  data <- tibble::tibble(D = lubridate::as_datetime(0))
  expect_error(has_irregulars(data, Datetime.colname = NotDatetime), "Datetime.colname must be part of the dataset")
  data_wrong_type <- tibble::tibble(Datetime = 1:3)
  expect_error(has_irregulars(data_wrong_type), "Datetime.colname must be a Datetime")
  data_ok <- tibble::tibble(Datetime = lubridate::as_datetime(0))
  expect_error(has_irregulars(data_ok, epoch = 123), "epoch must either be a duration or a string")
})

# Tests for extract_gaps

test_that("extract_gaps handles implicit gaps correctly (no Variable.colname)", {
  # No gaps
  data_no_gaps <- tibble::tibble(
    Datetime = lubridate::as_datetime(0) + lubridate::hours(0:2)
  )
  expect_message(result_no_gaps <- extract_gaps(data_no_gaps, epoch = "1 hour", full.days = FALSE), "No implicit gaps found")
  expect_equal(nrow(result_no_gaps), 0)
  
  # One gap
  data_one_gap <- tibble::tibble(
    Datetime = lubridate::as_datetime(0) + lubridate::hours(c(0, 2)) # gap at 1 hour
  )
  result_one_gap <- extract_gaps(data_one_gap, epoch = "1 hour", full.days = FALSE)
  expect_equal(nrow(result_one_gap), 1)
  expect_equal(result_one_gap$gap.id, 1)
  expect_equal(result_one_gap$start, lubridate::as_datetime(0) + lubridate::hours(1) - lubridate::duration(30, "minutes"))
  expect_equal(result_one_gap$end, lubridate::as_datetime(0) + lubridate::hours(1) + lubridate::duration(30, "minutes"))
  expect_equal(result_one_gap$duration, lubridate::duration(1, "hour"))
  
  # Multiple gaps
  data_multi_gaps <- tibble::tibble(
    Datetime = lubridate::as_datetime(0) + lubridate::hours(c(0, 3, 4, 7)) # gaps at 1,2 and 5,6
  )
  result_multi_gaps <- extract_gaps(data_multi_gaps, epoch = "1 hour", full.days = FALSE)
  expect_equal(nrow(result_multi_gaps), 2)
  expect_equal(result_multi_gaps$gap.id, c(1, 2))
  expect_equal(result_multi_gaps$start[1], lubridate::as_datetime(0) + lubridate::hours(1) - lubridate::duration(30, "minutes"))
  expect_equal(result_multi_gaps$end[1], lubridate::as_datetime(0) + lubridate::hours(2) + lubridate::duration(30, "minutes"))
  expect_equal(result_multi_gaps$duration[1], lubridate::duration(2, "hours"))
  expect_equal(result_multi_gaps$start[2], lubridate::as_datetime(0) + lubridate::hours(5) - lubridate::duration(30, "minutes"))
  expect_equal(result_multi_gaps$end[2], lubridate::as_datetime(0) + lubridate::hours(6) + lubridate::duration(30, "minutes"))
  expect_equal(result_multi_gaps$duration[2], lubridate::duration(2, "hours"))
  
  # full.days = TRUE
  data_fd <- tibble::tibble(Datetime = lubridate::as_datetime("2023-01-01 10:00:00") + lubridate::days(c(0,2))) # gap on 2023-01-02
  res_fd <- extract_gaps(data_fd, epoch="10 hours", full.days = TRUE)
  expect_equal(nrow(res_fd), 3)
  expect_equal(res_fd$start[1], lubridate::as_datetime("2023-01-01 00:00:00") - lubridate::duration(5, "hours"))
  expect_equal(res_fd$end[1],lubridate::as_datetime("2023-01-01 00:00:00") + lubridate::duration(5, "hours")) # seq creates end of day, then + epoch/2
  
})

test_that("extract_gaps handles explicit gaps (with Variable.colname)", {
  # No NA, no implicit gaps
  data_no_na_no_implicit <- tibble::tibble(
    Datetime = lubridate::as_datetime(0) + lubridate::hours(0:2),
    Value = c(10, 20, 30)
  )
  expect_message(result_no_na <- extract_gaps(data_no_na_no_implicit, Value, epoch = "1 hour", include.implicit.gaps = FALSE, full.days=FALSE), "No gaps found")
  expect_equal(nrow(result_no_na), 0)
  
  # Has NA, no implicit gaps
  data_has_na_no_implicit <- tibble::tibble(
    Datetime = lubridate::as_datetime(0) + lubridate::hours(0:3),
    Value = c(10, NA, NA, 40)
  )
  expect_warning( # due to include.implicit.gaps = FALSE and has_gaps being false
    result_has_na <- extract_gaps(data_has_na_no_implicit, Value, epoch = "1 hour", include.implicit.gaps = FALSE, full.days=FALSE),
    NA # No warning because no implicit gaps
  )
  
  expect_equal(nrow(result_has_na), 1)
  expect_equal(result_has_na$gap.id, 1)
  expect_equal(result_has_na$start, lubridate::as_datetime(0) + lubridate::hours(1) - lubridate::duration(30, "minutes")) # epoch/2 before first NA
  expect_equal(result_has_na$end, lubridate::as_datetime(0) + lubridate::hours(2) + lubridate::duration(30, "minutes"))   # epoch/2 after last NA
  expect_equal(result_has_na$duration, lubridate::duration(2, "hours"))
  
  # No NA, but has implicit gaps, include.implicit.gaps = TRUE
  data_no_na_has_implicit_true <- tibble::tibble(
    Datetime = lubridate::as_datetime(0) + lubridate::hours(c(0, 2)), # implicit gap at 1h
    Value = c(10, 30)
  )
  result_implicit_true <- extract_gaps(data_no_na_has_implicit_true, Value, epoch = "1 hour", include.implicit.gaps = TRUE, full.days=FALSE)
  # gap_handler adds Datetime at 1h, Value is NA. This is one gap.
  expect_equal(nrow(result_implicit_true), 1)
  expect_equal(result_implicit_true$gap.id, 1)
  expect_equal(result_implicit_true$start, lubridate::as_datetime(0) + lubridate::hours(1) - lubridate::duration(30, "minutes"))
  expect_equal(result_implicit_true$end, lubridate::as_datetime(0) + lubridate::hours(1) + lubridate::duration(30, "minutes"))
  expect_equal(result_implicit_true$duration, lubridate::duration(1, "hour"))
  
  # No NA, but has implicit gaps, include.implicit.gaps = FALSE
  data_no_na_has_implicit_false <- tibble::tibble(
    Datetime = lubridate::as_datetime(0) + lubridate::hours(c(0, 2)), # implicit gap at 1h
    Value = c(10, 30)
  )
  expect_warning(
    expect_message(
    result_implicit_false <- extract_gaps(data_no_na_has_implicit_false, Value, epoch = "1 hour", include.implicit.gaps = FALSE, full.days=FALSE)),
    "There are implicit gaps in the dataset that will not be part of the extracted summary"
  )
  expect_equal(nrow(result_implicit_false), 0)
  
  
  # Has NA AND implicit gaps, include.implicit.gaps = TRUE
  data_na_and_implicit_true <- tibble::tibble(
    Datetime = lubridate::as_datetime(0) + lubridate::hours(c(0, 1, 3, 5)), # implicit gap at 2h, 4h
    Value = c(10, NA, 30, NA) # NA at 1h (explicit), NA at 5h (explicit)
  ) # After gap_handler: DTs: 0,1,2,3,4,5; Vals: 10,NA,NA(implicit),30,NA(implicit),NA
  # Gaps: (1h, 2h), (4h, 5h)
  result_na_implicit_true <- extract_gaps(data_na_and_implicit_true, Value, epoch = "1 hour", include.implicit.gaps = TRUE, full.days=FALSE)
  expect_equal(nrow(result_na_implicit_true), 2)
  expect_equal(result_na_implicit_true$gap.id, c(1,2))
  # Gap 1: Dts 1h (NA), 2h (implicit NA)
  expect_equal(result_na_implicit_true$start[1], lubridate::as_datetime(0) + lubridate::hours(1) - lubridate::duration(30, "minutes"))
  expect_equal(result_na_implicit_true$end[1], lubridate::as_datetime(0) + lubridate::hours(2) + lubridate::duration(30, "minutes"))
  expect_equal(result_na_implicit_true$duration[1], lubridate::duration(2, "hours"))
  # Gap 2: Dts 4h (implicit NA), 5h (NA)
  expect_equal(result_na_implicit_true$start[2], lubridate::as_datetime(0) + lubridate::hours(4) - lubridate::duration(30, "minutes"))
  expect_equal(result_na_implicit_true$end[2], lubridate::as_datetime(0) + lubridate::hours(5) + lubridate::duration(30, "minutes"))
  expect_equal(result_na_implicit_true$duration[2], lubridate::duration(2, "hours"))
  
  
  # Has NA AND implicit gaps, include.implicit.gaps = FALSE
  data_na_and_implicit_false <- tibble::tibble(
    Datetime = lubridate::as_datetime(0) + lubridate::hours(c(0, 1, 3, 5)), # implicit gap at 2h, 4h
    Value = c(10, NA, 30, NA) # NA at 1h, NA at 5h
  )
  expect_warning(
    result_na_implicit_false <- extract_gaps(data_na_and_implicit_false, Value, epoch = "1 hour", include.implicit.gaps = FALSE, full.days=FALSE),
    "There are implicit gaps in the dataset that will not be part of the extracted summary"
  )
  # Only considers original NAs at 1h and 5h. These are separate gaps as per extract_states logic on original data.
  expect_equal(nrow(result_na_implicit_false), 2)
  expect_equal(result_na_implicit_false$gap.id, c(1,2))
  expect_equal(result_na_implicit_false$start[1], lubridate::as_datetime(0) + lubridate::hours(1) - lubridate::duration(30, "minutes"))
  expect_equal(result_na_implicit_false$end[1], lubridate::as_datetime(0) + lubridate::hours(1) + lubridate::duration(30, "minutes"))
  expect_equal(result_na_implicit_false$duration[1], lubridate::duration(1, "hour"))
  expect_equal(result_na_implicit_false$start[2], lubridate::as_datetime(0) + lubridate::hours(5) - lubridate::duration(30, "minutes"))
  expect_equal(result_na_implicit_false$end[2], lubridate::as_datetime(0) + lubridate::hours(5) + lubridate::duration(30, "minutes"))
  expect_equal(result_na_implicit_false$duration[2], lubridate::duration(1, "hour"))
})


test_that("extract_gaps works with grouped data", {
  # Implicit gaps, grouped
  data_grouped_implicit <- tibble::tibble(
    Id = rep(c("A", "B"), each = 2),
    Datetime = lubridate::as_datetime(0) + lubridate::hours(c(0, 2, 10, 12)) # gap at 1h for A, 11h for B
  ) %>% dplyr::group_by(Id)
  result_grp_implicit <- extract_gaps(data_grouped_implicit, epoch = "1 hour", full.days = FALSE)
  expect_equal(nrow(result_grp_implicit), 2)
  expect_equal(result_grp_implicit$Id, c("A", "B"))
  expect_equal(result_grp_implicit$gap.id, c(1, 1)) # gap.id is per group here
  expect_equal(result_grp_implicit$start[result_grp_implicit$Id=="A"], lubridate::as_datetime(0) + lubridate::hours(1) - lubridate::duration(30, "minutes"))
  expect_equal(result_grp_implicit$duration[result_grp_implicit$Id=="A"], lubridate::duration(1, "hour"))
  expect_equal(result_grp_implicit$start[result_grp_implicit$Id=="B"], lubridate::as_datetime(0) + lubridate::hours(11) - lubridate::duration(30, "minutes"))
  expect_equal(result_grp_implicit$duration[result_grp_implicit$Id=="B"], lubridate::duration(1, "hour"))
  
  # Explicit gaps, grouped, include.implicit.gaps = TRUE
  data_grouped_explicit_implicit_T <- tibble::tibble(
    Id = rep(c("A", "B"), each = 3),
    Datetime = lubridate::as_datetime(0) + lubridate::hours(c(0, 1, 3, 10, 12, 13)), # A implicit at 2h; B implicit at 11h
    Value = c(1, NA, 3, 10, NA, 30) # A explicit NA at 1h; B explicit NA at 12h
  ) %>% dplyr::group_by(Id)
  # For A: gap_handler DTs: 0,1,2,3; Vals: 1,NA,NA(impl),3 -> Gaps at 1h,2h
  # For B: gap_handler DTs: 10,11,12,13; Vals: 10,NA(impl),NA,30 -> Gaps at 11h,12h
  result_grp_exp_impl_T <- extract_gaps(data_grouped_explicit_implicit_T, Value, epoch = "1 hour", include.implicit.gaps = TRUE, full.days = FALSE)
  expect_equal(nrow(result_grp_exp_impl_T), 2) # One combined gap per group
  expect_equal(result_grp_exp_impl_T$Id, c("A", "B"))
  expect_equal(result_grp_exp_impl_T$gap.id, c(1,1))
  
  # Check A's gap (1h explicit, 2h implicit)
  res_A <- dplyr::filter(result_grp_exp_impl_T, Id == "A")
  expect_equal(res_A$start, lubridate::as_datetime(0) + lubridate::hours(1) - lubridate::duration(30, "minutes"))
  expect_equal(res_A$end, lubridate::as_datetime(0) + lubridate::hours(2) + lubridate::duration(30, "minutes"))
  expect_equal(res_A$duration, lubridate::duration(2, "hours"))
  
  # Check B's gap (11h implicit, 12h explicit)
  res_B <- dplyr::filter(result_grp_exp_impl_T, Id == "B")
  expect_equal(res_B$start, lubridate::as_datetime(0) + lubridate::hours(11) - lubridate::duration(30, "minutes"))
  expect_equal(res_B$end, lubridate::as_datetime(0) + lubridate::hours(12) + lubridate::duration(30, "minutes"))
  expect_equal(res_B$duration, lubridate::duration(2, "hours"))
  
  
  # Explicit gaps, grouped, include.implicit.gaps = FALSE
  data_grouped_explicit_implicit_F <- tibble::tibble(
    Id = rep(c("A", "B"), each = 3),
    Datetime = lubridate::as_datetime(0) + lubridate::hours(c(0, 1, 3, 10, 12, 13)), # A implicit at 2h; B implicit at 11h
    Value = c(1, NA, 3, 10, NA, 30) # A explicit NA at 1h; B explicit NA at 12h
  ) %>% dplyr::group_by(Id)
  
  expect_warning(
    result_grp_exp_impl_F <- extract_gaps(data_grouped_explicit_implicit_F, Value, epoch = "1 hour", include.implicit.gaps = FALSE, full.days = FALSE),
    "There are implicit gaps in the dataset that will not be part of the extracted summary"
  )
  # Only original NAs considered.
  # For A: NA at 1h
  # For B: NA at 12h
  expect_equal(nrow(result_grp_exp_impl_F), 2)
  expect_equal(result_grp_exp_impl_F$Id, c("A", "B"))
  expect_equal(result_grp_exp_impl_F$gap.id, c(1,1)) # gap.id is per group here
  
  res_A_F <- dplyr::filter(result_grp_exp_impl_F, Id == "A")
  expect_equal(res_A_F$start, lubridate::as_datetime(0) + lubridate::hours(1) - lubridate::duration(30, "minutes"))
  expect_equal(res_A_F$end, lubridate::as_datetime(0) + lubridate::hours(1) + lubridate::duration(30, "minutes"))
  expect_equal(res_A_F$duration, lubridate::duration(1, "hour"))
  
  res_B_F <- dplyr::filter(result_grp_exp_impl_F, Id == "B")
  expect_equal(res_B_F$start, lubridate::as_datetime(0) + lubridate::hours(12) - lubridate::duration(30, "minutes"))
  expect_equal(res_B_F$end, lubridate::as_datetime(0) + lubridate::hours(12) + lubridate::duration(30, "minutes"))
  expect_equal(res_B_F$duration, lubridate::duration(1, "hour"))
  
  
  # Single observation in one group (filtered out by gap_finder if no Variable.colname)
  dataset_single_obs_group <- tibble::tibble(
    Id = c("A", "A", "B", "A"),
    Datetime = lubridate::as_datetime(0) + lubridate::days(c(0, 2, 0, 3)), # A: 0,2,3 (gap at day 1); B: 0
    Value = c(1,2,10,3)
  ) %>% dplyr::group_by(Id)
  
  res_single_implicit <- extract_gaps(dataset_single_obs_group, epoch = "1 day", full.days=FALSE) # No Variable.colname
  # Group B is filtered by gapless_Datetimes.
  # Group A has gap at day 1.
  expect_equal(nrow(res_single_implicit), 1)
  expect_equal(res_single_implicit$Id, "A")
  expect_equal(res_single_implicit$start, lubridate::as_datetime(0) + lubridate::days(1) - lubridate::duration(12, "hours"))
  
  res_single_explicit <- extract_gaps(dataset_single_obs_group, Value, epoch = "1 day", include.implicit.gaps = TRUE, full.days=FALSE)
  # gap_handler for A: DTs 0,1,2,3; Vals 1,NA,2,3. Gap for A at day 1.
  # gap_handler for B: DT 0; Val 10. No gaps for B.
  expect_equal(nrow(res_single_explicit), 1)
  expect_equal(res_single_explicit$Id, "A")
  expect_equal(res_single_explicit$start, lubridate::as_datetime(0) + lubridate::days(1) - lubridate::duration(12, "hours"))
  expect_equal(res_single_explicit$duration, lubridate::duration(1, "day"))
  
})

test_that("extract_gaps error handling", {
  expect_error(extract_gaps(list()), "dataset is not a dataframe")
  data <- tibble::tibble(D = lubridate::as_datetime(0), V = 1)
  expect_error(extract_gaps(data, Datetime.colname = NotDatetime), "Datetime.colname must be part of the dataset")
  data_wrong_type <- tibble::tibble(Datetime = 1:3, V=1)
  expect_error(extract_gaps(data_wrong_type), "Datetime.colname must be a Datetime")
  data_ok <- tibble::tibble(Datetime = lubridate::as_datetime(0), V=1)
  expect_error(extract_gaps(data_ok, epoch = 123), "epoch must either be a duration or a string")
  expect_error(extract_gaps(data_ok, Variable.colname = NotValue), "Variable.colname must be part of the dataset")
})

Try the LightLogR package in your browser

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

LightLogR documentation built on June 10, 2025, 5:12 p.m.