Date: 2022-10-05

# aweek means ‘any week’

#### 2022-10-05

# Introduction

The day in which a week starts differs depending on context. For countries like the UK, the first day of the week is the first working day, which is Monday. This definition conforms with the ISO 8601 standard definition for the beginning of a week, but there are examples of situations where the first day of the week is different:

- The US CDC defines an “MMWR” week which starts on a Sunday.
- In some regions, MSF will define a week that starts on Saturday

This package provides tools to convert dates to weeks and back where a week can start on any day. You can use this package for any of the following:

- convert date to week starting on any day
- convert week numbers and years to dates
- convert week to date
- convert week to week
- create a factor of weeks that contains ordered levels that includes missing weeks.

# Converting dates to weeks

You can convert dates to weeks starting on any day by using `date2week()` with the `week_start` argument. This argument can be a number from 1 to 7 representing the ISO 8601 day of the week OR it can be a string representing the day of the week in either an English locale or the locale defined on your computer. The default of this argument is the value of `get_week_start()`, which is a thin wrapper around `options("aweek.week_start", 1L)`. Unless you have specified a default `aweek.week_start` option with `set_week_start()`, this will always be set to 1 (Monday).

> It is highly recommended that you set the default `aweek.week_start` either in the beginning of your Rscript, Rmarkdown document, or in your .Rprofile.

    library("aweek")

    set_week_start("Sunday") # setting the default week_start to Sunday

    set.seed(2019-03-03)
    dat <- as.Date("2019-03-03") + sample(-6:7, 10, replace = TRUE)
    dat

    ##  [1] "2019-03-10" "2019-03-05" "2019-02-27" "2019-03-01" "2019-02-28"
    ##  [6] "2019-02-25" "2019-03-03" "2019-02-25" "2019-03-03" "2019-03-02"

    print(w <- date2week(dat))

    ## <aweek start: Sunday>
    ##  [1] "2019-W11-1" "2019-W10-3" "2019-W09-4" "2019-W09-6" "2019-W09-5"
    ##  [6] "2019-W09-2" "2019-W10-1" "2019-W09-2" "2019-W10-1" "2019-W09-7"

If you need a different day on the fly, you can supply an integer or character day to the `week_start` argument.

    # Use character days
    date2week(dat, week_start = "Monday")

    ## <aweek start: Monday>
    ##  [1] "2019-W10-7" "2019-W10-2" "2019-W09-3" "2019-W09-5" "2019-W09-4"
    ##  [6] "2019-W09-1" "2019-W09-7" "2019-W09-1" "2019-W09-7" "2019-W09-6"

    # Use ISO 8601 days
    date2week(dat, week_start = 1)

    ## <aweek start: Monday>
    ##  [1] "2019-W10-7" "2019-W10-2" "2019-W09-3" "2019-W09-5" "2019-W09-4"
    ##  [6] "2019-W09-1" "2019-W09-7" "2019-W09-1" "2019-W09-7" "2019-W09-6"

If you want to save two extra keystrokes, you can also use the `as.aweek()` method for dates, which wraps `date2week()`:

    as.aweek(dat, week_start = 1)

    ## <aweek start: Monday>
    ##  [1] "2019-W10-7" "2019-W10-2" "2019-W09-3" "2019-W09-5" "2019-W09-4"
    ##  [6] "2019-W09-1" "2019-W09-7" "2019-W09-1" "2019-W09-7" "2019-W09-6"

What you get back is an `aweek` class object. It can be converted back to a date with either `as.Date()` or `week2date()`:

    week2date(w)

    ##  [1] "2019-03-10" "2019-03-05" "2019-02-27" "2019-03-01" "2019-02-28"
    ##  [6] "2019-02-25" "2019-03-03" "2019-02-25" "2019-03-03" "2019-03-02"

    as.Date(w)

    ##  [1] "2019-03-10" "2019-03-05" "2019-02-27" "2019-03-01" "2019-02-28"
    ##  [6] "2019-02-25" "2019-03-03" "2019-02-25" "2019-03-03" "2019-03-02"

## How does it work?

The calculation of weeks from dates requires knowledge of the current day of the week and the number of days past 1 January.

