R/zoned-time.R

Defines functions clock_init_zoned_time_utils check_zoned_time_precision check_zone add_nanoseconds.clock_zoned_time add_microseconds.clock_zoned_time add_milliseconds.clock_zoned_time add_seconds.clock_zoned_time add_minutes.clock_zoned_time add_hours.clock_zoned_time add_days.clock_zoned_time add_weeks.clock_zoned_time add_months.clock_zoned_time add_quarters.clock_zoned_time add_years.clock_zoned_time zoned_time_precision zoned_time_set_zone zoned_time_zone zoned_time_info zoned_time_now as.character.clock_zoned_time as_naive_time.clock_zoned_time as_sys_time.clock_zoned_time as_zoned_time.clock_zoned_time as_zoned_time.default as_zoned_time pillar_shaft.clock_zoned_time obj_print_footer.clock_zoned_time obj_print_data.clock_zoned_time print.clock_zoned_time vec_cast.clock_zoned_time.clock_zoned_time vec_ptype2.clock_zoned_time.clock_zoned_time vec_ptype.clock_zoned_time zone_pretty vec_ptype_abbr.clock_zoned_time vec_ptype_full.clock_zoned_time vec_restore.clock_zoned_time vec_proxy.clock_zoned_time zoned_time_parse_abbrev zoned_time_parse_complete zoned_time_format zoned_time_print_zone_name format.clock_zoned_time zoned_time_precision_attribute zoned_time_set_zone_attribute zoned_time_zone_attribute check_zoned_time is_zoned_time

Documented in as_zoned_time format.clock_zoned_time is_zoned_time zoned_time_info zoned_time_now zoned_time_parse_abbrev zoned_time_parse_complete zoned_time_precision zoned_time_set_zone zoned_time_zone

#' Is `x` a zoned-time?
#'
#' This function determines if the input is a zoned-time object.
#'
#' @param x `[object]`
#'
#'   An object.
#'
#' @return `TRUE` if `x` inherits from `"clock_zoned_time"`, otherwise `FALSE`.
#'
#' @export
#' @examples
#' is_zoned_time(1)
#' is_zoned_time(zoned_time_now("America/New_York"))
is_zoned_time <- function(x) {
  inherits(x, "clock_zoned_time")
}

check_zoned_time <- function(x, ..., arg = caller_arg(x), call = caller_env()) {
  check_inherits(x, what = "clock_zoned_time", arg = arg, call = call)
}

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

zoned_time_zone_attribute <- function(x) {
  attr(x, "zone", exact = TRUE)
}
zoned_time_set_zone_attribute <- function(x, zone) {
  attr(x, "zone") <- zone
  x
}

zoned_time_precision_attribute <- function(x) {
  attr(x, "precision", exact = TRUE)
}

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

