tests/testthat/test-utils-dates.R

# Tests for date utility functions

test_that("dow returns correct day of week", {
  # Sunday = 0
 expect_equal(dow(as.Date("2024-01-07")), 0)  # Sunday
  # Monday = 1
  expect_equal(dow(as.Date("2024-01-08")), 1)  # Monday
  # Saturday = 6
  expect_equal(dow(as.Date("2024-01-06")), 6)  # Saturday
})

test_that("is_leap_year identifies leap years correctly", {
  expect_true(is_leap_year(2024))
  expect_true(is_leap_year(2000))
  expect_false(is_leap_year(2023))
  expect_false(is_leap_year(1900))
})

test_that("yyyymm creates correct format", {
  expect_equal(yyyymm(2024, 1), 202401L)
  expect_equal(yyyymm(2024, 12), 202412L)
  expect_equal(yyyymm(2023, 6), 202306L)
})

test_that("first_valid_saturday calculates correctly", {
  # January 2024: starts on Monday (dow=1)
  # First Saturday is day 6
  # With min_days=4, need at least 4 days in week, so day 6 is OK (6 days in month)
  expect_equal(first_valid_saturday(2024, 1, min_days = 4), 6)

  # March 2024: starts on Friday (dow=5)
  # First Saturday is day 2 (only 2 days in March)
  # With min_days=4, need second Saturday (day 9)
  expect_equal(first_valid_saturday(2024, 3, min_days = 4), 9)

  # With min_days=3, first Saturday (day 2) might work
  # 2 days is less than 3, so still need second Saturday
  expect_equal(first_valid_saturday(2024, 3, min_days = 3), 9)
})

test_that("first_saturday_on_or_after works correctly", {
  # If date is Saturday, return same date
  expect_equal(first_saturday_on_or_after(as.Date("2024-01-06")), as.Date("2024-01-06"))

  # If date is Sunday, return next Saturday (6 days later)
  expect_equal(first_saturday_on_or_after(as.Date("2024-01-07")), as.Date("2024-01-13"))

  # If date is Monday, return Saturday (5 days later)
  expect_equal(first_saturday_on_or_after(as.Date("2024-01-08")), as.Date("2024-01-13"))
})

# =============================================================================
# DAY OF WEEK EDGE CASES
# =============================================================================

test_that("dow handles year boundaries correctly", {
  # Test year transitions
  expect_equal(dow(as.Date("2023-12-31")), 0)  # Sunday
  expect_equal(dow(as.Date("2024-01-01")), 1)  # Monday

  # Test epoch date (1970-01-01 = Thursday)
  expect_equal(dow(as.Date("1970-01-01")), 4)  # Thursday
})

test_that("dow works for all days of the week", {
  # Test a complete week in January 2024
  expect_equal(dow(as.Date("2024-01-07")), 0)  # Sunday
  expect_equal(dow(as.Date("2024-01-08")), 1)  # Monday
  expect_equal(dow(as.Date("2024-01-09")), 2)  # Tuesday
  expect_equal(dow(as.Date("2024-01-10")), 3)  # Wednesday
  expect_equal(dow(as.Date("2024-01-11")), 4)  # Thursday
  expect_equal(dow(as.Date("2024-01-12")), 5)  # Friday
  expect_equal(dow(as.Date("2024-01-06")), 6)  # Saturday
})

# =============================================================================
# LEAP YEAR EDGE CASES
# =============================================================================

test_that("is_leap_year handles century years correctly", {
  # Century years divisible by 400 are leap years
  expect_true(is_leap_year(2000))
  expect_true(is_leap_year(2400))

  # Century years not divisible by 400 are NOT leap years
  expect_false(is_leap_year(1900))
  expect_false(is_leap_year(2100))
  expect_false(is_leap_year(2200))
  expect_false(is_leap_year(2300))
})

test_that("is_leap_year handles standard leap year pattern", {
  # Regular leap years (divisible by 4, not century)
  expect_true(is_leap_year(2020))
  expect_true(is_leap_year(2024))
  expect_true(is_leap_year(2028))

  # Non-leap years
  expect_false(is_leap_year(2021))
  expect_false(is_leap_year(2022))
  expect_false(is_leap_year(2023))
})

# =============================================================================
# QUARTER HELPER FUNCTIONS
# =============================================================================

test_that("quarter_first_month returns correct first month", {
  expect_equal(PNADCperiods:::quarter_first_month(1), 1)   # Q1 starts in Jan
  expect_equal(PNADCperiods:::quarter_first_month(2), 4)   # Q2 starts in Apr
  expect_equal(PNADCperiods:::quarter_first_month(3), 7)   # Q3 starts in Jul
  expect_equal(PNADCperiods:::quarter_first_month(4), 10)  # Q4 starts in Oct
})