Week numbers are calculated in three steps:

1. Find the day of the week, relative to the week_start (d). The day of the week (d) relative to the week start (s) is calculated using the ISO week day (i) via `d = 1L + ((i + (7L - s)) %% 7L)`.
2. Find the date that represents midweek (m). The date that represents midweek is found by subtracting the day of the week (d) from 4 and adding that number of days to the current date: `m = date + (4 - d)`.
3. Find the week number (w) by counting the number of days since 1 January to (m), and use integer division by 7: `w = 1L + ((m - yyyy-01-01) %/% 7)`

For example, here’s how to calculate the week for Tuesday, 6 December 2016, assuming the week start is a Sunday:

    the_date <- as.Date("2016-12-06")
    jan_1    <- as.Date("2016-01-01")

    i <- as.POSIXlt(the_date)$wday # 2, the ISO date for Tuesday
    s <- 7L                        # week_start for sunday

    # 1. Find the day of the week
    print(d <- 1L + ((i + (7L - s)) %% 7L))

    ## [1] 3

    # 2. Find the date that represents midweek
    print(m <- the_date + (4L - d))

    ## [1] "2016-12-07"

    # 3. Find the week
    print(w <- 1L + as.integer(m - jan_1) %/% 7L)

    ## [1] 49

    # Format the week
    sprintf("2016-W%02d-%d", w, d)

    ## [1] "2016-W49-3"

For the weeks around 1 January, the year is determined by the week number. If the month is January, but the week number is 52 or 53, then the year for the week (YYYY) is the calendar year (yyyy) minus 1. However, if the month is December, but the week number is 1, then the year for the week (YYYY) is the calendar year (yyyy) plus 1.

## The `aweek` class

The result you see above is an object of class “aweek”. The `aweek` class is a character that contains the `week_start` attribute. This attribute allows it to be easily converted back to a date without the user needing to enter the start day every time. You can convert a character that matches the `YYYY-Www-d` pattern to an `aweek` class object with `as.aweek()`:

    x <- as.aweek("2019-W10-1")
    x

    ## <aweek start: Sunday>
    ## [1] "2019-W10-1"

> Under the hood, it checks the validity of the week string and then add the attribute and class:

>     x <- "2019-W10-1"
>     attr(x, "week_start") <- 7 # Sunday
>     class(x) <- "aweek"

If you need to remove the class, you can just use `as.character()`:

    as.character(x)

    ## [1] "2019-W10-1"

## Best practices

The `date2week()` function only checks that dates are in ISO 8601 (yyyy-mm-dd) format before converting to weeks, and otherwise assumes that the dates are accurate so it’s strongly recommended to make sure your dates are in either `Date` or `POISXt` format and accurate before converting to weeks. The lubridate can be used for this purpose.

Use `set_week_start()` at the beginning of all your scripts to explicitly define the day on which your weeks start. This can be overridden if need be in specific parts of your scripts. Otherwise, the default will be dependent on the value of `getOption("aweek.week_start", 1L)`.

Because the `week_start` arguments default to `get_week_start()`, it’s recommended to specify `week_start` in `date2week()` and `week2date()` if you don’t have an `aweek` object.

Before you combine aweek objects, confirm that they are actually aweek objects with `inherits(myObject, "aweek")`.

## Weekly aggregation

There are times where you would want to aggregate your days into weeks, you can do this by specifying `floor_day = TRUE` in `date2week()`. For example, here we can show the individual weeks:

    print(wf <- date2week(dat, week_start = "Saturday", floor_day = TRUE))

    ## <aweek start: Saturday>
    ##  [1] "2019-W11" "2019-W10" "2019-W09" "2019-W09" "2019-W09" "2019-W09"
    ##  [7] "2019-W10" "2019-W09" "2019-W10" "2019-W10"

    table(wf)

    ## wf
    ## 2019-W09 2019-W10 2019-W11
    ##        5        4        1