#' Formatting: zoned-time
#'
#' @description
#' This is a zoned-time method for the [format()] generic.
#'
#' This function allows you to format a zoned-time using a flexible `format`
#' string.
#'
#' If `format` is `NULL`, a default format of `"%Y-%m-%dT%H:%M:%S%Ez[%Z]"` is
#' used. This matches the default format that [zoned_time_parse_complete()]
#' parses. Additionally, this format matches the de-facto standard extension to
#' RFC 3339 for creating completely unambiguous date-times.
#'
#' @param x `[clock_zoned_time]`
#'
#'   A zoned-time.
#'
#' @param ... `[dots]`
#'
#'   Not used, but no error will be thrown if not empty to remain compatible
#'   with usage of the `format()` generic.
#'
#' @param format `[character(1) / NULL]`
#'
#'   If `NULL`, a default format is used, which depends on the type of the
#'   input.
#'
#'   Otherwise, a format string which is a combination of:
#'
#'   **Year**
#'
#'   - `%C`: The year divided by 100 using floored division. If the result
#'   is a single decimal digit, it is prefixed with `0`.
#'
#'   - `%y`: The last two decimal digits of the year. If the result is a single
#'   digit it is prefixed by `0`.
#'
#'   - `%Y`: The year as a decimal number. If the result is less than four
#'   digits it is left-padded with `0` to four digits.
#'
#'   **Month**
#'
#'   - `%b`, `%h`: The `locale`'s abbreviated month name.
#'
#'   - `%B`: The `locale`'s full month name.
#'
#'   - `%m`: The month as a decimal number. January is `01`. If the result is a
#'   single digit, it is prefixed with `0`.
#'
#'   **Day**
#'
#'   - `%d`: The day of month as a decimal number. If the result is a single
#'   decimal digit, it is prefixed with `0`.
#'
#'   **Day of the week**
#'
#'   - `%a`: The `locale`'s abbreviated weekday name.
#'
#'   - `%A`: The `locale`'s full weekday name.
#'
#'   - `%w`: The weekday as a decimal number (`0-6`), where Sunday is `0`.
#'
#'   **ISO 8601 week-based year**
#'
#'   - `%g`: The last two decimal digits of the ISO week-based year. If the
#'   result is a single digit it is prefixed by `0`.
#'
#'   - `%G`: The ISO week-based year as a decimal number. If the result is less
#'   than four digits it is left-padded with `0` to four digits.
#'
#'   - `%V`: The ISO week-based week number as a decimal number. If the result
#'   is a single digit, it is prefixed with `0`.
#'
#'   - `%u`: The ISO weekday as a decimal number (`1-7`), where Monday is `1`.
#'
#'   **Week of the year**
#'
#'   - `%U`: The week number of the year as a decimal number. The first Sunday
#'   of the year is the first day of week `01`. Days of the same year prior to
#'   that are in week `00`. If the result is a single digit, it is prefixed with
#'   `0`.
#'
#'   - `%W`: The week number of the year as a decimal number. The first Monday
#'   of the year is the first day of week `01`. Days of the same year prior to
#'   that are in week `00`. If the result is a single digit, it is prefixed with
#'   `0`.
#'
#'   **Day of the year**
#'
#'   - `%j`: The day of the year as a decimal number. January 1 is `001`. If the
#'   result is less than three digits, it is left-padded with `0` to three
#'   digits.
#'
#'   **Date**
#'
#'   - `%D`, `%x`: Equivalent to `%m/%d/%y`.
#'
#'   - `%F`: Equivalent to `%Y-%m-%d`.
#'
#'   **Time of day**
#'
#'   - `%H`: The hour (24-hour clock) as a decimal number. If the result is a
#'   single digit, it is prefixed with `0`.
#'
#'   - `%I`: The hour (12-hour clock) as a decimal number. If the result is a
#'   single digit, it is prefixed with `0`.
#'
#'   - `%M`: The minute as a decimal number. If the result is a single digit, it
#'   is prefixed with `0`.
#'
#'   - `%S`: Seconds as a decimal number. Fractional seconds are printed at the
#'   precision of the input. The character for the decimal point is localized
#'   according to `locale`.
#'
#'   - `%p`: The `locale`'s equivalent of the AM/PM designations associated with
#'   a 12-hour clock.
#'
#'   - `%R`: Equivalent to `%H:%M`.
#'
#'   - `%T`, `%X`: Equivalent to `%H:%M:%S`.
#'
#'   - `%r`: Nearly equivalent to `%I:%M:%S %p`, but seconds are always printed
#'   at second precision.
#'
#'   **Time zone**
#'
#'   - `%z`: The offset from UTC in the ISO 8601 format. For example `-0430`
#'   refers to 4 hours 30 minutes behind UTC. If the offset is zero, `+0000` is
#'   used. The modified command `%Ez` inserts a `:` between the hour and
#'   minutes, like `-04:30`.
#'
#'   - `%Z`: The full time zone name. If `abbreviate_zone` is `TRUE`, the time
#'   zone abbreviation.
#'
#'   **Miscellaneous**
#'
#'   - `%c`: A date and time representation. Similar to, but not exactly the
#'   same as, `%a %b %d %H:%M:%S %Y`.
#'
#'   - `%%`: A `%` character.
#'
#'   - `%n`: A newline character.
#'
#'   - `%t`: A horizontal-tab character.
#'
#' @param locale `[clock_locale]`
#'
#'   A locale object created from [clock_locale()].
#'
#' @param abbreviate_zone `[logical(1)]`
#'
#'   If `TRUE`, `%Z` returns an abbreviated time zone name.
#'
#'   If `FALSE`, `%Z` returns the full time zone name.
#'
#' @return A character vector of the formatted input.
#'
#' @export
#' @examples
#' x <- year_month_day(2019, 1, 1)
#' x <- as_zoned_time(as_naive_time(x), "America/New_York")
#'
#' format(x)
#' format(x, format = "%B %d, %Y")
#' format(x, format = "%B %d, %Y", locale = clock_locale("fr"))
format.clock_zoned_time <- function(x,
                                    ...,
                                    format = NULL,
                                    locale = clock_locale(),
                                    abbreviate_zone = FALSE) {
  check_clock_locale(locale)

  zone <- zoned_time_zone_attribute(x)
  precision <- zoned_time_precision_attribute(x)

  if (is_null(format)) {
    # Collect internal option
    print_zone_name <- zoned_time_print_zone_name(...)
    format <- zoned_time_format(print_zone_name)
  }

  labels <- locale$labels
  decimal_mark <- locale$decimal_mark

  out <- format_zoned_time_cpp(
    fields = x,
    zone = zone,
    abbreviate_zone = abbreviate_zone,
    format = format,
    precision_int = precision,
    month = labels$month,
    month_abbrev = labels$month_abbrev,
    weekday = labels$weekday,
    weekday_abbrev = labels$weekday_abbrev,
    am_pm = labels$am_pm,
    decimal_mark = decimal_mark
  )

  names(out) <- clock_rcrd_names(x)

  out
}

zoned_time_print_zone_name <- function(..., print_zone_name = TRUE) {
  print_zone_name
}
zoned_time_format <- function(print_zone_name) {
  if (print_zone_name) {
    "%Y-%m-%dT%H:%M:%S%Ez[%Z]"
  } else {
    "%Y-%m-%dT%H:%M:%S%Ez"
  }
}

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