test_that("quarter_month_n returns correct n-th month of quarter", {
  # Q1 (Jan-Mar)
  expect_equal(PNADCperiods:::quarter_month_n(1, 1), 1)  # 1st month = Jan
  expect_equal(PNADCperiods:::quarter_month_n(1, 2), 2)  # 2nd month = Feb
  expect_equal(PNADCperiods:::quarter_month_n(1, 3), 3)  # 3rd month = Mar

  # Q4 (Oct-Dec)
  expect_equal(PNADCperiods:::quarter_month_n(4, 1), 10)  # 1st month = Oct
  expect_equal(PNADCperiods:::quarter_month_n(4, 2), 11)  # 2nd month = Nov
  expect_equal(PNADCperiods:::quarter_month_n(4, 3), 12)  # 3rd month = Dec
})

# =============================================================================
# YYYYMM FORMAT EDGE CASES
# =============================================================================

test_that("yyyymm handles edge cases", {
  # Year boundaries
  expect_equal(yyyymm(1999, 12), 199912L)
  expect_equal(yyyymm(2000, 1), 200001L)

  # Recent years
  expect_equal(yyyymm(2023, 1), 202301L)
  expect_equal(yyyymm(2024, 12), 202412L)
})

# =============================================================================
# ISO WEEK FUNCTIONS
# =============================================================================

test_that("iso_week_year handles year boundary edge cases", {
  # Dec 31, 2020 is week 53 of ISO year 2020
  # (2020 is a leap year starting on Wednesday, so has 53 weeks)
  result_20201231 <- PNADCperiods:::iso_week_year(as.Date("2020-12-31"))
  expect_equal(result_20201231$year, 2020)  # ISO year 2020
  expect_equal(result_20201231$week, 53)     # Week 53

  # Jan 1, 2021 is week 53 of ISO year 2020
  # (belongs to previous ISO year)
  result_20210101 <- PNADCperiods:::iso_week_year(as.Date("2021-01-01"))
  expect_equal(result_20210101$year, 2020)  # ISO year 2020
  expect_equal(result_20210101$week, 53)     # Week 53

  # Jan 4 is always in week 1 of the current year (ISO 8601 rule)
  result_20210104 <- PNADCperiods:::iso_week_year(as.Date("2021-01-04"))
  expect_equal(result_20210104$year, 2021)  # ISO year 2021
  expect_equal(result_20210104$week, 1)      # Week 1
})

test_that("iso_week extracts week number correctly", {
  # Mid-year date
  expect_equal(PNADCperiods:::iso_week(as.Date("2024-06-15")), 24)

  # Jan 4 is always week 1
  expect_equal(PNADCperiods:::iso_week(as.Date("2024-01-04")), 1)

  # Year boundary: 2024-12-31 is Tuesday, Thursday of that week is 2025-01-02
  # So ISO week is 1 of ISO year 2025
  expect_equal(PNADCperiods:::iso_week(as.Date("2024-12-31")), 1L)
})

test_that("iso_year extracts ISO year correctly", {
  # Mid-year - ISO year equals calendar year
  expect_equal(PNADCperiods:::iso_year(as.Date("2024-06-15")), 2024)

  # Jan 1, 2024 is Monday, Thursday of that week is Jan 4 -> ISO year 2024
  expect_equal(PNADCperiods:::iso_year(as.Date("2024-01-01")), 2024L)

  # Dec 31, 2024 is Tuesday, Thursday of that week is Jan 2, 2025 -> ISO year 2025
  expect_equal(PNADCperiods:::iso_year(as.Date("2024-12-31")), 2025L)
})

# =============================================================================
# FIRST SATURDAY EDGE CASES
# =============================================================================

test_that("first_saturday_on_or_after handles month boundaries", {
  # Last day of month
  expect_equal(
    first_saturday_on_or_after(as.Date("2024-01-31")),  # Wednesday
    as.Date("2024-02-03")  # Next Saturday in February
  )

  # Last day of December
  expect_equal(
    first_saturday_on_or_after(as.Date("2024-12-31")),  # Tuesday
    as.Date("2025-01-04")  # Next Saturday in January
  )
})

test_that("first_saturday_on_or_after handles Friday edge case", {
  # Friday should return next day (Saturday)
  expect_equal(
    first_saturday_on_or_after(as.Date("2024-01-05")),  # Friday
    as.Date("2024-01-06")  # Saturday
  )
})

test_that("first_valid_saturday handles leap year February", {
  # February 2024 (leap year) starts on Thursday (dow=4)
  # First Saturday is Feb 3 (day 3)
  # 3 days from start of month, so min_days=4 would skip to day 10
  expect_equal(first_valid_saturday(2024, 2, min_days = 4), 10)

  # February 2023 (non-leap year) starts on Wednesday (dow=3)
  # First Saturday is Feb 4 (day 4)
  # 4 days from start, exactly meets min_days=4
  expect_equal(first_valid_saturday(2023, 2, min_days = 4), 4)
})

test_that("first_valid_saturday handles month starting on Saturday", {
  # April 2023 starts on Saturday
  # First Saturday is day 1, which has only 1 day in month
  # With min_days=4, should skip to second Saturday (day 8)
  expect_equal(first_valid_saturday(2023, 4, min_days = 4), 8)
})

# =============================================================================
# MONTH POSITION FUNCTIONS (INTERNAL)
# =============================================================================