If you convert this to date, then all the dates will represent the beginning of the week:

    print(dwf <- week2date(wf))

    ##  [1] "2019-03-09" "2019-03-02" "2019-02-23" "2019-02-23" "2019-02-23"
    ##  [6] "2019-02-23" "2019-03-02" "2019-02-23" "2019-03-02" "2019-03-02"

    weekdays(dwf)

    ##  [1] "Saturday" "Saturday" "Saturday" "Saturday" "Saturday" "Saturday"
    ##  [7] "Saturday" "Saturday" "Saturday" "Saturday"

If you want to aggregate your `aweek` objects after you created them, you can always use the `trunc()` function:

    w <- date2week(dat)
    w

    ## <aweek start: Sunday>
    ##  [1] "2019-W11-1" "2019-W10-3" "2019-W09-4" "2019-W09-6" "2019-W09-5"
    ##  [6] "2019-W09-2" "2019-W10-1" "2019-W09-2" "2019-W10-1" "2019-W09-7"

    trunc(w)

    ## <aweek start: Sunday>
    ##  [1] "2019-W11" "2019-W10" "2019-W09" "2019-W09" "2019-W09" "2019-W09"
    ##  [7] "2019-W10" "2019-W09" "2019-W10" "2019-W09"

## Factors

Weeks can be represented as factors, which is useful for tabulations across weeks. You can use `factor = TRUE` in `date2week()` and it will automatically fill in any missing weeks.

    dat[1] + c(0, 15)

    ## [1] "2019-03-10" "2019-03-25"

    date2week(dat[1] + c(0, 15), week_start = 1, factor = TRUE)

    ## <aweek start: Monday>
    ## [1] 2019-W10 2019-W13
    ## Levels: 2019-W10 2019-W11 2019-W12 2019-W13

If you already have an aweek object and want to convert it to a factor, you can use `factor_aweek()`:

    factor_aweek(w)

    ## <aweek start: Sunday>
    ##  [1] 2019-W11 2019-W10 2019-W09 2019-W09 2019-W09 2019-W09 2019-W10 2019-W09
    ##  [9] 2019-W10 2019-W09
    ## Levels: 2019-W09 2019-W10 2019-W11

Be careful when combining factors with other dates or aweek objects as they will force the other objects to be truncated as well.

## Weeks to weeks

You can use `change_week_start()` to convert between different week definitions if you have an `aweek` object:

    w # week starting on Sunday

    ## <aweek start: Sunday>
    ##  [1] "2019-W11-1" "2019-W10-3" "2019-W09-4" "2019-W09-6" "2019-W09-5"
    ##  [6] "2019-W09-2" "2019-W10-1" "2019-W09-2" "2019-W10-1" "2019-W09-7"

    ww <- change_week_start(w, week_start = "wednesday") # same dates, starting on Wednesday
    ww

    ## <aweek start: Wednesday>
    ##  [1] "2019-W10-5" "2019-W09-7" "2019-W09-1" "2019-W09-3" "2019-W09-2"
    ##  [6] "2019-W08-6" "2019-W09-5" "2019-W08-6" "2019-W09-5" "2019-W09-4"

    identical(as.Date(w), as.Date(ww))

    ## [1] TRUE

    # create a table with all days in the week
    d   <- as.Date("2019-03-03") + 0:6
    res <- lapply(weekdays(d), function(i) date2week(d, week_start = i))
    names(res) <- weekdays(d)
    data.frame(res)

    ##       Sunday     Monday    Tuesday  Wednesday   Thursday     Friday   Saturday
    ## 1 2019-W10-1 2019-W09-7 2019-W09-6 2019-W09-5 2019-W09-4 2019-W09-3 2019-W10-2
    ## 2 2019-W10-2 2019-W10-1 2019-W09-7 2019-W09-6 2019-W09-5 2019-W09-4 2019-W10-3
    ## 3 2019-W10-3 2019-W10-2 2019-W10-1 2019-W09-7 2019-W09-6 2019-W09-5 2019-W10-4
    ## 4 2019-W10-4 2019-W10-3 2019-W10-2 2019-W10-1 2019-W09-7 2019-W09-6 2019-W10-5
    ## 5 2019-W10-5 2019-W10-4 2019-W10-3 2019-W10-2 2019-W10-1 2019-W09-7 2019-W10-6
    ## 6 2019-W10-6 2019-W10-5 2019-W10-4 2019-W10-3 2019-W10-2 2019-W10-1 2019-W10-7
    ## 7 2019-W10-7 2019-W10-6 2019-W10-5 2019-W10-4 2019-W10-3 2019-W10-2 2019-W11-1