#' Parsing: zoned-time
#'
#' @description
#' There are two parsers into a zoned-time, `zoned_time_parse_complete()` and
#' `zoned_time_parse_abbrev()`.
#'
#' ## zoned_time_parse_complete()
#'
#' `zoned_time_parse_complete()` is a parser for _complete_ date-time strings,
#' like `"2019-01-01T00:00:00-05:00[America/New_York]"`. A complete date-time
#' string has both the time zone offset and full time zone name in the string,
#' which is the only way for the string itself to contain all of the information
#' required to construct a zoned-time. Because of this,
#' `zoned_time_parse_complete()` requires both the `%z` and `%Z` commands to be
#' supplied in the `format` string.
#'
#' The default options assume that `x` should be parsed at second precision,
#' using a `format` string of `"%Y-%m-%dT%H:%M:%S%Ez[%Z]"`. This matches the
#' default result from calling `format()` on a zoned-time. Additionally, this
#' format matches the de-facto standard extension to RFC 3339 for creating
#' completely unambiguous date-times.
#'
#' ## zoned_time_parse_abbrev()
#'
#' `zoned_time_parse_abbrev()` is a parser for date-time strings containing only
#' a time zone abbreviation, like `"2019-01-01 00:00:00 EST"`. The time zone
#' abbreviation is not enough to identify the full time zone name that the
#' date-time belongs to, so the full time zone name must be supplied as the
#' `zone` argument. However, the time zone abbreviation can help with resolving
#' ambiguity around daylight saving time fallbacks.
#'
#' For `zoned_time_parse_abbrev()`, `%Z` must be supplied and is interpreted as
#' the time zone abbreviation rather than the full time zone name.
#'
#' If used, the `%z` command must parse correctly, but its value will be
#' completely ignored.
#'
#' The default options assume that `x` should be parsed at second precision,
#' using a `format` string of `"%Y-%m-%d %H:%M:%S %Z"`. This matches the default
#' result from calling `print()` or `format(usetz = TRUE)` on a POSIXct
#' date-time.
#'
#' @details
#' If `zoned_time_parse_complete()` is given input that is length zero, all
#' `NA`s, or completely fails to parse, then no time zone will be able to be
#' determined. In that case, the result will use `"UTC"`.
#'
#' If your date-time strings contain time zone offsets (like `-04:00`), but
#' not the full time zone name, you might need [sys_time_parse()].
#'
#' If your date-time strings don't contain time zone offsets or the full time
#' zone name, you might need to use [naive_time_parse()]. From there, if you
#' know the time zone that the date-times are supposed to be in, you can convert
#' to a zoned-time with [as_zoned_time()].
#'
#' @section Full Precision Parsing:
#'
#' It is highly recommended to parse all of the information in the date-time
#' string into a type at least as precise as the string. For example, if your
#' string has fractional seconds, but you only require seconds, specify a
#' sub-second `precision`, then round to seconds manually using whatever
#' convention is appropriate for your use case. Parsing such a string directly
#' into a second precision result is ambiguous and undefined, and is unlikely to
#' work as you might expect.
#'
#' @inheritParams rlang::args_dots_empty
#'
#' @param x `[character]`
#'
#'   A character vector to parse.
#'
#' @param zone `[character(1)]`
#'
#'   A full time zone name.
#'
#' @param format `[character / NULL]`
#'
#'   A format string. A combination of the following commands, or `NULL`,
#'   in which case a default format string is used.
#'
#'   A vector of multiple format strings can be supplied. They will be tried in
#'   the order they are provided.
#'
#'   **Year**
#'
#'   - `%C`: The century as a decimal number. The modified command `%NC` where
#'   `N` is a positive decimal integer specifies the maximum number of
#'   characters to read. If not specified, the default is `2`. Leading zeroes
#'   are permitted but not required.
#'
#'   - `%y`: The last two decimal digits of the year. If the century is not
#'   otherwise specified (e.g. with `%C`), values in the range `[69 - 99]` are
#'   presumed to refer to the years `[1969 - 1999]`, and values in the range
#'   `[00 - 68]` are presumed to refer to the years `[2000 - 2068]`. The
#'   modified command `%Ny`, where `N` is a positive decimal integer, specifies
#'   the maximum number of characters to read. If not specified, the default is
#'   `2`. Leading zeroes are permitted but not required.
#'
#'   - `%Y`: The year as a decimal number. The modified command `%NY` where `N`
#'   is a positive decimal integer specifies the maximum number of characters to
#'   read. If not specified, the default is `4`. Leading zeroes are permitted
#'   but not required.
#'
#'   **Month**
#'
#'   - `%b`, `%B`, `%h`: The `locale`'s full or abbreviated case-insensitive
#'   month name.
#'
#'   - `%m`: The month as a decimal number. January is `1`. The modified command
#'   `%Nm` where `N` is a positive decimal integer specifies the maximum number
#'   of characters to read. If not specified, the default is `2`. Leading zeroes
#'   are permitted but not required.
#'
#'   **Day**
#'
#'   - `%d`, `%e`: The day of the month as a decimal number. The modified
#'   command `%Nd` where `N` is a positive decimal integer specifies the maximum
#'   number of characters to read. If not specified, the default is `2`. Leading
#'   zeroes are permitted but not required.
#'
#'   **Day of the week**
#'
#'   - `%a`, `%A`: The `locale`'s full or abbreviated case-insensitive weekday
#'   name.
#'
#'   - `%w`: The weekday as a decimal number (`0-6`), where Sunday is `0`. The
#'   modified command `%Nw` where `N` is a positive decimal integer specifies
#'   the maximum number of characters to read. If not specified, the default is
#'   `1`. Leading zeroes are permitted but not required.
#'
#'   **ISO 8601 week-based year**
#'
#'   - `%g`: The last two decimal digits of the ISO week-based year. The
#'   modified command `%Ng` where `N` is a positive decimal integer specifies
#'   the maximum number of characters to read. If not specified, the default is
#'   `2`. Leading zeroes are permitted but not required.
#'
#'   - `%G`: The ISO week-based year as a decimal number. The modified command
#'   `%NG` where `N` is a positive decimal integer specifies the maximum number
#'   of characters to read. If not specified, the default is `4`. Leading zeroes
#'   are permitted but not required.
#'
#'   - `%V`: The ISO week-based week number as a decimal number. The modified
#'   command `%NV` where `N` is a positive decimal integer specifies the maximum
#'   number of characters to read. If not specified, the default is `2`. Leading
#'   zeroes are permitted but not required.
#'
#'   - `%u`: The ISO weekday as a decimal number (`1-7`), where Monday is `1`.
#'   The modified command `%Nu` where `N` is a positive decimal integer
#'   specifies the maximum number of characters to read. If not specified, the
#'   default is `1`. Leading zeroes are permitted but not required.
#'
#'   **Week of the year**
#'
#'   - `%U`: The week number of the year as a decimal number. The first Sunday
#'   of the year is the first day of week `01`. Days of the same year prior to
#'   that are in week `00`. The modified command `%NU` where `N` is a positive
#'   decimal integer specifies the maximum number of characters to read. If not
#'   specified, the default is `2`. Leading zeroes are permitted but not
#'   required.
#'
#'   - `%W`: The week number of the year as a decimal number. The first Monday
#'   of the year is the first day of week `01`. Days of the same year prior to
#'   that are in week `00`. The modified command `%NW` where `N` is a positive
#'   decimal integer specifies the maximum number of characters to read. If not
#'   specified, the default is `2`. Leading zeroes are permitted but not
#'   required.
#'
#'   **Day of the year**
#'
#'   - `%j`: The day of the year as a decimal number. January 1 is `1`. The
#'   modified command `%Nj` where `N` is a positive decimal integer specifies
#'   the maximum number of characters to read. If not specified, the default is
#'   `3`. Leading zeroes are permitted but not required.
#'
#'   **Date**
#'
#'   - `%D`, `%x`: Equivalent to `%m/%d/%y`.
#'
#'   - `%F`: Equivalent to `%Y-%m-%d`. If modified with a width (like `%NF`),
#'   the width is applied to only `%Y`.
#'
#'   **Time of day**
#'
#'   - `%H`: The hour (24-hour clock) as a decimal number. The modified command
#'   `%NH` where `N` is a positive decimal integer specifies the maximum number
#'   of characters to read. If not specified, the default is `2`. Leading zeroes
#'   are permitted but not required.
#'
#'   - `%I`: The hour (12-hour clock) as a decimal number. The modified command
#'   `%NI` where `N` is a positive decimal integer specifies the maximum number
#'   of characters to read. If not specified, the default is `2`. Leading zeroes
#'   are permitted but not required.
#'
#'   - `%M`: The minutes as a decimal number. The modified command `%NM` where
#'   `N` is a positive decimal integer specifies the maximum number of
#'   characters to read. If not specified, the default is `2`. Leading zeroes
#'   are permitted but not required.
#'
#'   - `%S`: The seconds as a decimal number. Leading zeroes are permitted but
#'   not required. If encountered, the `locale` determines the decimal point
#'   character. Generally, the maximum number of characters to read is
#'   determined by the precision that you are parsing at. For example, a
#'   precision of `"second"` would read a maximum of 2 characters, while a
#'   precision of `"millisecond"` would read a maximum of 6 (2 for the values
#'   before the decimal point, 1 for the decimal point, and 3 for the values
#'   after it). The modified command `%NS`, where `N` is a positive decimal
#'   integer, can be used to exactly specify the maximum number of characters to
#'   read. This is only useful if you happen to have seconds with more than 1
#'   leading zero.
#'
#'   - `%p`: The `locale`'s equivalent of the AM/PM designations associated with
#'   a 12-hour clock. The command `%I` must precede `%p` in the format string.
#'
#'   - `%R`: Equivalent to `%H:%M`.
#'
#'   - `%T`, `%X`: Equivalent to `%H:%M:%S`.
#'
#'   - `%r`: Equivalent to `%I:%M:%S %p`.
#'
#'   **Time zone**
#'
#'   - `%z`: The offset from UTC in the format `[+|-]hh[mm]`. For example
#'   `-0430` refers to 4 hours 30 minutes behind UTC. And `04` refers to 4 hours
#'   ahead of UTC. The modified command `%Ez` parses a `:` between the hours and
#'   minutes and leading zeroes on the hour field are optional:
#'   `[+|-]h[h][:mm]`. For example `-04:30` refers to 4 hours 30 minutes behind
#'   UTC. And `4` refers to 4 hours ahead of UTC.
#'
#'   - `%Z`: The full time zone name or the time zone abbreviation, depending on
#'   the function being used. A single word is parsed. This word can only
#'   contain characters that are alphanumeric, or one of `'_'`, `'/'`, `'-'` or
#'   `'+'`.
#'
#'   **Miscellaneous**
#'
#'   - `%c`: A date and time representation. Equivalent to
#'   `%a %b %d %H:%M:%S %Y`.
#'
#'   - `%%`: A `%` character.
#'
#'   - `%n`: Matches one white space character. `%n`, `%t`, and a space can be
#'   combined to match a wide range of white-space patterns. For example `"%n "`
#'   matches one or more white space characters, and `"%n%t%t"` matches one to
#'   three white space characters.
#'
#'   - `%t`: Matches zero or one white space characters.
#'
#' @param precision `[character(1)]`
#'
#'   A precision for the resulting zoned-time. One of:
#'
#'   - `"second"`
#'
#'   - `"millisecond"`
#'
#'   - `"microsecond"`
#'
#'   - `"nanosecond"`
#'
#'   Setting the `precision` determines how much information `%S` attempts
#'   to parse.
#'
#' @param locale `[clock_locale]`
#'
#'   A locale object created from [clock_locale()].
#'
#' @return A zoned-time.
#'
#' @name zoned-parsing
#'
#' @examples
#' library(magrittr)
#'
#' zoned_time_parse_complete("2019-01-01T01:02:03-05:00[America/New_York]")
#'
#' zoned_time_parse_complete(
#'   "January 21, 2019 -0500 America/New_York",
#'   format = "%B %d, %Y %z %Z"
#' )
#'
#' # Nanosecond precision
#' x <- "2019/12/31 01:05:05.123456700-05:00[America/New_York]"
#' zoned_time_parse_complete(
#'   x,
#'   format = "%Y/%m/%d %H:%M:%S%Ez[%Z]",
#'   precision = "nanosecond"
#' )
#'
#' # The `%z` offset must correspond to the true offset that would be used
#' # if the input was parsed as a naive-time and then converted to a zoned-time
#' # with `as_zoned_time()`. For example, the time that was parsed above used an
#' # offset of `-05:00`. We can confirm that this is correct with:
#' year_month_day(2019, 1, 1, 1, 2, 3) %>%
#'   as_naive_time() %>%
#'   as_zoned_time("America/New_York")
#'
#' # So the following would not parse correctly
#' zoned_time_parse_complete("2019-01-01T01:02:03-04:00[America/New_York]")
#'
#' # `%z` is useful for breaking ties in otherwise ambiguous times. For example,
#' # these two times are on either side of a daylight saving time fallback.
#' # Without the `%z` offset, you wouldn't be able to tell them apart!
#' x <- c(
#'   "1970-10-25T01:30:00-04:00[America/New_York]",
#'   "1970-10-25T01:30:00-05:00[America/New_York]"
#' )
#'
#' zoned_time_parse_complete(x)
#'
#' # If you have date-time strings with time zone abbreviations,
#' # `zoned_time_parse_abbrev()` should be able to help. The `zone` must be
#' # provided, because multiple countries may use the same time zone
#' # abbreviation. For example:
#' x <- "1970-01-01 02:30:30 IST"
#'
#' # IST = India Standard Time
#' zoned_time_parse_abbrev(x, "Asia/Kolkata")
#'
#' # IST = Israel Standard Time
#' zoned_time_parse_abbrev(x, "Asia/Jerusalem")
#'
#' # The time zone abbreviation is mainly useful for resolving ambiguity
#' # around daylight saving time fallbacks. Without the abbreviation, these
#' # date-times would be ambiguous.
#' x <- c(
#'   "1970-10-25 01:30:00 EDT",
#'   "1970-10-25 01:30:00 EST"
#' )
#' zoned_time_parse_abbrev(x, "America/New_York")
NULL