test_that("calculate_month_position_min handles quarter boundaries", {
  # These functions are internal but critical for period identification
  # Test with dates at start of quarters

  # Q1 2024: Jan 1 is a Monday
  # First valid Saturday with min_days=4 determines month position
  result <- PNADCperiods:::calculate_month_position_min(
    as.Date("2024-01-06"),  # First Saturday
    2024, 1
  )
  expect_equal(result, 1)  # Should be month 1

  # Later date in same quarter
  result <- PNADCperiods:::calculate_month_position_min(
    as.Date("2024-03-15"),
    2024, 1
  )
  expect_equal(result, 3)  # Should be month 3
})

test_that("calculate_month_position_max handles quarter boundaries", {
  # Test with dates at end of quarters

  # Early date in quarter
  result <- PNADCperiods:::calculate_month_position_max(
    as.Date("2024-01-15"),
    2024, 1
  )
  expect_equal(result, 1)  # Should be month 1

  # Late date in quarter
  result <- PNADCperiods:::calculate_month_position_max(
    as.Date("2024-03-28"),
    2024, 1
  )
  expect_equal(result, 3)  # Should be month 3
})

# =============================================================================
# VECTORIZED INPUT TESTS
# =============================================================================

test_that("dow is vectorized", {
  dates <- as.Date(c("2024-01-06", "2024-01-07", "2024-01-08"))
  result <- dow(dates)
  expect_equal(result, c(6L, 0L, 1L))  # Sat, Sun, Mon
  expect_length(result, 3)
})

test_that("is_leap_year is vectorized", {
  years <- c(2000, 1900, 2020, 2023, 2024)
  result <- is_leap_year(years)
  expect_equal(result, c(TRUE, FALSE, TRUE, FALSE, TRUE))
  expect_length(result, 5)
})

test_that("iso_week_year is vectorized", {
  dates <- as.Date(c("2024-01-04", "2024-06-15", "2024-12-31"))
  result <- PNADCperiods:::iso_week_year(dates)

  expect_length(result$year, 3)
  expect_length(result$week, 3)
  expect_equal(result$year, c(2024L, 2024L, 2025L))
  expect_equal(result$week, c(1L, 24L, 1L))
})

# =============================================================================
# LOOKUP TABLE VALIDATION
# =============================================================================

test_that(".YEAR_BASE lookup table has correct values for known years", {
  year_base <- PNADCperiods:::.YEAR_BASE
  start <- PNADCperiods:::.YEAR_BASE_START

  # Spot-check known values
  # 2000-01-01 is day 10957 since 1970-01-01
  expect_equal(year_base[2000 - start + 1L], as.integer(as.Date("2000-01-01")))
  # 2024-01-01
  expect_equal(year_base[2024 - start + 1L], as.integer(as.Date("2024-01-01")))
  # 2012-01-01 (PNADC start)
  expect_equal(year_base[2012 - start + 1L], as.integer(as.Date("2012-01-01")))
})

test_that(".JAN4_BASE lookup table has correct values", {
  jan4_base <- PNADCperiods:::.JAN4_BASE
  start <- PNADCperiods:::.YEAR_BASE_START

  # Jan 4 of any year is always in ISO week 1
  expect_equal(jan4_base[2024 - start + 1L], as.integer(as.Date("2024-01-04")))
  expect_equal(jan4_base[2012 - start + 1L], as.integer(as.Date("2012-01-04")))
})

test_that("month offset tables have correct values", {
  # Regular year
  reg <- PNADCperiods:::.MONTH_OFFSET_REGULAR
  expect_equal(reg[1], 0L)   # Jan starts at 0
  expect_equal(reg[2], 31L)  # Feb starts after 31 days of Jan
  expect_equal(reg[3], 59L)  # Mar starts after 31+28=59 days (non-leap)
  expect_equal(reg[12], 334L) # Dec starts after 334 days

  # Leap year
  leap <- PNADCperiods:::.MONTH_OFFSET_LEAP
  expect_equal(leap[1], 0L)
  expect_equal(leap[2], 31L)
  expect_equal(leap[3], 60L)  # Mar starts after 31+29=60 days (leap)
  expect_equal(leap[12], 335L)
})

test_that("first_valid_saturday min_days=3 produces different result than min_days=4", {
  # June 2024 starts on Saturday (dow=6)
  # First Saturday is day 1, which has 1 day in month
  # min_days=4: 1 < 4, skip to day 8
  # min_days=3: 1 < 3, skip to day 8
  # Both skip for June 2024, but try a month where they differ
  #
  # September 2024 starts on Sunday (dow=0)
  # First Saturday is day 7 (7 days from start)
  # Both would accept day 7
  #
  # February 2024 starts on Thursday (dow=4)
  # First Saturday is day 3 (3 days from start)
  # min_days=4: 3 < 4, skip to day 10
  # min_days=3: 3 >= 3, use day 3
  expect_equal(first_valid_saturday(2024, 2, min_days = 4), 10)
  expect_equal(first_valid_saturday(2024, 2, min_days = 3), 3)
})

Try the PNADCperiods package in your browser

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

PNADCperiods documentation built on April 28, 2026, 9:07 a.m.