All of these columns contain the same dates:

    data.frame(lapply(res, as.Date))

    ##       Sunday     Monday    Tuesday  Wednesday   Thursday     Friday   Saturday
    ## 1 2019-03-03 2019-03-03 2019-03-03 2019-03-03 2019-03-03 2019-03-03 2019-03-03
    ## 2 2019-03-04 2019-03-04 2019-03-04 2019-03-04 2019-03-04 2019-03-04 2019-03-04
    ## 3 2019-03-05 2019-03-05 2019-03-05 2019-03-05 2019-03-05 2019-03-05 2019-03-05
    ## 4 2019-03-06 2019-03-06 2019-03-06 2019-03-06 2019-03-06 2019-03-06 2019-03-06
    ## 5 2019-03-07 2019-03-07 2019-03-07 2019-03-07 2019-03-07 2019-03-07 2019-03-07
    ## 6 2019-03-08 2019-03-08 2019-03-08 2019-03-08 2019-03-08 2019-03-08 2019-03-08
    ## 7 2019-03-09 2019-03-09 2019-03-09 2019-03-09 2019-03-09 2019-03-09 2019-03-09

## Combining `aweek` objects

You can add dates, aweek objects, or characters to aweek objects:

    c(as.aweek("2010-W10-1"),
      res$Sunday,
      "2010-W12-1",
      as.Date(res$Monday[1]) + 14)

    ## <aweek start: Sunday>
    ##  [1] "2010-W10-1" "2019-W10-1" "2019-W10-2" "2019-W10-3" "2019-W10-4"
    ##  [6] "2019-W10-5" "2019-W10-6" "2019-W10-7" "2010-W12-1" "2019-W12-1"

However, you can not combine aweek objects with different `week_start` attributes.

    c(res$Sunday[1], res$Wednesday[2], res$Friday[3])

    ## Error in c.aweek(res$Sunday[1], res$Wednesday[2], res$Friday[3]): All aweek objects must have the same week_start attribute. Please use change_week_start() to adjust the week_start attribute if you wish to combine these objects.

If you want to combine different aweek objects, you must first change their `week_start` attribute:

    wed <- change_week_start(res$Wednesday, get_week_start())
    fri <- change_week_start(res$Friday, get_week_start())
    c(res$Sunday[1], wed[2], fri[3])

    ## <aweek start: Sunday>
    ## [1] "2019-W10-1" "2019-W10-2" "2019-W10-3"

### Dates can be appended to aweek objects

Dates combined with aweek objects will will be automatically converted.

    c(res$Monday, as.Date("2019-04-03"))

    ## <aweek start: Monday>
    ## [1] "2019-W09-7" "2019-W10-1" "2019-W10-2" "2019-W10-3" "2019-W10-4"
    ## [6] "2019-W10-5" "2019-W10-6" "2019-W14-3"

### Add characters with caution

You can also add character representation of weeks, but be aware that it is assumed that these have the same `week_start` as the first object.

    s <- c(res$Saturday, "2019-W14-3")
    s

    ## <aweek start: Saturday>
    ## [1] "2019-W10-2" "2019-W10-3" "2019-W10-4" "2019-W10-5" "2019-W10-6"
    ## [6] "2019-W10-7" "2019-W11-1" "2019-W14-3"

    m <- c(res$Monday, "2019-W14-3")
    m

    ## <aweek start: Monday>
    ## [1] "2019-W09-7" "2019-W10-1" "2019-W10-2" "2019-W10-3" "2019-W10-4"
    ## [6] "2019-W10-5" "2019-W10-6" "2019-W14-3"

These will translate into different dates

    as.Date(s[7:8])

    ## [1] "2019-03-09" "2019-04-01"

    as.Date(m[7:8])

    ## [1] "2019-03-09" "2019-04-03"