#' @rdname zoned-parsing
#' @export
zoned_time_parse_complete <- function(x,
                                      ...,
                                      format = NULL,
                                      precision = "second",
                                      locale = clock_locale()) {
  check_dots_empty0(...)

  check_character(x)

  check_zoned_time_precision(precision)
  precision <- precision_to_integer(precision)

  check_clock_locale(locale)

  if (is_null(format)) {
    # Use both %z and %Z
    format <- zoned_time_format(print_zone_name = TRUE)
  }

  labels <- locale$labels
  mark <- locale$decimal_mark

  result <- zoned_time_parse_complete_cpp(
    x,
    format,
    precision,
    labels$month,
    labels$month_abbrev,
    labels$weekday,
    labels$weekday_abbrev,
    labels$am_pm,
    mark
  )

  new_zoned_time_from_fields(result$fields, precision, result$zone, names(x))
}

#' @rdname zoned-parsing
#' @export
zoned_time_parse_abbrev <- function(x,
                                    zone,
                                    ...,
                                    format = NULL,
                                    precision = "second",
                                    locale = clock_locale()) {
  check_dots_empty0(...)

  check_character(x)

  check_zoned_time_precision(precision)
  precision <- precision_to_integer(precision)

  check_clock_locale(locale)

  if (is_null(format)) {
    # Like what R POSIXct prints
    format <- "%Y-%m-%d %H:%M:%S %Z"
  }

  labels <- locale$labels
  mark <- locale$decimal_mark

  fields <- zoned_time_parse_abbrev_cpp(
    x,
    zone,
    format,
    precision,
    labels$month,
    labels$month_abbrev,
    labels$weekday,
    labels$weekday_abbrev,
    labels$am_pm,
    mark
  )

  new_zoned_time_from_fields(fields, precision, zone, names(x))
}

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

# ptype2 / cast will prevent zoned times with different zones from being
# compared, so the equality proxy doesn't need to worry about the `zone`.

#' @export
vec_proxy.clock_zoned_time <- function(x, ...) {
  .Call(`_clock_clock_rcrd_proxy`, x)
}

#' @export
vec_restore.clock_zoned_time <- function(x, to, ...) {
  .Call(`_clock_zoned_time_restore`, x, to)
}

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

#' @export
vec_ptype_full.clock_zoned_time <- function(x, ...) {
  zone <- zone_pretty(zoned_time_zone_attribute(x))
  precision <- zoned_time_precision_attribute(x)
  precision <- precision_to_string(precision)
  paste0("zoned_time<", precision, "><", zone, ">")
}

#' @export
vec_ptype_abbr.clock_zoned_time <- function(x, ...) {
  zone <- zone_pretty(zoned_time_zone_attribute(x))
  precision <- zoned_time_precision_attribute(x)
  precision <- precision_to_string(precision)
  precision <- precision_abbr(precision)
  paste0("zt<", precision, "><", zone, ">")
}

zone_pretty <- function(zone) {
  if (identical(zone, "")) {
    zone <- zone_current()
    zone <- paste0(zone, " (current)")
  }
  zone
}

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

#' @export
vec_ptype.clock_zoned_time <- function(x, ...) {
  zone <- zoned_time_zone_attribute(x)

  ptype_utc <- switch(
    zoned_time_precision_attribute(x) + 1L,
    abort("Internal error: Invalid precision"),
    abort("Internal error: Invalid precision"),
    abort("Internal error: Invalid precision"),
    abort("Internal error: Invalid precision"),
    abort("Internal error: Invalid precision"),
    abort("Internal error: Invalid precision"),
    abort("Internal error: Invalid precision"),
    clock_empty_zoned_time_utc_second,
    clock_empty_zoned_time_utc_millisecond,
    clock_empty_zoned_time_utc_microsecond,
    clock_empty_zoned_time_utc_nanosecond,
    abort("Internal error: Invalid precision.")
  )

  ptype <- zoned_time_set_zone_attribute(ptype_utc, zone)

  ptype
}