# Working with weeks in data frames

You may encounter a situation where you have a merged data frame with weeks starting on different days. This section will cover two situations where you may have weeks as numbers and weeks as ISO-week strings. First we will create our demonstration data that represents the same week with different `week_start` attributes.

    # create a table with all days in the week
    d    <- as.Date("2019-03-03") + 0:6
    res  <- lapply(weekdays(d), function(i) date2week(d, week_start = i))
    resn <- lapply(weekdays(d), function(i) date2week(d, week_start = i, numeric = TRUE))
    datf <- data.frame(wday = rep(weekdays(d), each = 7),
                       week = unlist(res), # note: unlist converts to character
                       week_number = unlist(resn),
                       year  = 2019,
                       stringsAsFactors = FALSE)
    datf$day <- substring(datf$week, 10, 11)
    head(datf, 10)

    ##      wday       week week_number year day
    ## 1  Sunday 2019-W10-1          10 2019   1
    ## 2  Sunday 2019-W10-2          10 2019   2
    ## 3  Sunday 2019-W10-3          10 2019   3
    ## 4  Sunday 2019-W10-4          10 2019   4
    ## 5  Sunday 2019-W10-5          10 2019   5
    ## 6  Sunday 2019-W10-6          10 2019   6
    ## 7  Sunday 2019-W10-7          10 2019   7
    ## 8  Monday 2019-W09-7           9 2019   7
    ## 9  Monday 2019-W10-1          10 2019   1
    ## 10 Monday 2019-W10-2          10 2019   2

To get the weeks (numbers or strings) to aweek objects, you should use the `start` argument to specify which day of the week they start on. Internally, this translates the week to their corresponding dates and then to aweek objects with the same `week_start` attribute (which defaults to `get_week_start()`).

## weeks as numbers

Most commonly, you will have weeks across data sets represented by numbers. These can be converted to aweek objects using the `get_aweek()` function and to dates using the `get_date()` function:

    datf$aweek <- with(datf, get_aweek(week = week_number, year = year, day = day, start = wday))
    datf$date  <- with(datf, get_date(week = week_number, year = year, day = day, start = wday))
    head(datf, 10)

    ##      wday       week week_number year day      aweek       date
    ## 1  Sunday 2019-W10-1          10 2019   1 2019-W10-1 2019-03-03
    ## 2  Sunday 2019-W10-2          10 2019   2 2019-W10-2 2019-03-04
    ## 3  Sunday 2019-W10-3          10 2019   3 2019-W10-3 2019-03-05
    ## 4  Sunday 2019-W10-4          10 2019   4 2019-W10-4 2019-03-06
    ## 5  Sunday 2019-W10-5          10 2019   5 2019-W10-5 2019-03-07
    ## 6  Sunday 2019-W10-6          10 2019   6 2019-W10-6 2019-03-08
    ## 7  Sunday 2019-W10-7          10 2019   7 2019-W10-7 2019-03-09
    ## 8  Monday 2019-W09-7           9 2019   7 2019-W10-1 2019-03-03
    ## 9  Monday 2019-W10-1          10 2019   1 2019-W10-2 2019-03-04
    ## 10 Monday 2019-W10-2          10 2019   2 2019-W10-3 2019-03-05

These functions are also useful for constructing weeks or dates on the fly if you only have a week and a year:

    get_aweek(11, 2019)

    ## <aweek start: Sunday>
    ## [1] "2019-W11-1"

    get_date(11, 2019)

    ## [1] "2019-03-10"

## weeks as characters