#' @export
vec_ptype2.clock_zoned_time.clock_zoned_time <- function(x, y, ...) {
  x_zone <- zoned_time_zone_attribute(x)
  y_zone <- zoned_time_zone_attribute(y)

  if (x_zone != y_zone) {
    stop_incompatible_type(x, y, ..., details = "Zones can't differ.")
  }

  x_precision <- zoned_time_precision_attribute(x)
  y_precision <- zoned_time_precision_attribute(y)

  if (x_precision >= y_precision) {
    x
  } else {
    y
  }
}

#' @export
vec_cast.clock_zoned_time.clock_zoned_time <- function(x, to, ...) {
  x_zone <- zoned_time_zone_attribute(x)
  to_zone <- zoned_time_zone_attribute(to)

  if (x_zone != to_zone) {
    stop_incompatible_cast(x, to, ..., details = "Zones can't differ.")
  }

  x_precision <- zoned_time_precision_attribute(x)
  to_precision <- zoned_time_precision_attribute(to)

  if (x_precision == to_precision) {
    return(x)
  }

  if (x_precision > to_precision) {
    stop_incompatible_cast(x, to, ..., details = "Precision would be lost.")
  }

  fields <- duration_cast_cpp(x, x_precision, to_precision)

  names <- clock_rcrd_names(x)

  new_zoned_time_from_fields(fields, to_precision, x_zone, names)
}

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

#' @export
print.clock_zoned_time <- function(x, ..., max = NULL) {
  clock_print(x, max)
}

# - Pass through internal option to not print zone name
# - Unlike vctrs, don't use `print(quote = FALSE)` since we want to match base R
#' @export
obj_print_data.clock_zoned_time <- function(x, ..., max) {
  if (vec_is_empty(x)) {
    return(invisible(x))
  }

  x <- max_slice(x, max)

  out <- format(x, print_zone_name = FALSE)

  # Pass `max` to avoid base R's default footer
  print(out, max = max)

  invisible(x)
}

#' @export
obj_print_footer.clock_zoned_time <- function(x, ..., max) {
  clock_print_footer(x, max)
}

# Align left to match pillar_shaft.Date
# @export - lazy in .onLoad()
pillar_shaft.clock_zoned_time <- function(x, ...) {
  out <- format(x, print_zone_name = FALSE)
  pillar::new_pillar_shaft_simple(out, align = "left")
}

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

#' Convert to a zoned-time
#'
#' @description
#' `as_zoned_time()` converts `x` to a zoned-time. You generally convert
#' to a zoned time from either a sys-time or a naive time. Each are documented
#' on their own page:
#'
#' - [sys-time][as-zoned-time-sys-time]
#'
#' - [naive-time][as-zoned-time-naive-time]
#'
#' There are also convenience methods for converting to a zoned time from
#' native R date and date-time types:
#'
#' - [dates (Date)][as-zoned-time-Date]
#'
#' - [date-times (POSIXct / POSIXlt)][as-zoned-time-posixt]
#'
#' @inheritParams rlang::args_dots_empty
#'
#' @param x `[object]`
#'
#'   An object to convert to a zoned-time.
#'
#' @return A zoned-time vector.
#'
#' @export
#' @examples
#' x <- as.Date("2019-01-01")
#' as_zoned_time(x, "Europe/London")
#'
#' y <- as_naive_time(year_month_day(2019, 2, 1))
#' as_zoned_time(y, zone = "America/New_York")
as_zoned_time <- function(x, ...) {
  UseMethod("as_zoned_time")
}

#' @export
as_zoned_time.default <- function(x, ...) {
  stop_clock_unsupported_conversion(x, "clock_zoned_time")
}

#' @export
as_zoned_time.clock_zoned_time <- function(x, ...) {
  check_dots_empty0(...)
  x
}

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

#' @export
as_sys_time.clock_zoned_time <- function(x, ...) {
  check_dots_empty0(...)
  names <- clock_rcrd_names(x)
  precision <- zoned_time_precision_attribute(x)
  new_sys_time_from_fields(x, precision, names)
}

#' @export
as_naive_time.clock_zoned_time <- function(x, ...) {
  check_dots_empty0(...)
  names <- clock_rcrd_names(x)
  zone <- zoned_time_zone_attribute(x)
  precision <- zoned_time_precision_attribute(x)
  fields <- get_naive_time_cpp(x, precision, zone)
  new_naive_time_from_fields(fields, precision, names)
}

#' @export
as.character.clock_zoned_time <- function(x, ...) {
  format(x)
}

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

#' What is the current zoned-time?
#'
#' @description
#' `zoned_time_now()` returns the current time in the corresponding `zone`. It
#' is a wrapper around [sys_time_now()] that attaches the time zone.
#'
#' @details
#' The time is returned with a nanosecond precision, but the actual amount
#' of data returned is OS dependent. Usually, information at at least the
#' microsecond level is returned, with some platforms returning nanosecond
#' information.
#'
#' @param zone `[character(1)]`
#'
#'   A time zone to get the current time for.
#'
#' @return A zoned-time of the current time.
#'
#' @export
#' @examples
#' x <- zoned_time_now("America/New_York")
zoned_time_now <- function(zone) {
  check_zone(zone)
  names <- NULL
  sys_time <- sys_time_now()
  precision <- time_point_precision_attribute(sys_time)
  new_zoned_time_from_fields(sys_time, precision, zone, names)
}

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

#' Info: zoned-time
#'
#' @description
#' `zoned_time_info()` retrieves a set of low-level information generally not
#' required for most date-time manipulations. It returns a data frame with the
#' same columns as [sys_time_info()], but the `begin` and `end` columns are
#' zoned-times with the same time zone as `x`.
#'
#' @param x `[clock_zoned_time]`
#'
#'   A zoned-time.
#'
#' @return A data frame of low level information.
#'
#' @export
#' @examples
#' x <- year_month_day(2021, 03, 14, c(01, 03), c(59, 00), c(59, 00))
#' x <- as_naive_time(x)
#' x <- as_zoned_time(x, "America/New_York")
#'
#' # x[1] is in EST, x[2] is in EDT
#' x
#'
#' info <- zoned_time_info(x)
#' info
#'
#' # `end` can be used to iterate through daylight saving time transitions
#' zoned_time_info(info$end)
zoned_time_info <- function(x) {
  check_zoned_time(x)

  zone <- zoned_time_zone_attribute(x)
  x <- as_sys_time(x)

  out <- sys_time_info(x, zone)
  out$begin <- as_zoned_time(out$begin, zone = zone)
  out$end <- as_zoned_time(out$end, zone = zone)

  out
}

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

#' Get or set the time zone
#'
#' @description
#' - `zoned_time_zone()` gets the time zone.
#'
#' - `zoned_time_set_zone()` sets the time zone _without changing the
#' underlying instant_. This means that the result will represent the equivalent
#' time in the new time zone.
#'
#' @param x `[zoned_time]`
#'
#'   A zoned time to get or set the time zone of.
#'
#' @param zone `[character(1)]`
#'
#'   A valid time zone to switch to.
#'
#' @return
#' - `zoned_time_zone()` returns a string containing the time zone.
#'
#' - `zoned_time_set_zone()` returns `x` with an altered time zone attribute.
#' The underlying instant is _not_ changed.
#'
#' @name zoned-zone
#' @examples
#' x <- year_month_day(2019, 1, 1)
#' x <- as_zoned_time(as_naive_time(x), "America/New_York")
#' x
#'
#' zoned_time_zone(x)
#'
#' # Equivalent UTC time
#' zoned_time_set_zone(x, "UTC")
#'
#' # To force a new time zone with the same printed time,
#' # convert to a naive time that has no implied time zone,
#' # then convert back to a zoned time in the new time zone.
#' nt <- as_naive_time(x)
#' nt
#' as_zoned_time(nt, "UTC")
NULL