If you have weeks formatted as ISO-week strings, then you can convert to aweek objects using `as.aweek()`:

    datf$aweek <- with(datf, as.aweek(week, start = wday))
    head(datf, 10)

    ##      wday       week week_number year day      aweek
    ## 1  Sunday 2019-W10-1          10 2019   1 2019-W10-1
    ## 2  Sunday 2019-W10-2          10 2019   2 2019-W10-2
    ## 3  Sunday 2019-W10-3          10 2019   3 2019-W10-3
    ## 4  Sunday 2019-W10-4          10 2019   4 2019-W10-4
    ## 5  Sunday 2019-W10-5          10 2019   5 2019-W10-5
    ## 6  Sunday 2019-W10-6          10 2019   6 2019-W10-6
    ## 7  Sunday 2019-W10-7          10 2019   7 2019-W10-7
    ## 8  Monday 2019-W09-7           9 2019   7 2019-W10-1
    ## 9  Monday 2019-W10-1          10 2019   1 2019-W10-2
    ## 10 Monday 2019-W10-2          10 2019   2 2019-W10-3

    str(datf)

    ## 'data.frame':    49 obs. of  6 variables:
    ##  $ wday       : chr  "Sunday" "Sunday" "Sunday" "Sunday" ...
    ##  $ week       : chr  "2019-W10-1" "2019-W10-2" "2019-W10-3" "2019-W10-4" ...
    ##  $ week_number: num  10 10 10 10 10 10 10 9 10 10 ...
    ##  $ year       : num  2019 2019 2019 2019 2019 ...
    ##  $ day        : chr  "1" "2" "3" "4" ...
    ##  $ aweek      : 'aweek' chr  "2019-W10-1" "2019-W10-2" "2019-W10-3" "2019-W10-4" ...
    ##   ..- attr(*, "week_start")= int 7

We can tabulate them to see how they transformed:

    print(with(datf, table(before = week, after = aweek)), zero.print = ".")

    ##             after
    ## before       2019-W10-1 2019-W10-2 2019-W10-3 2019-W10-4 2019-W10-5 2019-W10-6 2019-W10-7
    ##   2019-W09-3          1          .          .          .          .          .          .
    ##   2019-W09-4          1          1          .          .          .          .          .
    ##   2019-W09-5          1          1          1          .          .          .          .
    ##   2019-W09-6          1          1          1          1          .          .          .
    ##   2019-W09-7          1          1          1          1          1          .          .
    ##   2019-W10-1          1          1          1          1          1          1          .
    ##   2019-W10-2          1          1          1          1          1          1          1
    ##   2019-W10-3          .          1          1          1          1          1          1
    ##   2019-W10-4          .          .          1          1          1          1          1
    ##   2019-W10-5          .          .          .          1          1          1          1
    ##   2019-W10-6          .          .          .          .          1          1          1
    ##   2019-W10-7          .          .          .          .          .          1          1
    ##   2019-W11-1          .          .          .          .          .          .          1

# Converting weeks to dates

If you receive data that contains week definitions, you can convert it back to a date if you know where the week starts.

    week2date("2019-W10-1", week_start = "Sunday") # 2019-03-03

    ## [1] "2019-03-03"

    week2date("2019-W10-1", week_start = "Monday") # 2019-03-04

    ## [1] "2019-03-04"

If you have an `aweek` object, however, it will use the `week_start` attribute defined in the object, even if the default `week_start` attribute is different:

    set_week_start("Monday") # Set the default week_start to ISO week
    get_week_start(w)        # show the default week_start for w

    ## [1] 7

    week2date(w)

    ##  [1] "2019-03-10" "2019-03-05" "2019-02-27" "2019-03-01" "2019-02-28"
    ##  [6] "2019-02-25" "2019-03-03" "2019-02-25" "2019-03-03" "2019-03-02"

    identical(week2date(w), dat)               # TRUE

    ## [1] TRUE

    identical(week2date(as.character(w)), dat) # FALSE

    ## [1] FALSE

You can also use `as.Date()` and `as.POISXlt()` if you have an `aweek` object:

    as.Date(w)

    ##  [1] "2019-03-10" "2019-03-05" "2019-02-27" "2019-03-01" "2019-02-28"
    ##  [6] "2019-02-25" "2019-03-03" "2019-02-25" "2019-03-03" "2019-03-02"

    as.POSIXlt(w)

    ##  [1] "2019-03-10 UTC" "2019-03-05 UTC" "2019-02-27 UTC" "2019-03-01 UTC"
    ##  [5] "2019-02-28 UTC" "2019-02-25 UTC" "2019-03-03 UTC" "2019-02-25 UTC"
    ##  [9] "2019-03-03 UTC" "2019-03-02 UTC"