#' @rdname zoned-zone
#' @export
zoned_time_zone <- function(x) {
  check_zoned_time(x)
  zoned_time_zone_attribute(x)
}

#' @rdname zoned-zone
#' @export
zoned_time_set_zone <- function(x, zone) {
  check_zoned_time(x)
  check_zone(zone)
  zoned_time_set_zone_attribute(x, zone)
}

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

#' Precision: zoned-time
#'
#' `zoned_time_precision()` extracts the precision from a zoned-time. It
#' returns the precision as a single string.
#'
#' @param x `[clock_zoned_time]`
#'
#'   A zoned-time.
#'
#' @return A single string holding the precision of the zoned-time.
#'
#' @export
#' @examples
#' zoned_time_precision(zoned_time_now("America/New_York"))
zoned_time_precision <- function(x) {
  check_zoned_time(x)
  precision <- zoned_time_precision_attribute(x)
  precision <- precision_to_string(precision)
  precision
}

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

#' @export
add_years.clock_zoned_time <- function(x, n, ...) {
  details <- c(
    i = "Do you need to convert to a calendar first?",
    i = cli::format_inline(paste0(
      "Use {.fn as_naive_time} then {.fn as_year_month_day} to convert to a ",
      "calendar that supports {.fn add_years}."
    ))
  )
  stop_clock_unsupported(x, details = details)
}

#' @export
add_quarters.clock_zoned_time <- function(x, n, ...) {
  details <- c(
    i = "Do you need to convert to a calendar first?",
    i = cli::format_inline(paste0(
      "Use {.fn as_naive_time} then {.fn as_year_quarter_day} to convert to a ",
      "calendar that supports {.fn add_quarters}."
    ))
  )
  stop_clock_unsupported(x, details = details)
}

#' @export
add_months.clock_zoned_time <- function(x, n, ...) {
  details <- c(
    i = "Do you need to convert to a calendar first?",
    i = cli::format_inline(paste0(
      "Use {.fn as_naive_time} then {.fn as_year_month_day} to convert to a ",
      "calendar that supports {.fn add_months}."
    ))
  )
  stop_clock_unsupported(x, details = details)
}

#' @export
add_weeks.clock_zoned_time <- function(x, n, ...) {
  stop_clock_unsupported(x, details = advice_convert_to_time_point())
}

#' @export
add_days.clock_zoned_time <- function(x, n, ...) {
  stop_clock_unsupported(x, details = advice_convert_to_time_point())
}

#' @export
add_hours.clock_zoned_time <- function(x, n, ...) {
  stop_clock_unsupported(x, details = advice_convert_to_time_point())
}

#' @export
add_minutes.clock_zoned_time <- function(x, n, ...) {
  stop_clock_unsupported(x, details = advice_convert_to_time_point())
}

#' @export
add_seconds.clock_zoned_time <- function(x, n, ...) {
  stop_clock_unsupported(x, details = advice_convert_to_time_point())
}

#' @export
add_milliseconds.clock_zoned_time <- function(x, n, ...) {
  stop_clock_unsupported(x, details = advice_convert_to_time_point())
}

#' @export
add_microseconds.clock_zoned_time <- function(x, n, ...) {
  stop_clock_unsupported(x, details = advice_convert_to_time_point())
}

#' @export
add_nanoseconds.clock_zoned_time <- function(x, n, ...) {
  stop_clock_unsupported(x, details = advice_convert_to_time_point())
}

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

check_zone <- function(x, ..., arg = caller_arg(x), call = caller_env()) {
  check_string(x, arg = arg, call = call)

  if (zone_is_valid(x)) {
    return(invisible(NULL))
  }

  message <- c(
    "{.arg {arg}} must be a valid time zone name.",
    i = "{.str {x}} is invalid.",
    i = "Allowed time zone names are listed in {.run clock::tzdb_names()}."
  )

  cli::cli_abort(message, call = call)
}

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

check_zoned_time_precision <- function(x,
                                       ...,
                                       arg = caller_arg(x),
                                       call = caller_env()) {
  check_precision(
    x = x,
    values = c("second", "millisecond", "microsecond", "nanosecond"),
    arg = arg,
    call = call
  )
}

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

clock_init_zoned_time_utils <- function(env) {
  assign("clock_empty_zoned_time_utc_second", as_zoned_time(as_sys_time(duration_seconds()), "UTC"), envir = env)
  assign("clock_empty_zoned_time_utc_millisecond", as_zoned_time(as_sys_time(duration_milliseconds()), "UTC"), envir = env)
  assign("clock_empty_zoned_time_utc_microsecond", as_zoned_time(as_sys_time(duration_microseconds()), "UTC"), envir = env)
  assign("clock_empty_zoned_time_utc_nanosecond", as_zoned_time(as_sys_time(duration_nanoseconds()), "UTC"), envir = env)

  invisible(NULL)
}

Try the clock package in your browser

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

clock documentation built on May 31, 2023, 9:39 p.m.